diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx index 1ff17d92a..b44d9318d 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -20,7 +20,7 @@ along with this program. If not, see . /* eslint-disable no-case-declarations */ /* eslint-disable @typescript-eslint/no-var-requires */ import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common"; -import { ItemReference } from "@notesnook/core"; +import { ItemReference, parseInternalLink } from "@notesnook/core"; import type { Attachment } from "@notesnook/editor"; import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events"; import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; @@ -471,9 +471,8 @@ export const useEditorEvents = ( relationType: "from", title: strings.dataTypesPluralCamelCase.reminder(), onAdd: async () => { - const reminderFeature = await isFeatureAvailable( - "activeReminders" - ); + const reminderFeature = + await isFeatureAvailable("activeReminders"); if (!reminderFeature.isAllowed) { ToastManager.show({ type: "info", @@ -533,7 +532,59 @@ export const useEditorEvents = ( downloadAttachment((editorMessage.value as Attachment)?.hash, true); break; } + case EditorEvents.getLinkData: { + const url = (editorMessage.value as any)?.url as string; + const link = parseInternalLink(url); + if (!link) return; + switch (link.type) { + case "note": + case "notebook": + case "tag": { + const table = + link.type === "note" + ? "notes" + : link.type === "notebook" + ? "notebooks" + : "tags"; + const item = await db + .sql() + .selectFrom(table) + .where("id", "=", link.id) + .select("title") + .executeTakeFirst(); + editor.postMessage(NativeEvents.resolve, { + resolverId: editorMessage.resolverId, + data: { + type: link.type, + title: item?.title || "" + } + }); + break; + } + case "color": { + const color = await db + .sql() + .selectFrom("colors") + .where("id", "=", link.id) + .select(["title", "colorCode"]) + .executeTakeFirst(); + + editor.postMessage(NativeEvents.resolve, { + resolverId: editorMessage.resolverId, + data: { + type: "color", + title: color?.title || "", + metadata: { + colorCode: color?.colorCode || "" + } + } + }); + } + } + + break; + } case EditorEvents.getAttachmentData: { const data = (editorMessage.value as any)?.attachment as Attachment; diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index f55e00efc..a4efbd1ba 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/mobile", - "version": "3.3.17", + "version": "3.3.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/mobile", - "version": "3.3.17", + "version": "3.3.18", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index a80977140..291f95441 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -69,6 +69,7 @@ import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog"; import { ConfirmDialog } from "../../dialogs/confirm"; import { strings } from "@notesnook/intl"; import { handleInternalLink } from "../../common"; +import { db } from "../../common/db"; export type OnChangeHandler = ( content: () => string, @@ -426,6 +427,48 @@ function TipTap(props: TipTapProps) { const link = parseInternalLink(url); if (link) handleInternalLink(url, openInNewTab); else window.open(url, "_blank"); + }, + getLinkData: async (url) => { + const link = parseInternalLink(url); + if (!link) return; + + switch (link.type) { + case "note": + case "notebook": + case "tag": { + const table = + link.type === "note" + ? "notes" + : link.type === "notebook" + ? "notebooks" + : "tags"; + const item = await db + .sql() + .selectFrom(table) + .where("id", "=", link.id) + .select("title") + .executeTakeFirst(); + return { + type: link.type, + title: item?.title || "" + }; + } + case "color": { + const color = await db + .sql() + .selectFrom("colors") + .where("id", "=", link.id) + .select(["title", "colorCode"]) + .executeTakeFirst(); + return { + type: "color", + title: color?.title || "", + metadata: { + colorCode: color?.colorCode || "" + } + }; + } + } } }; }, [ diff --git a/packages/editor-mobile/package-lock.json b/packages/editor-mobile/package-lock.json index 24441ca6c..7c735f048 100644 --- a/packages/editor-mobile/package-lock.json +++ b/packages/editor-mobile/package-lock.json @@ -68,7 +68,7 @@ "@notesnook/intl": "file:../intl", "@notesnook/theme": "file:../theme", "@notesnook/ui": "file:../ui", - "@social-embed/lib": "^0.1.0-next.7", + "@social-embed/lib": "^0.1.0-next.11", "@tiptap/core": "2.6.6", "@tiptap/extension-blockquote": "^2.6.6", "@tiptap/extension-bullet-list": "^2.6.6", diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx index 697893636..66893f6cf 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -162,6 +162,11 @@ const Tiptap = ({ attachment ) as Promise; }, + getLinkData: (url: string) => { + return postAsyncWithTimeout(EditorEvents.getLinkData, { + url: url + }); + }, createInternalLink(attributes) { return postAsyncWithTimeout(EditorEvents.createInternalLink, { attributes diff --git a/packages/editor-mobile/src/utils/editor-events.ts b/packages/editor-mobile/src/utils/editor-events.ts index 83b8da698..c69673eb9 100644 --- a/packages/editor-mobile/src/utils/editor-events.ts +++ b/packages/editor-mobile/src/utils/editor-events.ts @@ -55,5 +55,6 @@ export const EditorEvents = { goForward: "editor-events:go-forward", saveScroll: "editor-events:save-scroll", newNote: "editor-events:new-note", - downloadCsv: "editor-events:download-csv" + downloadCsv: "editor-events:download-csv", + getLinkData: "editor-events:get-link-data" } as const; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 122c4bc77..cc8449401 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -88,12 +88,14 @@ import { strings } from "@notesnook/intl"; import { InlineCode } from "./extensions/inline-code/inline-code.js"; import { FontLigature } from "./extensions/font-ligature/font-ligature.js"; import { SearchResult } from "./extensions/search-result/search-result.js"; +import { LinkData } from "./types.js"; interface TiptapStorage { dateFormat?: DateTimeOptions["dateFormat"]; timeFormat?: DateTimeOptions["timeFormat"]; dayFormat?: DateTimeOptions["dayFormat"]; openLink?: (url: string, openInNewTab?: boolean) => void; + getLinkData?: (url: string) => Promise; downloadAttachment?: (attachment: Attachment) => void; openAttachmentPicker?: (type: AttachmentType) => void; previewAttachment?: (attachment: Attachment) => void; @@ -148,6 +150,7 @@ const useTiptap = ( openAttachmentPicker, previewAttachment, openLink, + getLinkData, onBeforeCreate, dateFormat, timeFormat, @@ -285,7 +288,13 @@ const useTiptap = ( }).configure({ openOnClick: !isMobile, autolink: false, - linkOnPaste: true + linkOnPaste: true, + protocols: [ + { + scheme: "nn", + optionalSlashes: true + } + ] }), Table.configure({ resizable: true, @@ -395,6 +404,7 @@ const useTiptap = ( editor.storage.createInternalLink = createInternalLink; editor.storage.getAttachmentData = getAttachmentData; editor.storage.downloadCsvTable = downloadCsvTable; + editor.storage.getLinkData = getLinkData; if (onBeforeCreate) onBeforeCreate({ editor }); }, diff --git a/packages/editor/src/toolbar/tools/link.tsx b/packages/editor/src/toolbar/tools/link.tsx index 266ac363c..d42831d6e 100644 --- a/packages/editor/src/toolbar/tools/link.tsx +++ b/packages/editor/src/toolbar/tools/link.tsx @@ -19,20 +19,24 @@ along with this program. If not, see . import { ToolProps } from "../types.js"; import { ToolButton } from "../components/tool-button.js"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ResponsivePresenter } from "../../components/responsive/index.js"; import { LinkPopup } from "../popups/link-popup.js"; import { useToolbarLocation } from "../stores/toolbar-store.js"; import { MoreTools } from "../components/more-tools.js"; import { useRefValue } from "../../hooks/use-ref-value.js"; import { findMark, selectionToOffset } from "../../utils/prosemirror.js"; -import { Flex, Link } from "@theme-ui/components"; +import { Flex, Link, Text } from "@theme-ui/components"; import { ImageNode } from "../../extensions/image/index.js"; import { Link as LinkNode } from "../../extensions/link/index.js"; import { getMarkAttributes } from "@tiptap/core"; import { useHoverPopupContext } from "../floating-menus/hover-popup/context.js"; import { strings } from "@notesnook/intl"; import { find } from "linkifyjs"; +import { Icons } from "../icons.js"; +import { mdiNoteOutline, mdiBookOutline, mdiPound } from "@mdi/js"; +import { Icon } from "@notesnook/ui"; +import { LinkData } from "../../types.js"; export function LinkSettings(props: ToolProps) { const { editor } = props; @@ -204,36 +208,62 @@ export function OpenLink(props: ToolProps) { ); const { node } = selectedNode.current || {}; const link = node ? findMark(node, "link") : null; - if (!link) return null; - const href = link?.attrs.href; - if (!href) return null; + const href = link?.attrs.href ?? null; + const [loading, setLoading] = useState(false); + const [linkData, setLinkData] = useState(undefined); + + useEffect(() => { + if (!href) return; + + (async () => { + try { + setLoading(true); + const result = await editor.storage.getLinkData?.(href); + setLinkData(result); + setLoading(false); + } catch (e) { + setLoading(false); + } + })(); + }, [href]); + + if (!link || !href) return null; + + const title = linkData?.title || href; return ( - { - e.preventDefault(); - editor.storage.openLink?.(href); - hide(); - }} - target="_blank" - variant="body" - sx={{ - fontSize: "subBody", - fontFamily: "body", - mr: 1, - color: "accent", - maxWidth: [150, 250], - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - ":visited": { color: "accent" }, - ":hover": { color: "accent", opacity: 0.8 } - }} - > - {href} - + {linkData?.type && ( + + )} + {loading ? ( + {strings.loading()} + ) : ( + { + e.preventDefault(); + editor.storage.openLink?.(href); + hide(); + }} + target="_blank" + variant="body" + sx={{ + fontSize: "subBody", + fontFamily: "body", + mr: 4, + color: "accent", + maxWidth: [150, 250], + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + ":visited": { color: "accent" }, + ":hover": { color: "accent", opacity: 0.8 } + }} + > + {title} + + )} = { + note: mdiNoteOutline, + notebook: mdiBookOutline, + tag: mdiPound, + color: Icons.circle +}; + +function LinkTypeIcon({ + type, + metadata +}: { + type: LinkData["type"]; + metadata?: LinkData["metadata"]; +}) { + const path = LINK_TYPE_ICONS[type]; + if (!path) return null; + + const color = + type === "color" && metadata?.colorCode + ? metadata.colorCode + : "icon-secondary"; + + return ; +} + export function isInternalLink(href?: string | null) { return typeof href === "string" ? href.startsWith("nn://") : false; } diff --git a/packages/editor/src/types.ts b/packages/editor/src/types.ts index a906b2144..9df0313fc 100644 --- a/packages/editor/src/types.ts +++ b/packages/editor/src/types.ts @@ -25,6 +25,12 @@ export type PermissionRequestEvent = CustomEvent<{ silent: boolean; }>; +export type LinkData = { + type: "note" | "notebook" | "color" | "tag"; + title?: string; + metadata?: Record; +}; + export class Editor extends TiptapEditor { private mutex: Mutex = new Mutex();