diff --git a/src/lib/chat.svelte.ts b/src/lib/chat.svelte.ts index 4dab4e1..49f42e8 100644 --- a/src/lib/chat.svelte.ts +++ b/src/lib/chat.svelte.ts @@ -3,6 +3,7 @@ import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; import { RELAY_URL, GROUP_ID } from "$lib/config"; import { auth } from "$lib/auth.svelte"; import { ingestNostrUser } from "$lib/profiles.svelte"; +import { extractMentionPubkeys, buildPTagHints } from "$lib/mentions"; export type ChatMessageData = { id: string; @@ -114,10 +115,21 @@ export async function sendChatMessage( .slice(-3) .map((m) => m.id.slice(0, 8)); + const notifyPubkeys = new Set(); + for (const pk of extractMentionPubkeys(content)) notifyPubkeys.add(pk); + if (replyTo && replyTo.pubkey !== ownPubkey) + notifyPubkeys.add(replyTo.pubkey); + notifyPubkeys.delete(ownPubkey); + + const hints = await buildPTagHints(notifyPubkeys); + const tags: string[][] = [["h", GROUP_ID]]; if (replyTo) { tags.push(["q", replyTo.id, RELAY_URL, replyTo.pubkey]); - if (replyTo.pubkey !== ownPubkey) tags.push(["p", replyTo.pubkey]); + } + for (const pk of notifyPubkeys) { + const hint = hints.get(pk); + tags.push(hint ? ["p", pk, hint] : ["p", pk]); } if (previousRefs.length > 0) tags.push(["previous", ...previousRefs]); diff --git a/src/lib/components/ChatContent.svelte b/src/lib/components/ChatContent.svelte new file mode 100644 index 0000000..0d68766 --- /dev/null +++ b/src/lib/components/ChatContent.svelte @@ -0,0 +1,81 @@ + + +{#each tokens as t (t)}{#if t.type === "mention"}{@const u = + profiles[t.pubkey] ?? resolvedUsers[t.pubkey]}@{u?.shortName ?? t.fallback}{:else}{t.value}{/if}{/each} diff --git a/src/lib/components/ChatSidebar.svelte b/src/lib/components/ChatSidebar.svelte index e6bc261..4d52377 100644 --- a/src/lib/components/ChatSidebar.svelte +++ b/src/lib/components/ChatSidebar.svelte @@ -9,6 +9,8 @@ import { auth, openLogin } from "$lib/auth.svelte"; import { withJoin } from "$lib/join.svelte"; import type { NostrUser } from "@nostr/gadgets/metadata"; + import MentionAutocomplete from "$lib/components/MentionAutocomplete.svelte"; + import ChatContent from "$lib/components/ChatContent.svelte"; type Props = { expanded?: boolean; @@ -19,7 +21,7 @@ let asideEl: HTMLElement; let listEl = $state(null); - let inputEl = $state(null); + let inputEl = $state(null); let openMenuId = $state(null); let replyTarget = $state(null); let inputValue = $state(""); @@ -29,6 +31,9 @@ const messages = $derived(chatStore.messages); const profiles = $derived(chatStore.profiles); + // Distinct authors of loaded messages — power the @ autocomplete context. + const contextPubkeys = $derived([...new Set(messages.map((m) => m.pubkey))]); + function resolveAuthor(pubkey: string) { const u: NostrUser | undefined = profiles[pubkey]; return { @@ -45,7 +50,9 @@ } function truncate(s: string, n = 60) { - const t = s.replace(/\s+/g, " ").trim(); + // Collapse mentions to @… so the preview stays readable. + const stripped = s.replace(/nostr:(?:npub1|nprofile1)[a-z0-9]+/gi, "@…"); + const t = stripped.replace(/\s+/g, " ").trim(); return t.length > n ? t.slice(0, n) + "…" : t; } @@ -137,14 +144,14 @@ diff --git a/src/lib/components/MentionAutocomplete.svelte b/src/lib/components/MentionAutocomplete.svelte new file mode 100644 index 0000000..ac9f3bd --- /dev/null +++ b/src/lib/components/MentionAutocomplete.svelte @@ -0,0 +1,401 @@ + + +
+ + {#if mentionOpen} +
+ {#if mentionQuery === ""} +
0} + class:border-neutral-100={mergedResults.length > 0} + > + Type to search +
+ {:else if remoteSearching && mergedResults.length > 0} +
+ + Searching… +
+ {/if} + {#if mergedResults.length === 0 && mentionQuery !== ""} +
+ {#if remoteSearching} + + Searching… + {:else} + No matches + {/if} +
+ {:else if mergedResults.length > 0} +
    + {#each mergedResults as entry, i (entry.pubkey)} +
  • { + e.preventDefault(); + selectMention(entry); + }} + onmouseenter={() => { + userMovedCursor = true; + mentionIndex = i; + }} + > + {#if entry.picture} + + {:else} + + {/if} + + {profileLabel(entry)} + + {#if profileSubLabel(entry)} + + {profileSubLabel(entry)} + + {/if} +
  • + {/each} +
+ {/if} +
+ {/if} +
diff --git a/src/lib/components/MessageEditor.svelte b/src/lib/components/MessageEditor.svelte index fb304e5..85db2be 100644 --- a/src/lib/components/MessageEditor.svelte +++ b/src/lib/components/MessageEditor.svelte @@ -1,16 +1,9 @@ -
+
{#if previewing}
{#if value.trim()} @@ -342,113 +90,15 @@ {/if}
{:else} -
- - {#if mentionOpen} -
- {#if mentionQuery === ""} -
0} - class:border-neutral-100={mergedResults.length > 0} - > - Type to search -
- {:else if remoteSearching && mergedResults.length > 0} -
- - Searching… -
- {/if} - {#if mergedResults.length === 0 && mentionQuery !== ""} -
- {#if remoteSearching} - - Searching… - {:else} - No matches - {/if} -
- {:else if mergedResults.length > 0} -
    - {#each mergedResults as entry, i (entry.pubkey)} -
  • { - e.preventDefault(); - selectMention(entry); - }} - onmouseenter={() => { - userMovedCursor = true; - mentionIndex = i; - }} - > - {#if entry.picture} - - {:else} - - {/if} - - {profileLabel(entry)} - - {#if profileSubLabel(entry)} - - {profileSubLabel(entry)} - - {/if} -
  • - {/each} -
- {/if} -
- {/if} -
+ {/if}