Add proper p tags for replies and mentions
This commit is contained in:
parent
a8aebe8862
commit
33f650276d
4 changed files with 89 additions and 1 deletions
|
|
@ -105,6 +105,17 @@
|
||||||
textareaEl?.focus();
|
textareaEl?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warm the kind:10002 cache for thread participants so relay hints are
|
||||||
|
// ready by publish time. Best-effort, dedup across focus events.
|
||||||
|
const prefetched = new Set<string>();
|
||||||
|
function onTextareaFocus() {
|
||||||
|
for (const pk of contextPubkeys) {
|
||||||
|
if (prefetched.has(pk)) continue;
|
||||||
|
prefetched.add(pk);
|
||||||
|
loadRelayList(pk).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function togglePreview() {
|
function togglePreview() {
|
||||||
previewing = !previewing;
|
previewing = !previewing;
|
||||||
}
|
}
|
||||||
|
|
@ -334,6 +345,7 @@
|
||||||
onkeydown={onTextareaKeydown}
|
onkeydown={onTextareaKeydown}
|
||||||
onclick={onTextareaClickOrSelect}
|
onclick={onTextareaClickOrSelect}
|
||||||
onkeyup={onTextareaClickOrSelect}
|
onkeyup={onTextareaClickOrSelect}
|
||||||
|
onfocus={onTextareaFocus}
|
||||||
onblur={onTextareaBlur}
|
onblur={onTextareaBlur}
|
||||||
aria-autocomplete="list"
|
aria-autocomplete="list"
|
||||||
aria-controls={mentionOpen ? "mention-listbox" : undefined}
|
aria-controls={mentionOpen ? "mention-listbox" : undefined}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { SimplePool } from "@nostr/tools";
|
||||||
import { auth } from "$lib/auth.svelte";
|
import { auth } from "$lib/auth.svelte";
|
||||||
import { withJoin } from "$lib/join.svelte";
|
import { withJoin } from "$lib/join.svelte";
|
||||||
import { GROUP_ID, RELAY_URL } from "$lib/config";
|
import { GROUP_ID, RELAY_URL } from "$lib/config";
|
||||||
|
import { extractMentionPubkeys, buildPTagHints } from "$lib/mentions";
|
||||||
|
|
||||||
let modalOpen = $state(false);
|
let modalOpen = $state(false);
|
||||||
let iconized = $state(false);
|
let iconized = $state(false);
|
||||||
|
|
@ -80,6 +81,10 @@ export async function publishDraft(): Promise<{ ok: boolean; threadId?: string }
|
||||||
publishError = null;
|
publishError = null;
|
||||||
let threadId: string | undefined;
|
let threadId: string | undefined;
|
||||||
|
|
||||||
|
const ownPubkey = auth.user?.pubkey;
|
||||||
|
const mentionPubkeys = extractMentionPubkeys(c).filter((pk) => pk !== ownPubkey);
|
||||||
|
const hints = await buildPTagHints(mentionPubkeys);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await withJoin(async () => {
|
const success = await withJoin(async () => {
|
||||||
const tags: string[][] = [
|
const tags: string[][] = [
|
||||||
|
|
@ -87,6 +92,10 @@ export async function publishDraft(): Promise<{ ok: boolean; threadId?: string }
|
||||||
["title", t],
|
["title", t],
|
||||||
];
|
];
|
||||||
for (const l of labels) tags.push(["t", l]);
|
for (const l of labels) tags.push(["t", l]);
|
||||||
|
for (const pk of mentionPubkeys) {
|
||||||
|
const hint = hints.get(pk);
|
||||||
|
tags.push(hint ? ["p", pk, hint] : ["p", pk]);
|
||||||
|
}
|
||||||
|
|
||||||
const event = await auth.signer!.signEvent({
|
const event = await auth.signer!.signEvent({
|
||||||
kind: 11,
|
kind: 11,
|
||||||
|
|
|
||||||
46
src/lib/mentions.ts
Normal file
46
src/lib/mentions.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as nip19 from "@nostr/tools/nip19";
|
||||||
|
import { loadRelayList } from "@nostr/gadgets/lists";
|
||||||
|
|
||||||
|
const RELAY_HINT_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export function extractMentionPubkeys(content: string): string[] {
|
||||||
|
const re = /nostr:(npub1[a-z0-9]+|nprofile1[a-z0-9]+)/gi;
|
||||||
|
const out = new Set<string>();
|
||||||
|
for (const m of content.matchAll(re)) {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(m[1]);
|
||||||
|
if (decoded.type === "npub") out.add(decoded.data);
|
||||||
|
else if (decoded.type === "nprofile") out.add(decoded.data.pubkey);
|
||||||
|
} catch {
|
||||||
|
// Invalid bech32, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...out];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function relayHintFor(pubkey: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const TIMEOUT = Symbol();
|
||||||
|
const result = await Promise.race([
|
||||||
|
loadRelayList(pubkey),
|
||||||
|
new Promise<typeof TIMEOUT>((resolve) =>
|
||||||
|
setTimeout(() => resolve(TIMEOUT), RELAY_HINT_TIMEOUT_MS),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (result === TIMEOUT) return undefined;
|
||||||
|
return result.items.find((r) => r.write)?.url;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildPTagHints(pubkeys: Iterable<string>): Promise<Map<string, string>> {
|
||||||
|
const hints = new Map<string, string>();
|
||||||
|
await Promise.all(
|
||||||
|
[...pubkeys].map(async (pk) => {
|
||||||
|
const url = await relayHintFor(pk);
|
||||||
|
if (url) hints.set(pk, url);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import { RELAY_URL, GROUP_ID } from "$lib/config";
|
||||||
import { threads as mockThreads } from "$lib/mock";
|
import { threads as mockThreads } from "$lib/mock";
|
||||||
import { auth } from "$lib/auth.svelte";
|
import { auth } from "$lib/auth.svelte";
|
||||||
import { ingestNostrUser } from "$lib/profiles.svelte";
|
import { ingestNostrUser } from "$lib/profiles.svelte";
|
||||||
|
import { extractMentionPubkeys, buildPTagHints } from "$lib/mentions";
|
||||||
|
|
||||||
const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id);
|
const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id);
|
||||||
|
|
||||||
|
|
@ -112,11 +113,31 @@ export async function sendReply(content: string, ownPubkey: string) {
|
||||||
.slice(-3)
|
.slice(-3)
|
||||||
.map((p) => p.id.slice(0, 8));
|
.map((p) => p.id.slice(0, 8));
|
||||||
|
|
||||||
|
// NIP-7D mandates flat-against-root, so parent === root for every reply.
|
||||||
|
// Build the notify set: thread participants + mentions in content, minus self.
|
||||||
|
const notifyPubkeys = new Set<string>();
|
||||||
|
notifyPubkeys.add(detail.op.pubkey);
|
||||||
|
for (const r of detail.replies) notifyPubkeys.add(r.pubkey);
|
||||||
|
for (const pk of extractMentionPubkeys(content)) notifyPubkeys.add(pk);
|
||||||
|
notifyPubkeys.delete(ownPubkey);
|
||||||
|
|
||||||
|
// Best-effort relay hints — cached calls return instantly, others race a timeout.
|
||||||
|
const hints = await buildPTagHints([...notifyPubkeys, detail.op.pubkey]);
|
||||||
|
|
||||||
|
const opHint = hints.get(detail.op.pubkey) ?? RELAY_URL;
|
||||||
|
|
||||||
const tags: string[][] = [
|
const tags: string[][] = [
|
||||||
["h", GROUP_ID],
|
["h", GROUP_ID],
|
||||||
|
["E", detail.id, opHint, detail.op.pubkey],
|
||||||
["K", "11"],
|
["K", "11"],
|
||||||
["E", detail.id, RELAY_URL, detail.op.pubkey],
|
["P", detail.op.pubkey, opHint],
|
||||||
|
["e", detail.id, opHint, detail.op.pubkey],
|
||||||
|
["k", "11"],
|
||||||
];
|
];
|
||||||
|
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]);
|
if (previousRefs.length > 0) tags.push(["previous", ...previousRefs]);
|
||||||
|
|
||||||
console.log("[reply] signing event…");
|
console.log("[reply] signing event…");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue