Move editor stuff to its own module

This commit is contained in:
Jon Staab
2024-08-29 09:23:54 -07:00
parent 88318e9753
commit 719a8a3458
9 changed files with 231 additions and 172 deletions
+2 -2
View File
@@ -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 {
+14 -151
View File
@@ -1,63 +1,24 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {nprofileEncode} from "nostr-tools/nip19"
import {createEditor, type Editor, EditorContent, SvelteNodeViewRenderer} from "svelte-tiptap"
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 {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {NProfileExtension, ImageExtension} from "nostr-editor"
import {createEvent, CHAT_MESSAGE} from "@welshman/util"
import {LinkExtension, TopicExtension, createSuggestions, findNodes} from "@lib/tiptap"
import {TopicExtension, findNodes} from "@lib/tiptap"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
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,
publishThunk,
makeThunk,
searchTopics,
userRelayUrlsByNom,
} from "@app/state"
import {getPubkeyHints, makeMention, makeIMeta} from "@app/commands"
import {publishThunk, makeThunk, userRelayUrlsByNom} from "@app/state"
import {makeMention, makeIMeta} from "@app/commands"
import {getChatEditorOptions, addFile} from "@app/editor"
export let nom
const uploading = writable(false)
let editor: Readable<Editor>
let uploading = false
const asInline = (extend: Record<string, any>) => ({inline: true, group: "inline", ...extend})
const addFile = () => $editor.chain().selectFiles().run()
const uploadFiles = () => $editor.chain().uploadFiles().run()
const sendMessage = async () => {
const sendMessage = () => {
const json = $editor.getJSON()
const relays = $userRelayUrlsByNom.get(nom)
const event = createEvent(CHAT_MESSAGE, {
@@ -80,120 +41,22 @@
}
onMount(() => {
editor = createEditor({
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()
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 = true
return $signer!.sign(event)
},
onComplete: () => {
uploading = false
sendMessage()
},
}),
],
content: "",
})
editor = createEditor(getChatEditorOptions({uploading, sendMessage}))
})
</script>
<div
class="shadow-top-xl relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100 p-2">
<Button
on:click={addFile}
on:click={() => addFile($editor)}
class="center h-10 w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200">
{#if uploading}
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon="gallery-send" />
{/if}
</Button>
<div class="flex-grow">
<div class="flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
</div>
+12 -1
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import {onMount} from "svelte"
import twColors from "tailwindcss/colors"
import type {Readable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {readable, derived} from "svelte/store"
import {hash, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
@@ -20,6 +23,7 @@
import {repository} from "@app/base"
import type {PublishStatusData} from "@app/state"
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
import {getChatViewOptions} from "@app/editor"
export let event: TrustedEvent
export let showPubkey: boolean
@@ -66,6 +70,8 @@
const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) =>
$ps.find(({status}) => statuses.includes(status))
let editor: Readable<Editor>
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
$: parentProfile = deriveProfile(parentPubkey)
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
@@ -73,6 +79,11 @@
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
$: failure =
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
onMount(() => {
editor = createEditor(getChatViewOptions(event.content))
console.log($editor)
})
</script>
<div in:fly class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300">
@@ -104,7 +115,7 @@
</div>
{/if}
<p class="text-sm">
{event.content}
<EditorContent editor={$editor} />
{#if isPending}
<span class="flex-inline ml-1 gap-1">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
+175
View File
@@ -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<boolean>
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),
}),
],
})
+17 -14
View File
@@ -593,15 +593,20 @@ export const {
name: "groups",
store: groups,
getKey: (group: PublishedGroup) => group.nom,
load: (nom: string, hints: string[] = [], request: Partial<SubscribeRequest> = {}) =>
Promise.all([
load: async (nom: string, hints: string[] = [], request: Partial<SubscribeRequest> = {}) => {
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}]})
},
})
+3 -2
View File
@@ -16,7 +16,7 @@ export interface LinkAttributes {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
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) {
+1 -1
View File
@@ -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
+6
View File
@@ -1,5 +1,11 @@
import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
export const asInline = (extend: Record<string, any>) => ({
inline: true,
group: "inline",
...extend,
})
export const createPasteRuleMatch = <T extends Record<string, unknown>>(
match: RegExpMatchArray,
data: T,
@@ -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()