From 23ae530cd497f6acddd134d5940e41e68ffc6094 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 2 Jan 2025 10:04:28 -0800 Subject: [PATCH] Small fixes/performance improvements --- package-lock.json | 289 ++++++++----- package.json | 1 + src/app/commands.ts | 2 +- src/app/components/ChannelCompose.svelte | 46 +- src/app/components/EventCreate.svelte | 33 +- src/app/components/ProfileMultiSelect.svelte | 6 +- src/app/components/ThreadCreate.svelte | 32 +- src/app/components/ThreadReply.svelte | 28 +- src/app/editor/EditMention.svelte | 16 + .../editor/ProfileSuggestion.svelte} | 8 +- src/app/editor/index.ts | 102 +++++ src/app/notifications.ts | 35 +- src/app/requests.ts | 7 +- src/app/state.ts | 3 +- src/lib/components/SearchSelect.svelte | 3 +- src/lib/editor/EditBolt11.svelte | 20 - src/lib/editor/EditEvent.svelte | 42 -- src/lib/editor/EditImage.svelte | 18 - src/lib/editor/EditMention.svelte | 23 - src/lib/editor/EditVideo.svelte | 16 - src/lib/editor/FileUpload.ts | 395 ------------------ src/lib/editor/SuggestionString.svelte | 5 - src/lib/editor/Suggestions.svelte | 99 ----- src/lib/editor/Suggestions.ts | 104 ----- src/lib/editor/index.ts | 155 ------- src/lib/editor/util.ts | 96 ----- src/routes/+layout.svelte | 7 +- src/routes/spaces/[relay]/[room]/+page.svelte | 5 +- 28 files changed, 407 insertions(+), 1189 deletions(-) create mode 100644 src/app/editor/EditMention.svelte rename src/{lib/editor/SuggestionProfile.svelte => app/editor/ProfileSuggestion.svelte} (81%) create mode 100644 src/app/editor/index.ts delete mode 100644 src/lib/editor/EditBolt11.svelte delete mode 100644 src/lib/editor/EditEvent.svelte delete mode 100644 src/lib/editor/EditImage.svelte delete mode 100644 src/lib/editor/EditMention.svelte delete mode 100644 src/lib/editor/EditVideo.svelte delete mode 100644 src/lib/editor/FileUpload.ts delete mode 100644 src/lib/editor/SuggestionString.svelte delete mode 100644 src/lib/editor/Suggestions.svelte delete mode 100644 src/lib/editor/Suggestions.ts delete mode 100644 src/lib/editor/index.ts delete mode 100644 src/lib/editor/util.ts diff --git a/package-lock.json b/package-lock.json index 2f0f5f4..5507a24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@welshman/app": "~0.0.34", "@welshman/content": "~0.0.14", "@welshman/dvm": "~0.0.12", + "@welshman/editor": "^0.0.3", "@welshman/feeds": "~0.0.27", "@welshman/lib": "~0.0.33", "@welshman/net": "~0.0.43", @@ -3782,9 +3783,10 @@ } }, "node_modules/@tiptap/core": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.7.2.tgz", - "integrity": "sha512-rGAH90LPMR5OIG7vuTDRw8WxDYxPXSxuGtu++mxPF+Bv7V2ijPOy3P1oyV1G3KGoS0pPiNugLh+tVLsElcx/9Q==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.0.tgz", + "integrity": "sha512-0S3AWx6E2QqwdQqb6z0/q6zq2u9lA9oL3BLyAaITGSC9zt8OwjloS2k1zN6wLa9hp2rO0c0vDnWsTPeFaEaMdw==", + "license": "MIT", "peer": true, "funding": { "type": "github", @@ -3812,53 +3814,57 @@ } }, "node_modules/@tiptap/extension-code": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.6.tgz", - "integrity": "sha512-JrEFKsZiLvfvOFhOnnrpA0TzCuJjDeysfbMeuKUZNV4+DhYOL28d39H1++rEtJAX0LcbBU60oC5/PrlU9SpvRQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.0.tgz", + "integrity": "sha512-2roNZxcny1bGjyZ8x6VmGTuKbwfJyTZ1hiqPc/CRTQ1u42yOhbjF4ziA5kfyUoQlzygZrWH9LR5IMYGzPQ1N3w==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-code-block": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.6.tgz", - "integrity": "sha512-1YLp/zHMHSkE2xzht8nPR6T4sQJJ3ket798czxWuQEbetFv/l0U/mpiPpYSLObj6oTAoqYZ0kWXZj5eQSpPB8Q==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.0.tgz", + "integrity": "sha512-8of3qTOLjpveHBrrk8KVliSUVd6R2i2TNrBj0f/21HcFVAy0fP++02p6vI6UPOhwM3+p3CprGdSM48DFCu1rqw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-document": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.6.tgz", - "integrity": "sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.0.tgz", + "integrity": "sha512-9YI0AT3mxyUZD7NHECHyV1uAjQ8KwxOS5ACwvrK1MU8TqY084LmodYNTXPKwpqbr51yvt3qZq1R7UIVu4/22Cg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.6.tgz", - "integrity": "sha512-O6CeKriA9uyHsg7Ui4z5ZjEWXQxrIL+1zDekffW0wenGC3G4LUsCzAiFS4LSrR9a3u7tnwqGApW10rdkmCGF4w==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.0.tgz", + "integrity": "sha512-p7tUtlz7KzBa+06+7W2LJ8AEiHG5chdnUIapojZ7SqQCrFRVw70R+orpkzkoictxNNHsun0A9FCUy4rz8L0+nQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-floating-menu": { @@ -3879,41 +3885,44 @@ } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.6.tgz", - "integrity": "sha512-O2lQ2t0X0Vsbn3yLWxFFHrXY6C2N9Y6ZF/M7LWzpcDTUZeWuhoNkFE/1yOM0h6ZX1DO2A9hNIrKpi5Ny8yx+QA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.0.tgz", + "integrity": "sha512-1TVOthPkUYwTQnQwP0BzuIHVz09epOiXJQ3GqgNZsmTehwcMzz2vGCpx1JXhZ5DoMaREHNLCdraXb1n2FdhDNA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz", - "integrity": "sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.0.tgz", + "integrity": "sha512-7pMgPNk2FnPT0LcWaWNNxOLK3LQnRSYFgrdBGMXec3sy+y3Lit3hM+EZhbZcHpTIQTbWWs+eskh1waRMIt0ZaQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-history": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.6.tgz", - "integrity": "sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.0.tgz", + "integrity": "sha512-eEUEDoOtS17AHVEPbGfZ+x2L5A87SiIsppWYTkpfIH/8EnVQmzu+3i1tcT9cWvHC31d9JTG7TDptVuuHr30TJw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-image": { @@ -3947,15 +3956,16 @@ } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz", - "integrity": "sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.0.tgz", + "integrity": "sha512-xLNC05An3SQq0bVHJtOTLa8As5r6NxDZFpK0NZqO2hTq/fAIRL/9VPeZ8E0tziXULwIvIPp+L0Taw3TvaUkRUg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-placeholder": { @@ -3973,41 +3983,43 @@ } }, "node_modules/@tiptap/extension-text": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.6.tgz", - "integrity": "sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.0.tgz", + "integrity": "sha512-LcyrP+7ZEVx3YaKzjMAeujq+4xRt4mZ3ITGph2CQ4vOKFaMI8bzSR909q18t7Qyyvek0a9VydEU1NHSaq4G5jw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/pm": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.7.2.tgz", - "integrity": "sha512-RiRPlwpuE6IHDJytE0tglbFlWELOaqeyGRGv25wBTjzV1plnqC5B3U65XY/8kKuuLjdd3NpRfR68DXBafusSBg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.0.tgz", + "integrity": "sha512-4RU6bpODkMY+ZshzdRFcuUc5jWlMW82LWXR6UOsHK/X/Mav41ZFS0Cyf+hQM6gxxTB09YFIICmGpEpULb+/CuA==", + "license": "MIT", "peer": true, "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.6.0", + "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", - "prosemirror-markdown": "^1.13.0", + "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.22.3", + "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.4.0", + "prosemirror-tables": "^1.6.1", "prosemirror-trailing-node": "^3.0.0", - "prosemirror-transform": "^1.10.0", - "prosemirror-view": "^1.33.10" + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.0" }, "funding": { "type": "github", @@ -4015,16 +4027,17 @@ } }, "node_modules/@tiptap/suggestion": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.6.4.tgz", - "integrity": "sha512-t4GOEcsVSCwTlugHjZdK5Swe6or/tBej5E3ZWYOFHxkNLDod76Q7hvAeBPYrLeDo6m3sPnxrazfdqSeVclk72g==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.11.0.tgz", + "integrity": "sha512-f+KcczhzEEy2f7/0N/RSID+Z6NjxCX6ab26NLfWZxdaEm/J+vQ2Pqh/e5Z59vMfKiC0DJXVcO0rdv2LBh23qDw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.4", - "@tiptap/pm": "^2.6.4" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@trapezedev/gradle-parse": { @@ -4289,11 +4302,6 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, - "node_modules/@types/events": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==" - }, "node_modules/@types/fs-extra": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", @@ -4394,6 +4402,15 @@ "license": "MIT", "peer": true }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0.tgz", @@ -4704,6 +4721,32 @@ "nostr-tools": "^2.7.2" } }, + "node_modules/@welshman/editor": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.3.tgz", + "integrity": "sha512-H0P4yktjoivyWATaAcaWM0LQ124Yrj9h9J27MD+kTingcF1aWegTnQ7lPlozIJEhuit6P8G7ASzENW+BKBwKpA==", + "peerDependencies": { + "@tiptap/core": "^2.9.1", + "@tiptap/extension-code": "^2.9.1", + "@tiptap/extension-code-block": "^2.9.1", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-dropcursor": "^2.9.1", + "@tiptap/extension-gapcursor": "^2.9.1", + "@tiptap/extension-hard-break": "^2.9.1", + "@tiptap/extension-history": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-placeholder": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", + "@tiptap/pm": "^2.9.1", + "@tiptap/suggestion": "^2.9.1", + "@welshman/lib": "^0.0.36", + "@welshman/util": "^0.0.53", + "nostr-editor": "github:cesardeazevedo/nostr-editor#a211491c", + "nostr-tools": "^2.8.1", + "svelte": "^4.0.0", + "svelte-tiptap": "^1.0.0" + } + }, "node_modules/@welshman/feeds": { "version": "0.0.27", "resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.27.tgz", @@ -4715,14 +4758,12 @@ } }, "node_modules/@welshman/lib": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.33.tgz", - "integrity": "sha512-otaTKItm0DDR+/IHI5puYo1hU3ssd0R9LTxS+DcIKL6H+0fxtn6OLUmhcHROQukqZ6Jf7l7sfj9MX50KqPicjQ==", + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.36.tgz", + "integrity": "sha512-X02ycu8cxlPc+0yprExd6lu5/K0xow2NV20FOPF6YxQv7gLo4wKFjE87McUZZdcE2CHfzV3qTQWnbTtkGbLSYA==", "license": "MIT", "dependencies": { - "@scure/base": "^1.1.6", - "@types/events": "^3.0.3", - "events": "^3.3.0" + "@scure/base": "^1.1.6" } }, "node_modules/@welshman/net": { @@ -4764,13 +4805,17 @@ } }, "node_modules/@welshman/util": { - "version": "0.0.52", - "resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.52.tgz", - "integrity": "sha512-w17nJ9T8mhwy010WnSjGzRn9kPerZvtG6Ay5fGHw13ZC0hnOD8fkWi85r4/sI+FbCaMLAeKM57P9XD8rIkOfpw==", + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.53.tgz", + "integrity": "sha512-Bsbxq5tNK61akb3coF7aAo7WmO34ILwLDeV+nVhI6mDwFk8MeB09bhDzou+aghkJXB68rx86jUt6LKh3rxZDZg==", "license": "MIT", "dependencies": { - "@welshman/lib": "~0.0.33", + "@types/ws": "^8.5.13", + "@welshman/lib": "~0.0.34", "nostr-tools": "^2.7.2" + }, + "engines": { + "node": ">=10.4.0" } }, "node_modules/@xml-tools/parser": { @@ -7095,14 +7140,6 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -9741,8 +9778,7 @@ }, "node_modules/nostr-editor": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.3.tgz", - "integrity": "sha512-ODfwzebBRweaYt8l0pz8EbV4OqbEKZpDAVdoU+j7ubmfjhqIyk1PcQoikEZ8UasqkBcZjEQMAPl776F8nb55fQ==", + "resolved": "git+ssh://git@github.com/cesardeazevedo/nostr-editor.git#a211491c7cfeb792ae58ba91d295fe747c151ded", "license": "MIT", "dependencies": { "light-bolt11-decoder": "^3.1.1" @@ -9773,9 +9809,10 @@ } }, "node_modules/nostr-tools": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz", - "integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", + "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", + "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", @@ -9785,7 +9822,7 @@ "@scure/bip39": "1.2.1" }, "optionalDependencies": { - "nostr-wasm": "v0.1.0" + "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -10587,14 +10624,15 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz", - "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz", + "integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==", + "license": "MIT", "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.10.2" } }, "node_modules/prosemirror-dropcursor": { @@ -10653,15 +10691,42 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz", - "integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz", + "integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==", + "license": "MIT", "peer": true, "dependencies": { + "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.20.0" } }, + "node_modules/prosemirror-markdown/node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT", + "peer": true + }, + "node_modules/prosemirror-markdown/node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/prosemirror-markdown/node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "peer": true + }, "node_modules/prosemirror-menu": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", @@ -10675,9 +10740,10 @@ } }, "node_modules/prosemirror-model": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz", - "integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz", + "integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==", + "license": "MIT", "peer": true, "dependencies": { "orderedmap": "^2.0.0" @@ -10715,16 +10781,17 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz", - "integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz", + "integrity": "sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ==", + "license": "MIT", "peer": true, "dependencies": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.1" } }, "node_modules/prosemirror-trailing-node": { @@ -10743,18 +10810,20 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz", - "integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz", + "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==", + "license": "MIT", "peer": true, "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.33.11", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.11.tgz", - "integrity": "sha512-K0z9oMf6EI2ZifS9yW8PUPjEw2o1ZoFAaNzvcuyfcjIzsU6pJMo3tk9r26MyzEsuGHXZwmKPEmrjgFd78biTGA==", + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz", + "integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==", + "license": "MIT", "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", diff --git a/package.json b/package.json index 6d40289..b52fcce 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@welshman/app": "~0.0.34", "@welshman/content": "~0.0.14", "@welshman/dvm": "~0.0.12", + "@welshman/editor": "^0.0.3", "@welshman/feeds": "~0.0.27", "@welshman/lib": "~0.0.33", "@welshman/net": "~0.0.43", diff --git a/src/app/commands.ts b/src/app/commands.ts index 5abcf61..a92b4c2 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -108,7 +108,7 @@ export const loginWithNip46 = async ({ connectSecret?: string }) => { const broker = Nip46Broker.get({relays, clientSecret, signerPubkey}) - const result = await broker.connect(signerPubkey, connectSecret, NIP46_PERMS) + const result = await broker.connect(connectSecret, NIP46_PERMS) // TODO: remove ack result if (!["ack", connectSecret].includes(result)) return false diff --git a/src/app/components/ChannelCompose.svelte b/src/app/components/ChannelCompose.svelte index 1423fb5..bea9d23 100644 --- a/src/app/components/ChannelCompose.svelte +++ b/src/app/components/ChannelCompose.svelte @@ -1,56 +1,54 @@
+ on:submit|preventDefault={$uploading ? undefined : submit}>
- +
diff --git a/src/app/components/EventCreate.svelte b/src/app/components/EventCreate.svelte index cf2b3ed..eea9b64 100644 --- a/src/app/components/EventCreate.svelte +++ b/src/app/components/EventCreate.svelte @@ -1,9 +1,8 @@ @@ -86,13 +83,13 @@ slot="input" class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
- +
+ diff --git a/src/lib/editor/SuggestionProfile.svelte b/src/app/editor/ProfileSuggestion.svelte similarity index 81% rename from src/lib/editor/SuggestionProfile.svelte rename to src/app/editor/ProfileSuggestion.svelte index 1fadee8..ac746a5 100644 --- a/src/lib/editor/SuggestionProfile.svelte +++ b/src/app/editor/ProfileSuggestion.svelte @@ -3,18 +3,16 @@ import { userFollows, deriveUserWotScore, - deriveProfile, deriveHandleForPubkey, displayHandle, deriveProfileDisplay, } from "@welshman/app" - import Avatar from "@lib/components/Avatar.svelte" import WotScore from "@lib/components/WotScore.svelte" + import ProfileCircle from "@app/components/ProfileCircle.svelte" export let value const pubkey = value - const profile = deriveProfile(pubkey) const profileDisplay = deriveProfileDisplay(pubkey) const handle = deriveHandleForPubkey(pubkey) const score = deriveUserWotScore(pubkey) @@ -24,11 +22,11 @@
- +
-
+
{$profileDisplay}
diff --git a/src/app/editor/index.ts b/src/app/editor/index.ts new file mode 100644 index 0000000..cc6749f --- /dev/null +++ b/src/app/editor/index.ts @@ -0,0 +1,102 @@ +import type {Writable} from "svelte/store" +import {derived} from "svelte/store" +import {createEditor, SvelteNodeViewRenderer} from "svelte-tiptap" +import {ctx} from "@welshman/lib" +import type {StampedEvent} from "@welshman/util" +import {signer, profileSearch} from "@welshman/app" +import {MentionSuggestion, WelshmanExtension} from "@welshman/editor" +import {getSetting, userSettingValues} from "@app/state" +import ProfileSuggestion from "./ProfileSuggestion.svelte" +import EditMention from "./EditMention.svelte" + +export const getUploadType = () => getSetting<"nip96" | "blossom">("upload_type") + +export const getUploadUrl = () => { + const {upload_type, nip96_urls, blossom_urls} = userSettingValues.get() + + return upload_type === "nip96" + ? nip96_urls[0] || "https://nostr.build" + : blossom_urls[0] || "https://cdn.satellite.earth" +} + +export const signWithAssert = async (template: StampedEvent) => { + const event = await signer.get().sign(template) + + return event! +} + +export const getEditor = ({ + aggressive = false, + autofocus = false, + charCount, + content = "", + element, + placeholder = "", + submit, + uploading, + wordCount, +}: { + aggressive?: boolean + autofocus?: boolean + charCount?: Writable + content?: string + element: HTMLElement + placeholder?: string + submit: () => void + uploading?: Writable + wordCount?: Writable +}) => + createEditor({ + element, + content, + autofocus, + extensions: [ + WelshmanExtension.configure({ + submit, + sign: signWithAssert, + defaultUploadType: getUploadType(), + defaultUploadUrl: getUploadUrl(), + extensions: { + placeholder: { + config: { + placeholder, + }, + }, + breakOrSubmit: { + config: { + aggressive, + }, + }, + fileUpload: { + config: { + onDrop() { + uploading?.set(true) + }, + onComplete() { + uploading?.set(false) + }, + }, + }, + nprofile: { + extend: { + addNodeView: () => SvelteNodeViewRenderer(EditMention), + addProseMirrorPlugins() { + return [ + MentionSuggestion({ + editor: (this as any).editor, + search: derived(profileSearch, s => s.searchValues), + getRelays: (pubkey: string) => ctx.app.router.FromPubkeys([pubkey]).getUrls(), + component: ProfileSuggestion, + }), + ] + }, + }, + }, + }, + }), + ], + onUpdate({editor}) { + wordCount?.set(editor.storage.wordCount.words) + charCount?.set(editor.storage.wordCount.chars) + }, + }) diff --git a/src/app/notifications.ts b/src/app/notifications.ts index a20b62f..1c45c09 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -1,7 +1,7 @@ import {derived} from "svelte/store" -import {synced} from "@welshman/store" +import {synced, throttled} from "@welshman/store" import {pubkey} from "@welshman/app" -import {prop, sortBy, now} from "@welshman/lib" +import {prop, identity, now} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import {MESSAGE} from "@welshman/util" import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes" @@ -9,7 +9,7 @@ import { THREAD_FILTER, COMMENT_FILTER, chats, - getEventsForUrl, + getUrlsForEvent, userRoomsByUrl, repositoryStore, } from "@app/state" @@ -25,11 +25,12 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [ // Derived notifications state export const notifications = derived( - [pubkey, checked, chats, userRoomsByUrl, repositoryStore], - ([$pubkey, $checked, $chats, $userRoomsByUrl, $repository]) => { - const hasNotification = (path: string, events: TrustedEvent[]) => { - const [latestEvent] = sortBy($e => -$e.created_at, events) - + throttled( + 1000, + derived([pubkey, checked, chats, userRoomsByUrl, repositoryStore, getUrlsForEvent], identity), + ), + ([$pubkey, $checked, $chats, $userRoomsByUrl, $repository, $getUrlsForEvent]) => { + const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => { if (!latestEvent || latestEvent.pubkey === $pubkey) { return false } @@ -50,29 +51,33 @@ export const notifications = derived( for (const {pubkeys, messages} of $chats) { const chatPath = makeChatPath(pubkeys) - if (hasNotification(chatPath, messages)) { + if (hasNotification(chatPath, messages[0])) { paths.add("/chat") paths.add(chatPath) } } + const allThreadEvents = $repository.query([THREAD_FILTER, COMMENT_FILTER]) + const allMessageEvents = $repository.query([{kinds: [MESSAGE]}]) + for (const [url, rooms] of $userRoomsByUrl.entries()) { const spacePath = makeSpacePath(url) const threadPath = makeThreadPath(url) - const threadFilters = [THREAD_FILTER, COMMENT_FILTER] - const threadEvents = getEventsForUrl($repository, url, threadFilters) + const latestEvent = allThreadEvents.find(e => $getUrlsForEvent(e.id).includes(url)) - if (hasNotification(threadPath, threadEvents)) { + if (hasNotification(threadPath, latestEvent)) { paths.add(spacePath) paths.add(threadPath) } for (const room of rooms) { const roomPath = makeRoomPath(url, room) - const roomFilters = [{kinds: [MESSAGE], "#h": [room]}] - const roomEvents = getEventsForUrl($repository, url, roomFilters) + const latestEvent = allMessageEvents.find( + e => + $getUrlsForEvent(e.id).includes(url) && e.tags.find(t => t[0] === "h" && t[1] === room), + ) - if (hasNotification(roomPath, roomEvents)) { + if (hasNotification(roomPath, latestEvent)) { paths.add(spacePath) paths.add(roomPath) } diff --git a/src/app/requests.ts b/src/app/requests.ts index b6656b5..f67951c 100644 --- a/src/app/requests.ts +++ b/src/app/requests.ts @@ -1,20 +1,23 @@ +import {get} from "svelte/store" import {partition, assoc, now, ago, MONTH} from "@welshman/lib" import {MESSAGE, DELETE, THREAD, COMMENT} from "@welshman/util" import type {Subscription} from "@welshman/net" import type {AppSyncOpts} from "@welshman/app" import {subscribe, repository, pull, hasNegentropy} from "@welshman/app" -import {userRoomsByUrl, getEventsForUrl} from "@app/state" +import {userRoomsByUrl, getUrlsForEvent} from "@app/state" // Utils export const pullConservatively = ({relays, filters}: AppSyncOpts) => { + const $getUrlsForEvent = get(getUrlsForEvent) const [smart, dumb] = partition(hasNegentropy, relays) const promises = [pull({relays: smart, filters})] + const allEvents = repository.query(filters, {shouldSort: false}) // Since pulling from relays without negentropy is expensive, limit how many // duplicates we repeatedly download for (const url of dumb) { - const events = getEventsForUrl(repository, url, filters) + const events = allEvents.filter(e => $getUrlsForEvent(e.id).includes(url)) if (events.length > 100) { filters = filters.map(assoc("since", events[10]!.created_at)) diff --git a/src/app/state.ts b/src/app/state.ts index a1394ca..6902c9a 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -607,7 +607,8 @@ export const userSettingValues = withGetter( derived(userSettings, $s => $s?.values || defaultSettings), ) -export const getSetting = (key: keyof Settings["values"]) => userSettingValues.get()[key] +export const getSetting = (key: keyof Settings["values"]) => + userSettingValues.get()[key] as T export const userMembership = withGetter( derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => { diff --git a/src/lib/components/SearchSelect.svelte b/src/lib/components/SearchSelect.svelte index f6a5153..bcaa51c 100644 --- a/src/lib/components/SearchSelect.svelte +++ b/src/lib/components/SearchSelect.svelte @@ -4,10 +4,9 @@ import {type Instance} from "tippy.js" import {identity} from "@welshman/lib" import {createSearch} from "@welshman/app" + import {Suggestions, SuggestionString} from "@welshman/editor" import Icon from "@lib/components/Icon.svelte" import Tippy from "@lib/components/Tippy.svelte" - import Suggestions from "@lib/editor/Suggestions.svelte" - import SuggestionString from "@lib/editor/SuggestionString.svelte" export let value: string export let options: string[] diff --git a/src/lib/editor/EditBolt11.svelte b/src/lib/editor/EditBolt11.svelte deleted file mode 100644 index 0a59a52..0000000 --- a/src/lib/editor/EditBolt11.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/src/lib/editor/EditEvent.svelte b/src/lib/editor/EditEvent.svelte deleted file mode 100644 index 987e430..0000000 --- a/src/lib/editor/EditEvent.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {displayEvent($event)} - - diff --git a/src/lib/editor/EditImage.svelte b/src/lib/editor/EditImage.svelte deleted file mode 100644 index 99981a6..0000000 --- a/src/lib/editor/EditImage.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - {#if node.attrs.uploading} - - {:else} - - {/if} - {node.attrs.file.name} - diff --git a/src/lib/editor/EditMention.svelte b/src/lib/editor/EditMention.svelte deleted file mode 100644 index 0dc21a6..0000000 --- a/src/lib/editor/EditMention.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - @{displayProfile($profile)} - - diff --git a/src/lib/editor/EditVideo.svelte b/src/lib/editor/EditVideo.svelte deleted file mode 100644 index cc54d52..0000000 --- a/src/lib/editor/EditVideo.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - {#if node.attrs.uploading} - - {:else} - - {/if} - {node.attrs.file.name} - diff --git a/src/lib/editor/FileUpload.ts b/src/lib/editor/FileUpload.ts deleted file mode 100644 index bdaacee..0000000 --- a/src/lib/editor/FileUpload.ts +++ /dev/null @@ -1,395 +0,0 @@ -import type {CommandProps, Editor} from "@tiptap/core" -import {Extension} from "@tiptap/core" -import {now} from "@welshman/lib" -import type {StampedEvent, SignedEvent} from "@welshman/util" -import type {ImageAttributes, VideoAttributes} from "nostr-editor" -import {readServerConfig, uploadFile} from "nostr-tools/nip96" -import {getToken} from "nostr-tools/nip98" -import type {Node} from "prosemirror-model" -import {Plugin, PluginKey} from "prosemirror-state" -import {writable} from "svelte/store" - -declare module "@tiptap/core" { - interface Commands { - uploadFile: { - selectFiles: () => ReturnType - uploadFiles: () => ReturnType - getMetaTags: () => string[][] - } - } -} - -export interface FileUploadOptions { - allowedMimeTypes: string[] - expiration: number - immediateUpload: boolean - hash: (file: File) => Promise - sign?: (event: StampedEvent) => Promise - onDrop: (currentEditor: Editor, file: File, pos: number) => void - onComplete: (currentEditor: Editor) => void -} - -interface UploadTask { - url?: string - sha256?: string - error?: string -} - -function bufferToHex(buffer: ArrayBuffer) { - return Array.from(new Uint8Array(buffer)) - .map(b => b.toString(16).padStart(2, "0")) - .join("") -} - -export const FileUploadExtension = Extension.create({ - name: "fileUpload", - - addStorage() { - return { - loading: writable(false), - tags: [] as string[][], - } - }, - - addOptions() { - return { - allowedMimeTypes: [ - "image/jpeg", - "image/png", - "image/gif", - "video/mp4", - "video/mpeg", - "video/webm", - ], - immediateUpload: true, - expiration: 60000, - async hash(file: File) { - return bufferToHex(await crypto.subtle.digest("SHA-256", await file.arrayBuffer())) - }, - onDrop() {}, - onComplete() {}, - } - }, - - addCommands() { - return { - selectFiles: () => props => { - props.tr.setMeta("selectFiles", true) - return true - }, - uploadFiles: () => (props: CommandProps) => { - props.tr.setMeta("uploadFiles", true) - return true - }, - getMetaTags: () => - ((props: CommandProps) => { - const tags: string[][] = [] - // make sure the file uploaded is still in the editor content - props.editor.state.doc.descendants(node => { - if (!(node.type.name === "image" || node.type.name === "video")) { - return - } - const tag = props.editor.storage.fileUpload.tags.find((t: string[]) => - t[1].includes(node.attrs.src), - ) - if (tag) { - tags.push(tag) - } - }) - return tags - }) as any, - } - }, - - addProseMirrorPlugins() { - const uploader = new Uploader(this.editor, this.options) - return [ - new Plugin({ - key: new PluginKey("fileUploadPlugin"), - state: { - init() { - return {} - }, - apply(tr) { - setTimeout(() => { - if (tr.getMeta("selectFiles")) { - uploader.selectFiles() - tr.setMeta("selectFiles", null) - } else if (tr.getMeta("uploadFiles")) { - uploader.uploadFiles() - tr.setMeta("uploadFiles", null) - } - }) - return {} - }, - }, - props: { - handleDrop: (_, event) => { - return uploader.handleDrop(event) - }, - }, - }), - ] - }, -}) - -class Uploader { - constructor( - public editor: Editor, - private options: FileUploadOptions, - ) {} - - get view() { - return this.editor.view - } - - addFile(file: File, pos: number) { - if ( - !this.options.allowedMimeTypes.some(amt => amt.split("*").every(s => file.type.includes(s))) - ) { - return false - } - const {tr} = this.view.state - const [mimetype] = file.type.split("/") - const node = this.view.state.schema.nodes[mimetype].create({ - file, - src: URL.createObjectURL(file), - alt: "", - uploading: false, - uploadError: null, - }) - tr.insert(pos, node) - this.view.dispatch(tr) - - if (this.options.immediateUpload) { - this.editor.storage.fileUpload.loading.set(true) - this.upload(node).then(() => this.editor.storage.fileUpload.loading.set(false)) - } - this.options.onDrop(this.editor, file, pos) - return true - } - - findNodePosition(node: Node) { - let pos = -1 - this.view.state.doc.descendants((n, p) => { - if (n === node) { - pos = p - return false - } - }) - return pos - } - - findNodes(uploading: boolean) { - const nodes = [] as [Node, number][] - this.view.state.doc.descendants((node, pos) => { - if (!(node.type.name === "image" || node.type.name === "video")) { - return - } - if (node.attrs.sha256) { - return - } - if ((node.attrs.uploading || false) !== uploading) { - return - } - nodes.push([node, pos]) - }) - return nodes - } - - updateNodeAttributes(nodeRef: Node, attrs: Record) { - const {tr} = this.editor.view.state - - const pos = this.findNodePosition(nodeRef) - if (pos === -1) return - - Object.entries(attrs).forEach( - ([key, value]) => value !== undefined && tr.setNodeAttribute(pos, key, value), - ) - this.view.dispatch(tr) - } - - onUploadDone(nodeRef: Node, response: UploadTask) { - this.findNodes(true).forEach(([node, pos]) => { - if (node.attrs.src === nodeRef.attrs.src) { - this.updateNodeAttributes(node, { - uploading: false, - src: response.url, - sha256: response.sha256, - uploadError: response.error, - }) - } - }) - } - - async upload(node: Node) { - const {sign, hash, expiration} = this.options - - const { - file, - alt, - uploadType, - uploadUrl: serverUrl, - } = node.attrs as ImageAttributes | VideoAttributes - - this.updateNodeAttributes(node, {uploading: true, uploadError: null}) - - try { - if (uploadType === "nip96") { - const res = (await uploadNIP96({file, alt, sign: sign!, serverUrl}))! - - // add the tags as received from nip-96 to the storage - this.editor.storage.fileUpload.tags.push(["imeta", ...res.tags!]) - this.onUploadDone(node, res) - } else { - const res = await uploadBlossom({file, serverUrl, hash, sign, expiration}) - this.editor.storage.fileUpload.tags.push([ - "imeta", - `url ${res.url}`, - `size ${res.size}`, - `m ${res.type}`, - `x ${res.sha256}`, - ]) - this.onUploadDone(node, res) - } - } catch (error) { - const msg = error as string - this.onUploadDone(node, {error: msg}) - throw new Error(msg as string) - } - } - - async uploadFiles() { - const tasks = this.findNodes(false).map(([node]) => { - return this.upload(node) - }) - try { - this.editor.storage.fileUpload.loading.set(true) - await Promise.all(tasks) - this.options.onComplete(this.editor) - } finally { - this.editor.storage.fileUpload.loading.set(false) - } - } - - selectFiles() { - const input = document.createElement("input") - input.type = "file" - input.multiple = true - input.accept = this.options.allowedMimeTypes.join(",") - input.onchange = event => { - const files = (event.target as HTMLInputElement).files - if (files) { - Array.from(files).forEach(file => { - if (file) { - const pos = this.view.state.selection.from + 1 - this.addFile(file, pos) - } - }) - } - } - input.click() - } - - handleDrop(event: DragEvent) { - event.preventDefault() - - const pos = this.view.posAtCoords({left: event.clientX, top: event.clientY})?.pos - - if (pos === undefined) return false - - const file = event.dataTransfer?.files?.[0] - if (file) { - this.addFile(file, pos) - } - } -} - -export interface NIP96Options { - file: File - alt?: string - serverUrl: string - expiration?: number - sign: (event: StampedEvent) => Promise -} - -export async function uploadNIP96(options: NIP96Options) { - try { - const server = await readServerConfig(options.serverUrl) - const authorization = await getToken(server.api_url, "POST", options.sign as any, true) - const res = await uploadFile(options.file, server.api_url, authorization, { - alt: options.alt || "", - expiration: options.expiration?.toString() || "", - content_type: options.file.type, - }) - if (res.status === "error") { - throw new Error(res.message) - } - const url = res.nip94_event?.tags.find(x => x[0] === "url")?.[1] || "" - const sha256 = res.nip94_event?.tags.find(x => x[0] === "x")?.[1] || "" - return { - url, - sha256, - tags: res.nip94_event?.tags.flatMap(item => item.join(" ")), - } - } catch (error) { - console.warn(error) - } -} - -export interface BlossomOptions { - file: File - serverUrl: string - expiration?: number - hash?: (file: File) => Promise - sign?: (event: StampedEvent) => Promise -} - -export interface BlossomResponse { - sha256: string - size: number - type: string - uploaded: number - url: string -} - -export interface BlossomResponseError { - message: string -} - -export async function uploadBlossom(options: BlossomOptions) { - if (!options.hash) { - throw new Error("No hash function provided") - } - if (!options.sign) { - throw new Error("No signer provided") - } - const created_at = now() - const hash = await options.hash(options.file) - const event = await options.sign({ - kind: 24242, - content: `Upload ${options.file.name}`, - created_at, - tags: [ - ["t", "upload"], - ["x", hash], - ["size", options.file.size.toString()], - ["expiration", (created_at + (options.expiration || 60000)).toString()], - ], - }) - const data = JSON.stringify(event) - const base64 = btoa(data) - const authorization = `Nostr ${base64}` - const res = await fetch(options.serverUrl + "/upload", { - method: "PUT", - body: options.file, - headers: { - authorization, - }, - }) - const json = await res.json() - if (res.status === 200) { - return json as BlossomResponse - } - throw new Error((json as BlossomResponseError).message) -} diff --git a/src/lib/editor/SuggestionString.svelte b/src/lib/editor/SuggestionString.svelte deleted file mode 100644 index c19d261..0000000 --- a/src/lib/editor/SuggestionString.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -{value} diff --git a/src/lib/editor/Suggestions.svelte b/src/lib/editor/Suggestions.svelte deleted file mode 100644 index d841119..0000000 --- a/src/lib/editor/Suggestions.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - - -{#if term} -
- {#if term && allowCreate && !items.includes(term)} - - {/if} - {#each items as value, i (value)} - - {/each} -
- {#if loading} -
-
- -
- Loading more options... -
- {/if} -{/if} diff --git a/src/lib/editor/Suggestions.ts b/src/lib/editor/Suggestions.ts deleted file mode 100644 index 603e574..0000000 --- a/src/lib/editor/Suggestions.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type {SvelteComponent, ComponentType} from "svelte" -import type {Readable} from "svelte/store" -import tippy, {type Instance} from "tippy.js" -import type {Editor} from "@tiptap/core" -import {PluginKey} from "@tiptap/pm/state" -import Suggestion from "@tiptap/suggestion" -import type {Search} from "@welshman/app" - -export type SuggestionsOptions = { - char: string - name: string - editor: Editor - search: Readable> - select: (value: any, props: any) => void - allowCreate?: boolean - suggestionComponent: ComponentType - suggestionsComponent: ComponentType -} - -export const createSuggestions = (options: SuggestionsOptions) => - Suggestion({ - char: options.char, - editor: options.editor, - pluginKey: new PluginKey(`suggest-${options.name}`), - command: ({editor, range, props}) => { - // increase range.to by one when the next node is of type "text" - // and starts with a space character - const nodeAfter = editor.view.state.selection.$to.nodeAfter - const overrideSpace = nodeAfter?.text?.startsWith(" ") - - if (overrideSpace) { - range.to += 1 - } - - editor - .chain() - .focus() - .insertContentAt(range, [ - {type: options.name, attrs: props}, - {type: "text", text: " "}, - ]) - .run() - - window.getSelection()?.collapseToEnd() - }, - allow: ({state, range}) => { - const $from = state.doc.resolve(range.from) - const type = state.schema.nodes[options.name] - - return !!$from.parent.type.contentMatch.matchType(type) - }, - render: () => { - let popover: Instance[] - let suggestions: SvelteComponent - - const mapProps = (props: any) => ({ - term: props.query, - search: options.search, - allowCreate: options.allowCreate, - component: options.suggestionComponent, - select: (value: string) => options.select(value, props), - }) - - return { - onStart: props => { - const target = document.createElement("div") - - popover = tippy("body", { - getReferenceClientRect: props.clientRect as any, - appendTo: document.querySelector("dialog[open]") || document.body, - content: target, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }) - - suggestions = new options.suggestionsComponent({target, props: mapProps(props)}) - }, - onUpdate: props => { - suggestions.$set(mapProps(props)) - - if (props.clientRect) { - popover[0].setProps({ - getReferenceClientRect: props.clientRect as any, - }) - } - }, - onKeyDown: props => { - if (props.event.key === "Escape") { - popover[0].hide() - - return true - } - - return Boolean(suggestions.onKeyDown?.(props.event)) - }, - onExit: () => { - popover[0].destroy() - suggestions.$destroy() - }, - } - }, - }) diff --git a/src/lib/editor/index.ts b/src/lib/editor/index.ts deleted file mode 100644 index 7932996..0000000 --- a/src/lib/editor/index.ts +++ /dev/null @@ -1,155 +0,0 @@ -import {nprofileEncode} from "nostr-tools/nip19" -import {SvelteNodeViewRenderer} from "svelte-tiptap" -import Placeholder from "@tiptap/extension-placeholder" -import Code from "@tiptap/extension-code" -import CodeBlock from "@tiptap/extension-code-block" -import Document from "@tiptap/extension-document" -import Dropcursor from "@tiptap/extension-dropcursor" -import Gapcursor from "@tiptap/extension-gapcursor" -import History from "@tiptap/extension-history" -import Paragraph from "@tiptap/extension-paragraph" -import Text from "@tiptap/extension-text" -import HardBreakExtension from "@tiptap/extension-hard-break" -import { - Bolt11Extension, - NProfileExtension, - NEventExtension, - NAddrExtension, - ImageExtension, - VideoExtension, - TagExtension, -} from "nostr-editor" -import type {StampedEvent} from "@welshman/util" -import {toNostrURI} from "@welshman/util" -import {signer, profileSearch} from "@welshman/app" -import {FileUploadExtension} from "./FileUpload" -import {createSuggestions} from "./Suggestions" -import EditMention from "./EditMention.svelte" -import EditEvent from "./EditEvent.svelte" -import EditImage from "./EditImage.svelte" -import EditBolt11 from "./EditBolt11.svelte" -import EditVideo from "./EditVideo.svelte" -import Suggestions from "./Suggestions.svelte" -import SuggestionProfile from "./SuggestionProfile.svelte" -import {asInline} from "./util" -import {getSetting} from "@app/state" - -export { - createSuggestions, - EditMention, - EditEvent, - EditImage, - EditBolt11, - EditVideo, - Suggestions, - SuggestionProfile, -} -export * from "./util" - -type UploadType = "nip96" | "blossom" - -type EditorOptions = { - submit: () => void - getPubkeyHints: (pubkey: string) => string[] - submitOnEnter?: boolean - placeholder?: string - autofocus?: boolean - uploadType?: UploadType - defaultUploadUrl?: string -} - -export const getEditorOptions = ({ - submit, - getPubkeyHints, - submitOnEnter, - placeholder = "", - autofocus = false, - uploadType = getSetting("upload_type") as UploadType, - defaultUploadUrl = getSetting("upload_type") == "nip96" - ? (getSetting("nip96_urls") as string[])[0] || "https://nostr.build" - : (getSetting("blossom_urls") as string[])[0] || "https://cdn.satellite.earth", -}: EditorOptions) => ({ - autofocus, - content: "", - extensions: [ - Code, - CodeBlock, - Document, - Dropcursor, - Gapcursor, - History, - Paragraph, - Text, - TagExtension, - Placeholder.configure({placeholder}), - HardBreakExtension.extend({ - addKeyboardShortcuts() { - return { - "Shift-Enter": () => this.editor.commands.setHardBreak(), - "Mod-Enter": () => { - if (this.editor.getText().trim()) { - submit() - return true - } - - return this.editor.commands.setHardBreak() - }, - Enter: () => { - if (submitOnEnter && this.editor.getText().trim()) { - submit() - return true - } - - return this.editor.commands.setHardBreak() - }, - } - }, - }), - Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})), - NProfileExtension.extend({ - addNodeView: () => SvelteNodeViewRenderer(EditMention), - renderText: props => toNostrURI(props.node.attrs.nprofile), - addProseMirrorPlugins() { - return [ - createSuggestions({ - char: "@", - name: "nprofile", - editor: this.editor, - search: profileSearch, - select: (pubkey: string, props: any) => { - const relays = getPubkeyHints(pubkey) - const nprofile = nprofileEncode({pubkey, relays}) - - return props.command({pubkey, nprofile, relays}) - }, - suggestionComponent: SuggestionProfile, - suggestionsComponent: Suggestions, - }), - ] - }, - }), - NEventExtension.extend( - asInline({ - addNodeView: () => SvelteNodeViewRenderer(EditEvent), - renderText: (props: any) => toNostrURI(props.node.attrs.nevent), - }), - ), - NAddrExtension.extend( - asInline({ - addNodeView: () => SvelteNodeViewRenderer(EditEvent), - renderText: (props: any) => toNostrURI(props.node.attrs.nevent), - }), - ), - ImageExtension.extend( - asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}), - ).configure({defaultUploadUrl, defaultUploadType: uploadType}), - VideoExtension.extend( - asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}), - ).configure({defaultUploadUrl, defaultUploadType: uploadType}), - FileUploadExtension.configure({ - immediateUpload: true, - allowedMimeTypes: ["image/*", "video/*"], - sign: (event: StampedEvent) => signer.get()!.sign(event), - }), - ], -}) diff --git a/src/lib/editor/util.ts b/src/lib/editor/util.ts deleted file mode 100644 index 6231e7c..0000000 --- a/src/lib/editor/util.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type {JSONContent, PasteRuleMatch, InputRuleMatch} from "@tiptap/core" -import {Editor} from "@tiptap/core" -import {ctx} from "@welshman/lib" -import {Address} from "@welshman/util" -import {repository} from "@welshman/app" - -export const asInline = (extend: Record) => ({ - inline: true, - group: "inline", - ...extend, -}) - -export const createInputRuleMatch = >( - match: RegExpMatchArray, - data: T, -): InputRuleMatch => ({index: match.index!, text: match[0], match, data}) - -export const createPasteRuleMatch = >( - match: RegExpMatchArray, - data: T, -): PasteRuleMatch => ({index: match.index!, text: match[0], match, data}) - -export const findNodes = (type: string, json: JSONContent) => { - const results: JSONContent[] = [] - - for (const node of json.content || []) { - if (node.type === type) { - results.push(node) - } - - for (const result of findNodes(type, node)) { - results.push(result) - } - } - - return results -} - -export const findMarks = (type: string, json: JSONContent) => { - const results: JSONContent[] = [] - - for (const node of json.content || []) { - for (const mark of node.marks || []) { - if (mark.type === type) { - results.push(mark) - } - } - - for (const result of findMarks(type, node)) { - results.push(result) - } - } - - return results -} - -export const getEditorTags = (editor: Editor) => { - const json = editor.getJSON() - - const topicTags = findMarks("tag", json).map(({attrs}: any) => [ - "t", - attrs.tag.replace(/^#/, "").toLowerCase(), - ]) - - const naddrTags = findNodes("naddr", json).map( - ({attrs: {kind, pubkey, identifier, relays = []}}: any) => { - const address = new Address(kind, pubkey, identifier).toString() - - return ["q", address, ctx.app.router.FromRelays(relays).getUrl(), pubkey] - }, - ) - - const neventTags = findNodes("nevent", json).map(({attrs: {id, author, relays = []}}: any) => { - const event = repository.getEvent(id) - const pubkey = author || repository.getEvent(id)?.pubkey || "" - const scenario = event ? ctx.app.router.Event(event) : ctx.app.router.FromPubkeys([pubkey]) - - return ["q", id, scenario.getUrl(), pubkey] - }) - - const mentionTags = findNodes("nprofile", json).map(({attrs: {pubkey, relays = []}}: any) => [ - "p", - pubkey, - ctx.app.router.FromRelays(relays).getUrl(), - "", - ]) - - const imetaTags = findNodes("image", json).map(({attrs: {src, sha256}}: any) => [ - "imeta", - `url ${src}`, - `x ${sha256}`, - `ox ${sha256}`, - ]) - - return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags] -} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dda7827..60089ef 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -29,7 +29,7 @@ getPubkeyTagValues, getListTags, } from "@welshman/util" - import {throttled, custom} from "@welshman/store" + import {custom} from "@welshman/store" import { relays, handles, @@ -204,8 +204,8 @@ setupAnalytics() ready = initStorage("flotilla", 4, { - relays: {keyPath: "url", store: throttled(3000, relays)}, - handles: {keyPath: "nip05", store: throttled(3000, handles)}, + relays: storageAdapters.fromCollectionStore("url", relays, {throttle: 3000}), + handles: storageAdapters.fromCollectionStore("nip05", handles, {throttle: 3000}), freshness: storageAdapters.fromObjectStore(freshness, { throttle: 3000, migrate: migrateFreshness, @@ -251,6 +251,7 @@ let unsubSpaces: any userMembership.subscribe($membership => { + console.log("subscribe") unsubSpaces?.() unsubSpaces = listenForNotifications() }) diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index 2998936..073faa1 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -1,9 +1,7 @@