From 719a8a3458dda80a58f3516cbad39000107a9a6e Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 29 Aug 2024 09:23:54 -0700 Subject: [PATCH] Move editor stuff to its own module --- src/app.css | 4 +- src/app/components/GroupCompose.svelte | 165 ++--------------- src/app/components/GroupNote.svelte | 13 +- src/app/editor.ts | 175 ++++++++++++++++++ src/app/state.ts | 31 ++-- src/lib/tiptap/LinkExtension.ts | 5 +- src/lib/tiptap/TopicExtension.ts | 2 +- src/lib/tiptap/util.ts | 6 + src/routes/spaces/[nom]/[[room]]/+page.svelte | 2 +- 9 files changed, 231 insertions(+), 172 deletions(-) create mode 100644 src/app/editor.ts diff --git a/src/app.css b/src/app.css index 62be0b25..d1d65e74 100644 --- a/src/app.css +++ b/src/app.css @@ -92,7 +92,7 @@ /* tiptap */ -.tiptap { +.tiptap[contenteditable="true"] { @apply max-h-[350px] overflow-y-auto rounded-box bg-base-300 p-2 px-4; } @@ -105,7 +105,7 @@ } .link-content { - @apply rounded bg-neutral px-1 text-sm text-neutral-content no-underline; + @apply inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-sm text-neutral-content no-underline; } .link-content.link-content-selected { diff --git a/src/app/components/GroupCompose.svelte b/src/app/components/GroupCompose.svelte index 34339769..768796fd 100644 --- a/src/app/components/GroupCompose.svelte +++ b/src/app/components/GroupCompose.svelte @@ -1,63 +1,24 @@
-
+
diff --git a/src/app/components/GroupNote.svelte b/src/app/components/GroupNote.svelte index 6c9c8f0b..0d182943 100644 --- a/src/app/components/GroupNote.svelte +++ b/src/app/components/GroupNote.svelte @@ -1,5 +1,8 @@
@@ -104,7 +115,7 @@
{/if}

- {event.content} + {#if isPending} diff --git a/src/app/editor.ts b/src/app/editor.ts new file mode 100644 index 00000000..b0fb57d8 --- /dev/null +++ b/src/app/editor.ts @@ -0,0 +1,175 @@ +import type {Writable} from "svelte/store" +import {nprofileEncode} from "nostr-tools/nip19" +import {SvelteNodeViewRenderer} from "svelte-tiptap" +import {Editor} from "@tiptap/core" +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, + FileUploadExtension, +} from "nostr-editor" +import type {StampedEvent} from "@welshman/util" +import {LinkExtension, TopicExtension, asInline, createSuggestions} from "@lib/tiptap" +import GroupComposeMention from "@app/components/GroupComposeMention.svelte" +import GroupComposeTopic from "@app/components/GroupComposeTopic.svelte" +import GroupComposeEvent from "@app/components/GroupComposeEvent.svelte" +import GroupComposeImage from "@app/components/GroupComposeImage.svelte" +import GroupComposeBolt11 from "@app/components/GroupComposeBolt11.svelte" +import GroupComposeVideo from "@app/components/GroupComposeVideo.svelte" +import GroupComposeLink from "@app/components/GroupComposeLink.svelte" +import GroupComposeSuggestions from "@app/components/GroupComposeSuggestions.svelte" +import GroupComposeTopicSuggestion from "@app/components/GroupComposeTopicSuggestion.svelte" +import GroupComposeProfileSuggestion from "@app/components/GroupComposeProfileSuggestion.svelte" +import {signer} from "@app/base" +import {searchProfiles, searchTopics} from "@app/state" +import {getPubkeyHints} from "@app/commands" + +export const addFile = (editor: Editor) => editor.chain().selectFiles().run() + +export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run() + +type ChatComposeEditorOptions = { + uploading: Writable + sendMessage: () => void +} + +export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditorOptions) => ({ + content: "", + autofocus: true, + extensions: [ + Code, + CodeBlock, + Document, + Dropcursor, + Gapcursor, + History, + Paragraph, + Text, + HardBreakExtension.extend({ + addKeyboardShortcuts() { + return { + "Shift-Enter": () => this.editor.commands.setHardBreak(), + "Mod-Enter": () => this.editor.commands.setHardBreak(), + Enter: () => { + if (this.editor.getText().trim()) { + uploadFiles(this.editor) + + return true + } + + return false + }, + } + }, + }), + LinkExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink), + }), + Bolt11Extension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)}), + ), + NProfileExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention), + addProseMirrorPlugins() { + return [ + createSuggestions({ + char: "@", + name: "nprofile", + editor: this.editor, + search: searchProfiles, + select: (pubkey: string, props: any) => { + const relays = getPubkeyHints(pubkey) + const nprofile = nprofileEncode({pubkey, relays}) + + return props.command({pubkey, nprofile, relays}) + }, + suggestionComponent: GroupComposeProfileSuggestion, + suggestionsComponent: GroupComposeSuggestions, + }), + ] + }, + }), + NEventExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}), + ), + NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)})), + ImageExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)}), + ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), + VideoExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)}), + ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), + TopicExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic), + addProseMirrorPlugins() { + return [ + createSuggestions({ + char: "#", + name: "topic", + editor: this.editor, + search: searchTopics, + select: (name: string, props: any) => props.command({name}), + allowCreate: true, + suggestionComponent: GroupComposeTopicSuggestion, + suggestionsComponent: GroupComposeSuggestions, + }), + ] + }, + }), + FileUploadExtension.configure({ + immediateUpload: false, + sign: (event: StampedEvent) => { + uploading.set(true) + + return signer.get()!.sign(event) + }, + onComplete: () => { + uploading.set(false) + sendMessage() + }, + }), + ], +}) + +export const getChatViewOptions = (content: string) => ({ + content, + editable: false, + shouldRerenderOnTransaction: false, + extensions: [ + Code, + CodeBlock, + Document, + Paragraph, + Text, + LinkExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink), + }), + Bolt11Extension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)}), + ), + NProfileExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention), + }), + NEventExtension.extend( + asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}), + ), + NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)})), + ImageExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)})), + VideoExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)})), + TopicExtension.extend({ + addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic), + }), + ], +}) diff --git a/src/app/state.ts b/src/app/state.ts index 12d4878a..451393df 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -593,15 +593,20 @@ export const { name: "groups", store: groups, getKey: (group: PublishedGroup) => group.nom, - load: (nom: string, hints: string[] = [], request: Partial = {}) => - Promise.all([ + load: async (nom: string, hints: string[] = [], request: Partial = {}) => { + if (hints.length === 0) { + hints = relayUrlsByNom.get().get(nom) || [] + } + + await Promise.all([ ...hints.map(loadRelay), load({ ...request, relays: hints, filters: [{kinds: [GROUP_META], "#d": [nom]}], }), - ]), + ]) + }, }) export const searchGroups = derived(groups, $groups => @@ -642,15 +647,17 @@ export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups => groupBy($qg => $qg.group.nom, $qualifiedGroups), ) -export const relayUrlsByNom = derived(qualifiedGroups, $qualifiedGroups => { - const $relayUrlsByNom = new Map() +export const relayUrlsByNom = withGetter( + derived(qualifiedGroups, $qualifiedGroups => { + const $relayUrlsByNom = new Map() - for (const {relay, group} of $qualifiedGroups) { - pushToMapKey($relayUrlsByNom, group.nom, relay.url) - } + for (const {relay, group} of $qualifiedGroups) { + pushToMapKey($relayUrlsByNom, group.nom, relay.url) + } - return $relayUrlsByNom -}) + return $relayUrlsByNom + }), +) // Group membership @@ -760,10 +767,6 @@ export const { const timestamps = chat?.messages.map(m => m.event.created_at) || [] const since = Math.max(0, max(timestamps) - 3600) - if (relays.length === 0) { - console.warn(`Attempted to load chat for ${nom} with no qualified groups`) - } - return load({...request, relays, filters: [{"#h": [nom], since}]}) }, }) diff --git a/src/lib/tiptap/LinkExtension.ts b/src/lib/tiptap/LinkExtension.ts index 3fbc9fd5..81453037 100644 --- a/src/lib/tiptap/LinkExtension.ts +++ b/src/lib/tiptap/LinkExtension.ts @@ -16,7 +16,7 @@ export interface LinkAttributes { declare module "@tiptap/core" { interface Commands { - link: { + inlineLink: { insertLink: (options: {url: string}) => ReturnType } } @@ -24,7 +24,7 @@ declare module "@tiptap/core" { export const LinkExtension = Node.create({ atom: true, - name: "link", + name: "inlineLink", group: "inline", inline: true, selectable: true, @@ -74,6 +74,7 @@ export const LinkExtension = Node.create({ const matches = [] for (const match of text.matchAll(LINK_REGEX)) { + console.log(text, match) try { matches.push(createPasteRuleMatch(match, {url: match[0]})) } catch (e) { diff --git a/src/lib/tiptap/TopicExtension.ts b/src/lib/tiptap/TopicExtension.ts index fd3bad24..c1723783 100644 --- a/src/lib/tiptap/TopicExtension.ts +++ b/src/lib/tiptap/TopicExtension.ts @@ -3,7 +3,7 @@ import type {Node as ProsemirrorNode} from "@tiptap/pm/model" import type {MarkdownSerializerState} from "prosemirror-markdown" import {createPasteRuleMatch} from "@lib/tiptap/util" -export const TOPIC_REGEX = /(#[^\s]+)/g +export const TOPIC_REGEX = /(?:^|\s)(#[^\s]+)/g export interface TopicAttributes { name: string diff --git a/src/lib/tiptap/util.ts b/src/lib/tiptap/util.ts index 8f6a1f5a..d1652c8a 100644 --- a/src/lib/tiptap/util.ts +++ b/src/lib/tiptap/util.ts @@ -1,5 +1,11 @@ import type {JSONContent, PasteRuleMatch} from "@tiptap/core" +export const asInline = (extend: Record) => ({ + inline: true, + group: "inline", + ...extend, +}) + export const createPasteRuleMatch = >( match: RegExpMatchArray, data: T, diff --git a/src/routes/spaces/[nom]/[[room]]/+page.svelte b/src/routes/spaces/[nom]/[[room]]/+page.svelte index 475363fa..36f17faf 100644 --- a/src/routes/spaces/[nom]/[[room]]/+page.svelte +++ b/src/routes/spaces/[nom]/[[room]]/+page.svelte @@ -62,7 +62,7 @@ onMount(() => { const sub = subscribe({ filters: [{"#h": [nom], since: now() - 30}], - relays: $userRelayUrlsByNom.get(nom)!, + relays: $userRelayUrlsByNom.get(nom) || [], }) return () => sub.close()