From 2fb04577a08e6698f56b4fa5ccff90b4615e98c4 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 13 Apr 2026 10:15:30 -1000 Subject: [PATCH 01/29] feat: rich text editor with formatting toolbar, image ref chips, and TipTap integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace plain textarea message composer with TipTap-based rich text editor. - Add FormattingToolbar with bold, italic, strikethrough, code, and link controls - Add Toggle UI primitive (shared component) - Integrate image uploads as context chips with autocomplete suggestions (ImageRefAutocomplete, imageRefExtension, useImageRefSuggestions) - New useRichTextEditor hook encapsulating TipTap setup - New ComposerAttachments component for uploaded media display - Remove legacy ComposerMentionOverlay (replaced by TipTap mention/ref system) - Update MessageComposer and MessageComposerToolbar for new editor - Keyboard shortcuts: ⌘B bold, ⌘I italic, ⌘K link, ⌘S sidebar toggle - Update ChannelPane, sidebar, globals.css for layout/style adjustments - Update dependencies (Cargo.toml, package.json, pnpm-lock.yaml) --- Cargo.toml | 2 +- desktop/package.json | 9 + desktop/pnpm-lock.yaml | 754 +++++++++++++++++- .../src/features/channels/ui/ChannelPane.tsx | 3 +- .../messages/lib/imageRefExtension.ts | 67 ++ .../messages/lib/useImageRefSuggestions.ts | 113 +++ .../features/messages/lib/useMediaUpload.ts | 65 +- .../messages/lib/useRichTextEditor.ts | 196 +++++ .../messages/ui/ComposerAttachments.tsx | 78 ++ .../messages/ui/ComposerMentionOverlay.tsx | 116 --- .../messages/ui/FormattingToolbar.tsx | 179 +++++ .../messages/ui/ImageRefAutocomplete.tsx | 64 ++ .../features/messages/ui/MessageComposer.tsx | 553 +++++++------ .../messages/ui/MessageComposerToolbar.tsx | 22 +- desktop/src/shared/styles/globals.css | 97 +++ desktop/src/shared/ui/sidebar.tsx | 2 +- desktop/src/shared/ui/toggle.tsx | 43 + 17 files changed, 1973 insertions(+), 390 deletions(-) create mode 100644 desktop/src/features/messages/lib/imageRefExtension.ts create mode 100644 desktop/src/features/messages/lib/useImageRefSuggestions.ts create mode 100644 desktop/src/features/messages/lib/useRichTextEditor.ts create mode 100644 desktop/src/features/messages/ui/ComposerAttachments.tsx delete mode 100644 desktop/src/features/messages/ui/ComposerMentionOverlay.tsx create mode 100644 desktop/src/features/messages/ui/FormattingToolbar.tsx create mode 100644 desktop/src/features/messages/ui/ImageRefAutocomplete.tsx create mode 100644 desktop/src/shared/ui/toggle.tsx diff --git a/Cargo.toml b/Cargo.toml index 33ea3bc18..643d4ebe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ "crates/sprout-cli", "crates/sprout-sdk", ] -exclude = ["desktop/src-tauri"] +exclude = ["desktop/src-tauri", ".worktree/chat-style/desktop/src-tauri"] resolver = "2" [workspace.package] diff --git a/desktop/package.json b/desktop/package.json index 25848ae1b..8643690ec 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -34,6 +34,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.168.10", @@ -42,11 +43,18 @@ "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.0", + "@tiptap/core": "^3.22.3", + "@tiptap/extension-link": "^3.22.3", + "@tiptap/extension-placeholder": "^3.22.3", + "@tiptap/pm": "^3.22.3", + "@tiptap/react": "^3.22.3", + "@tiptap/starter-kit": "^3.22.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "jdenticon": "^3.3.0", "lucide-react": "^0.577.0", + "motion": "^12.38.0", "react": "^19.1.0", "react-diff-view": "^3.3.2", "react-dom": "^19.1.0", @@ -55,6 +63,7 @@ "remark-gfm": "^4.0.1", "shiki": "^4.0.2", "tailwind-merge": "^3.5.0", + "tiptap-markdown": "^0.9.0", "yaml": "^2.8.3" }, "devDependencies": { diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index cdbbd4f23..de2801d2f 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -62,6 +65,24 @@ importers: '@tauri-apps/plugin-updater': specifier: ^2.10.0 version: 2.10.0 + '@tiptap/core': + specifier: ^3.22.3 + version: 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-link': + specifier: ^3.22.3 + version: 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-placeholder': + specifier: ^3.22.3 + version: 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/pm': + specifier: ^3.22.3 + version: 3.22.3 + '@tiptap/react': + specifier: ^3.22.3 + version: 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/starter-kit': + specifier: ^3.22.3 + version: 3.22.3 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -77,6 +98,9 @@ importers: lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) + motion: + specifier: ^12.38.0 + version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.1.0 version: 19.2.4 @@ -101,6 +125,9 @@ importers: tailwind-merge: specifier: ^3.5.0 version: 3.5.0 + tiptap-markdown: + specifier: ^0.9.0 + version: 0.9.0(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) yaml: specifier: ^2.8.3 version: 2.8.3 @@ -837,6 +864,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -938,6 +978,9 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -993,7 +1036,6 @@ packages: resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} @@ -1035,7 +1077,6 @@ packages: resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} @@ -1278,6 +1319,160 @@ packages: '@tauri-apps/plugin-updater@2.10.0': resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==} + '@tiptap/core@3.22.3': + resolution: {integrity: sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==} + peerDependencies: + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-blockquote@3.22.3': + resolution: {integrity: sha512-IaUx3zh7yLHXzIXKL+fw/jzFhsIImdhJyw0lMhe8FfYrefFqXJFYW/sey6+L/e8B3AWvTksPA6VBwefzbH77JA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-bold@3.22.3': + resolution: {integrity: sha512-tysipHla2zCWr8XNIWRaW9O+7i7/SoEqnRqSRUUi2ailcJjlia+RBy3RykhkgyThrQDStu5KGBS/UvrXwA+O1A==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-bubble-menu@3.22.3': + resolution: {integrity: sha512-Y6zQjh0ypDg32HWgICEvmPSKjGLr39k3aDxxt/H0uQEZSfw4smT0hxUyyyjVjx68C6t6MTnwdfz0hPI5lL68vQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-bullet-list@3.22.3': + resolution: {integrity: sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-code-block@3.22.3': + resolution: {integrity: sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-code@3.22.3': + resolution: {integrity: sha512-wafWTDQOuMKtXpZEuk1PFQmzopabBciNLryL90MB9S03MNLaQQZYLnmYkDBlzAaLAbgF5QiC+2XZQEBQuTVjFQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-document@3.22.3': + resolution: {integrity: sha512-MCSr1PFPtTd++lA3H1RNgqAczAE59XXJ5wUFIQf2F+/0DPY5q2SU4g5QsNJVxPPft5mrNT4C6ty8xBPrALFEdA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-dropcursor@3.22.3': + resolution: {integrity: sha512-taXq9Tl5aybdFbptJtFRHX9LFJzbXphAbPp4/vutFyTrBu5meXDxuS+B9pEmE+Or0XcolTlW2nDZB0Tqnr18JQ==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-floating-menu@3.22.3': + resolution: {integrity: sha512-0f8b4KZ3XKai8GXWseIYJGdOfQr3evtFbBo3U08zy2aYzMMXWG0zEF7qe5/oiYp2aZ95edjjITnEceviTsZkIg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-gapcursor@3.22.3': + resolution: {integrity: sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-hard-break@3.22.3': + resolution: {integrity: sha512-J0v8I99y9tbvVmgKYKzKP/JYNsWaZYS7avn4rzLft2OhnyTfwt3OoY8DtpHmmi6apSUaCtoWHWta/TmoEfK1nQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-heading@3.22.3': + resolution: {integrity: sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-horizontal-rule@3.22.3': + resolution: {integrity: sha512-wI2bFzScs+KgWeBH/BtypcVKeYelCyqV0RG8nxsZMWtPrBhqixzNd0Oi3gEKtjSjKUqMQ/kjJAIRuESr5UzlHA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-italic@3.22.3': + resolution: {integrity: sha512-LteA4cb4EGCiUtrK2JHvDF/Zg0/YqV4DUyHhAAho+oGEQDupZlsS6m0ia5wQcclkiTLzsoPrwcSNu6RDGQ16wQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-link@3.22.3': + resolution: {integrity: sha512-S8/P2o9pv6B3kqLjH2TRWwSAximGbciNc6R8/QcN6HWLYxp0N0JoqN3rZHl9VWIBAGRWc4zkt80dhqrl2xmgfQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-list-item@3.22.3': + resolution: {integrity: sha512-80CNf4oO5y8+LdckT4CyMe1t01EyhpRrQC9H45JW20P7559Nrchp5my3vvMtIAJbpTPPZtcB7LwdzWGKsG5drg==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-list-keymap@3.22.3': + resolution: {integrity: sha512-pKuyj5llu35zd/s2u/H9aydKZjmPRAIK5P1q/YXULhhCNln2RnmuRfQ5NklAqTD3yGciQ2lxDwwf7J6iw3ergA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-list@3.22.3': + resolution: {integrity: sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-ordered-list@3.22.3': + resolution: {integrity: sha512-orAghtmd+K4Euu4BgI1hG+iZDXBYOyl5YTwiLBc2mQn+pqtZ9LqaH2us4ETwEwNP3/IWXGSAimUZ19nuL+eM2w==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-paragraph@3.22.3': + resolution: {integrity: sha512-oO7rhfyhEuwm+50s9K3GZPjYyEEEvFAvm1wXopvZnhbkBLydIWImBfrZoC5IQh4/sRDlTIjosV2C+ji5y0tUSg==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-placeholder@3.22.3': + resolution: {integrity: sha512-7vbtlDVO00odqCnsMSmA4b6wjL5PFdfExFsdsDO0K0VemqHZ/doIRx/tosNUD1VYSOyKQd8U7efUjkFyVoIPlg==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-strike@3.22.3': + resolution: {integrity: sha512-jY2InoUlKkuk5KHoIDGdML1OCA2n6PRHAtxwHNkAmiYh0Khf0zaVPGFpx4dgQrN7W5Q1WE6oBZnjrvy6qb7w0g==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-text@3.22.3': + resolution: {integrity: sha512-Q9R7JsTdomP5uUjtPjNKxHT1xoh/i9OJZnmgJLe7FcgZEaPOQ3bWxmKZoLZQfDfZjyB8BtH+Hc7nUvhCMOePxw==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-underline@3.22.3': + resolution: {integrity: sha512-Ch6CBWRa5w90yYSPUW6x9Py9JdrXMqk3pZ9OIlMYD8A7BqyZGfiHerX7XDMYDS09KjyK3U9XH60/zxYOzXdDLA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extensions@3.22.3': + resolution: {integrity: sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/pm@3.22.3': + resolution: {integrity: sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==} + + '@tiptap/react@3.22.3': + resolution: {integrity: sha512-6MNr6z0PxwfJFs+BKhHcvPNvY+UV1PXgqzTiTM4Z9guml84iVZxv7ZOCSj1dFYTr3Bf1MiOs4hT1yvBFlTfIaQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.22.3': + resolution: {integrity: sha512-vdW/Oo1fdwTL1VOQ5YYbTov00ANeHLquBVEZyL/EkV7Xv5io9rXQsCysJfTSHhiQlyr2MtWFB4+CPGuwXjQWOQ==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1302,9 +1497,27 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/linkify-it@3.0.5': + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@13.0.9': + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@1.0.5': + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1325,6 +1538,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1353,6 +1569,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -1444,6 +1663,9 @@ packages: cookie-es@2.0.1: resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1493,6 +1715,10 @@ packages: emoji-mart@5.6.0: resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -1502,6 +1728,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -1517,6 +1747,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1540,6 +1774,20 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1666,6 +1914,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -1684,6 +1938,13 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + markdown-it-task-lists@2.1.1: + resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -1735,6 +1996,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1827,6 +2091,26 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1870,6 +2154,9 @@ packages: oniguruma-to-es@4.3.5: resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -1963,6 +2250,68 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2072,6 +2421,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2159,6 +2511,11 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tiptap-markdown@0.9.0: + resolution: {integrity: sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==} + peerDependencies: + '@tiptap/core': ^3.0.1 + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2185,6 +2542,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -2290,6 +2650,9 @@ packages: yaml: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -2917,6 +3280,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -3002,6 +3376,8 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@remirror/core-constants@3.0.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.59.0': @@ -3281,6 +3657,187 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tiptap/core@3.22.3(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-blockquote@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-bold@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + optional: true + + '@tiptap/extension-bullet-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-code@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-document@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-dropcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + optional: true + + '@tiptap/extension-gapcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-hard-break@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-heading@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-horizontal-rule@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-italic@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-link@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-list-keymap@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-ordered-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-paragraph@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-placeholder@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-strike@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-text@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-underline@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/pm@3.22.3': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/react@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.22.3': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-blockquote': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-bold': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-bullet-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-code': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-code-block': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-document': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-dropcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-gapcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-hard-break': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-heading': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-horizontal-rule': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-italic': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-link': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-list-item': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-list-keymap': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-ordered-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-paragraph': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-strike': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-text': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-underline': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -3316,10 +3873,28 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/linkify-it@3.0.5': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@13.0.9': + dependencies: + '@types/linkify-it': 3.0.5 + '@types/mdurl': 1.0.5 + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@1.0.5': {} + + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} '@types/node@25.5.2': @@ -3338,6 +3913,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@25.5.2)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.3))': @@ -3365,6 +3942,8 @@ snapshots: arg@5.0.2: {} + argparse@2.0.1: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -3455,6 +4034,8 @@ snapshots: cookie-es@2.0.1: {} + crelt@1.0.6: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -3487,6 +4068,8 @@ snapshots: emoji-mart@5.6.0: {} + entities@4.5.0: {} + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -3518,6 +4101,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} esprima@4.0.1: {} @@ -3526,6 +4111,8 @@ snapshots: extend@3.0.2: {} + fast-equals@5.4.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3548,6 +4135,15 @@ snapshots: fraction.js@5.3.4: {} + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + fsevents@2.3.2: optional: true @@ -3669,6 +4265,12 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + lodash@4.17.23: {} longest-streak@3.1.0: {} @@ -3685,6 +4287,17 @@ snapshots: dependencies: react: 19.2.4 + markdown-it-task-lists@2.1.1: {} + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} mdast-util-find-and-replace@3.0.2: @@ -3845,6 +4458,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -4043,6 +4658,20 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + ms@2.1.3: {} mz@2.7.0: @@ -4083,6 +4712,8 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + orderedmap@2.1.1: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -4158,6 +4789,111 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + punycode.js@2.3.1: {} + queue-microtask@1.2.3: {} react-diff-view@3.3.2(react@19.2.4): @@ -4331,6 +5067,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4440,6 +5178,14 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tiptap-markdown@0.9.0(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)): + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@types/markdown-it': 13.0.9 + markdown-it: 14.1.1 + markdown-it-task-lists: 2.1.1 + prosemirror-markdown: 1.13.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4461,6 +5207,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@2.1.0: {} + undici-types@7.18.2: {} unified@11.0.5: @@ -4555,6 +5303,8 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + w3c-keyname@2.2.8: {} + warning@4.0.3: dependencies: loose-envify: 1.4.0 diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index e8693019c..a153854c9 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -118,8 +118,7 @@ export const ChannelPane = React.memo(function ChannelPane({ !activeChannel || !activeChannel.isMember || activeChannel.archivedAt !== null || - activeChannel.channelType === "forum" || - isSending + activeChannel.channelType === "forum" } editTarget={editTarget} isSending={isSending} diff --git a/desktop/src/features/messages/lib/imageRefExtension.ts b/desktop/src/features/messages/lib/imageRefExtension.ts new file mode 100644 index 000000000..614b2f7f0 --- /dev/null +++ b/desktop/src/features/messages/lib/imageRefExtension.ts @@ -0,0 +1,67 @@ +import { mergeAttributes, Node } from "@tiptap/core"; + +/** + * Custom Tiptap inline node for image reference chips. + * + * Stores the attachment URL and short hash. Renders as a non-editable + * inline pill like `![a3f2]`. On send, the composer resolves these to + * `![image](url)` markdown. + */ +export const ImageRefNode = Node.create({ + name: "imageRef", + group: "inline", + inline: true, + atom: true, // non-editable, treated as a single unit + + addAttributes() { + return { + url: { default: null }, + hash: { default: null }, + mediaType: { default: "image" }, + thumb: { default: null }, + }; + }, + + parseHTML() { + return [{ tag: "span[data-image-ref]" }]; + }, + + addStorage() { + return { + markdown: { + serialize( + state: { write: (text: string) => void }, + node: { attrs: { hash?: string } }, + ) { + state.write(`![${node.attrs.hash ?? "?"}]`); + }, + parse: {}, + }, + }; + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + const thumb = HTMLAttributes.thumb || HTMLAttributes.url || ""; + const hash = HTMLAttributes.hash ?? "?"; + + return [ + "span", + mergeAttributes(HTMLAttributes, { + "data-image-ref": "", + class: + "inline-flex items-center rounded-lg overflow-hidden align-baseline cursor-default select-none border border-border/50", + contenteditable: "false", + title: `![${hash}]`, + }), + [ + "img", + { + src: thumb, + alt: `![${hash}]`, + class: "inline-block h-16 max-w-48 rounded-lg object-contain", + draggable: "false", + }, + ], + ]; + }, +}); diff --git a/desktop/src/features/messages/lib/useImageRefSuggestions.ts b/desktop/src/features/messages/lib/useImageRefSuggestions.ts new file mode 100644 index 000000000..721addc52 --- /dev/null +++ b/desktop/src/features/messages/lib/useImageRefSuggestions.ts @@ -0,0 +1,113 @@ +import * as React from "react"; + +import type { BlobDescriptor } from "@/shared/api/tauri"; +import { shortHash } from "./useMediaUpload"; + +export type ImageRefSuggestion = { + url: string; + hash: string; + type: string; + thumb?: string; +}; + +/** + * Detects `![` typed in the editor and shows suggestions from attached + * images. Lightweight alternative to @tiptap/suggestion — works with + * plain text + cursor position from the existing bridge. + */ +export function useImageRefSuggestions(attachments: BlobDescriptor[]) { + const [isOpen, setIsOpen] = React.useState(false); + const [query, setQuery] = React.useState(""); + const [selectedIndex, setSelectedIndex] = React.useState(0); + + const suggestions: ImageRefSuggestion[] = React.useMemo(() => { + const items = attachments.map((a) => ({ + url: a.url, + hash: shortHash(a.sha256), + type: a.type, + thumb: a.thumb, + })); + if (!query) return items; + return items.filter((s) => + s.hash.toLowerCase().includes(query.toLowerCase()), + ); + }, [attachments, query]); + + /** Call on every editor update with the plain text and cursor position. */ + const updateQuery = React.useCallback( + (text: string, cursor: number) => { + if (attachments.length === 0) { + if (isOpen) setIsOpen(false); + return; + } + + // Look for `![` before cursor, not yet closed with `]` + const before = text.slice(0, cursor); + const match = /!\[([^\]]*)$/.exec(before); + + if (match) { + setQuery(match[1]); + setIsOpen(true); + setSelectedIndex(0); + } else if (isOpen) { + setIsOpen(false); + setQuery(""); + } + }, + [attachments.length, isOpen], + ); + + /** Handle keyboard navigation. Returns { handled, suggestion? }. */ + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (!isOpen || suggestions.length === 0) { + return { handled: false } as const; + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + setSelectedIndex((i) => (i + 1) % suggestions.length); + return { handled: true } as const; + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + setSelectedIndex((i) => (i <= 0 ? suggestions.length - 1 : i - 1)); + return { handled: true } as const; + } + + if (event.key === "Tab" || event.key === "Enter") { + event.preventDefault(); + const suggestion = suggestions[selectedIndex]; + setIsOpen(false); + setQuery(""); + return { handled: true, suggestion } as const; + } + + if (event.key === "Escape") { + event.preventDefault(); + setIsOpen(false); + setQuery(""); + return { handled: true } as const; + } + + return { handled: false } as const; + }, + [isOpen, suggestions, selectedIndex], + ); + + const clear = React.useCallback(() => { + setIsOpen(false); + setQuery(""); + setSelectedIndex(0); + }, []); + + return { + isOpen, + suggestions, + selectedIndex, + updateQuery, + handleKeyDown, + clear, + }; +} diff --git a/desktop/src/features/messages/lib/useMediaUpload.ts b/desktop/src/features/messages/lib/useMediaUpload.ts index 981492283..c07b6ba38 100644 --- a/desktop/src/features/messages/lib/useMediaUpload.ts +++ b/desktop/src/features/messages/lib/useMediaUpload.ts @@ -6,7 +6,7 @@ import { uploadMediaBytes, } from "@/shared/api/tauri"; -const ALLOWED_TYPES = [ +export const ALLOWED_MEDIA_TYPES = [ "image/jpeg", "image/png", "image/gif", @@ -18,14 +18,22 @@ const ALLOWED_TYPES = [ "video/x-msvideo", ]; +/** + * First 4 hex chars of the sha256 — used as a short display name. + * Note: 4 hex chars = 65,536 possible values. Collision is unlikely + * within a single message's attachments but theoretically possible. + * If collisions become an issue, extend to 6+ chars. + */ +export function shortHash(sha256: string): string { + return sha256.slice(0, 4); +} + type UploadState = { status: "idle" | "uploading" | "error"; message?: string; }; -export function useMediaUpload( - setContent: React.Dispatch>, -) { +export function useMediaUpload() { const [uploadState, setUploadState] = React.useState({ status: "idle", }); @@ -34,18 +42,10 @@ export function useMediaUpload( const pendingImetaRef = React.useRef(pendingImeta); pendingImetaRef.current = pendingImeta; - const onUploaded = React.useCallback( - (descriptor: BlobDescriptor) => { - const isVideo = descriptor.type === "video/mp4"; - const markdown = isVideo - ? `\n![video](${descriptor.url})\n` - : `\n![image](${descriptor.url})\n`; - setContent((prev) => prev + markdown); - setPendingImeta((prev) => [...prev, descriptor]); - setUploadState({ status: "idle" }); - }, - [setContent], - ); + const onUploaded = React.useCallback((descriptor: BlobDescriptor) => { + setPendingImeta((prev) => [...prev, descriptor]); + setUploadState({ status: "idle" }); + }, []); const handlePaperclip = React.useCallback(async () => { setUploadState({ status: "uploading" }); @@ -70,7 +70,7 @@ export function useMediaUpload( const file = files[0]; if (!file) return; - if (!ALLOWED_TYPES.includes(file.type)) { + if (!ALLOWED_MEDIA_TYPES.includes(file.type)) { setUploadState({ status: "error", message: @@ -99,9 +99,14 @@ export function useMediaUpload( ); const handlePaste = React.useCallback( - async (event: React.ClipboardEvent) => { + async (event: { + clipboardData: DataTransfer; + preventDefault: () => void; + }) => { const items = Array.from(event.clipboardData.items); - const mediaItem = items.find((item) => ALLOWED_TYPES.includes(item.type)); + const mediaItem = items.find((item) => + ALLOWED_MEDIA_TYPES.includes(item.type), + ); if (!mediaItem) return; event.preventDefault(); @@ -120,6 +125,26 @@ export function useMediaUpload( [onUploaded], ); + /** Upload a File directly — used by Tiptap's editorProps.handlePaste. */ + const uploadFile = React.useCallback( + async (file: File) => { + if (!ALLOWED_MEDIA_TYPES.includes(file.type)) return; + setUploadState({ status: "uploading" }); + try { + const buffer = await file.arrayBuffer(); + const descriptor = await uploadMediaBytes([...new Uint8Array(buffer)]); + onUploaded(descriptor); + } catch (err) { + setUploadState({ status: "error", message: String(err) }); + } + }, + [onUploaded], + ); + + const removeAttachment = React.useCallback((url: string) => { + setPendingImeta((prev) => prev.filter((d) => d.url !== url)); + }, []); + const isUploading = uploadState.status === "uploading"; return { @@ -130,8 +155,10 @@ export function useMediaUpload( isUploading, pendingImeta, pendingImetaRef, + removeAttachment, setPendingImeta, setUploadState, + uploadFile, uploadState, }; } diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts new file mode 100644 index 000000000..bd8de6602 --- /dev/null +++ b/desktop/src/features/messages/lib/useRichTextEditor.ts @@ -0,0 +1,196 @@ +import * as React from "react"; + +import { Markdown as TiptapMarkdown } from "tiptap-markdown"; +import { useEditor, type Editor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Placeholder from "@tiptap/extension-placeholder"; +import Link from "@tiptap/extension-link"; + +import { ImageRefNode } from "./imageRefExtension"; + +export type RichTextEditorOptions = { + placeholder?: string; + onUpdate?: (info: { markdown: string; text: string }) => void; + editable?: boolean; +}; + +/** + * Creates and manages a Tiptap editor configured for Markdown output. + * + * The editor uses StarterKit (bold, italic, strike, code, blockquote, lists, + * headings, code blocks, hard breaks) plus Link and the tiptap-markdown + * extension for serialisation. + * + * `getMarkdown()` returns the current document as a Markdown string. + */ +export function useRichTextEditor({ + placeholder, + onUpdate, + editable = true, +}: RichTextEditorOptions) { + const onUpdateRef = React.useRef(onUpdate); + onUpdateRef.current = onUpdate; + + const editor = useEditor( + { + extensions: [ + StarterKit.configure({ + // Use hard breaks (Shift+Enter) — Enter submits the message. + hardBreak: { + keepMarks: true, + }, + }), + Placeholder.configure({ + placeholder: placeholder ?? "Write a message…", + }), + Link.configure({ + openOnClick: false, + autolink: true, + linkOnPaste: true, + HTMLAttributes: { + class: "text-primary underline underline-offset-4 cursor-pointer", + }, + }), + TiptapMarkdown.configure({ + html: false, + transformPastedText: true, + transformCopiedText: true, + breaks: true, + }), + ImageRefNode, + ], + editorProps: { + attributes: { + class: + "min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-6 md:leading-6 shadow-none focus-visible:ring-0 caret-foreground outline-none prose-sm max-w-none", + "data-testid": "message-input", + }, + }, + onUpdate: ({ editor: ed }) => { + const markdown = getMarkdownFromEditor(ed); + const text = ed.state.doc.textContent; + onUpdateRef.current?.({ markdown, text }); + }, + }, + [placeholder], + ); + + // Toggle editable without destroying the editor instance. + React.useEffect(() => { + if (editor && editor.isEditable !== editable) { + editor.setEditable(editable); + } + }, [editor, editable]); + + const getMarkdown = React.useCallback((): string => { + if (!editor) return ""; + return getMarkdownFromEditor(editor); + }, [editor]); + + const isEmpty = React.useCallback((): boolean => { + if (!editor) return true; + return editor.isEmpty; + }, [editor]); + + const clearContent = React.useCallback(() => { + editor?.commands.clearContent(true); + }, [editor]); + + const setContent = React.useCallback( + (markdown: string) => { + if (!editor) return; + editor.commands.setContent(markdown); + }, + [editor], + ); + + const focus = React.useCallback(() => { + editor?.commands.focus("end"); + }, [editor]); + + /** + * Returns the plain-text content and an approximate cursor offset. + * Used to bridge the existing useMentions / useChannelLinks hooks which + * were designed for a plain