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()