From 8e9577831801c1df12b1769fbb9fddd1f12ce0a5 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 18 Feb 2026 18:30:52 +0100 Subject: [PATCH] add event copy-button and kind label to all events --- src/lib/components/EventCopyButton.svelte | 98 ++++++++ src/lib/services/nostr/discussions-service.ts | 20 +- src/routes/repos/[npub]/[repo]/+page.svelte | 230 +++++++----------- static/icons/copy.svg | 4 + 4 files changed, 207 insertions(+), 145 deletions(-) create mode 100644 src/lib/components/EventCopyButton.svelte create mode 100644 static/icons/copy.svg diff --git a/src/lib/components/EventCopyButton.svelte b/src/lib/components/EventCopyButton.svelte new file mode 100644 index 0000000..e1ae02d --- /dev/null +++ b/src/lib/components/EventCopyButton.svelte @@ -0,0 +1,98 @@ + + +
+ {#if kind !== undefined} + kind {kind} + {/if} + +
+ + diff --git a/src/lib/services/nostr/discussions-service.ts b/src/lib/services/nostr/discussions-service.ts index 84c7246..c5fc158 100644 --- a/src/lib/services/nostr/discussions-service.ts +++ b/src/lib/services/nostr/discussions-service.ts @@ -31,21 +31,29 @@ export interface DiscussionEntry { content: string; author: string; createdAt: number; + kind?: number; // Event kind (11 for threads, 1111 for comments) + pubkey?: string; // Event pubkey (for naddr encoding) comments?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; // Event kind (1111 for comments) + pubkey?: string; // Event pubkey replies?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; replies?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; }>; }>; }>; @@ -313,6 +321,8 @@ export class DiscussionsService { content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; parentId?: string; replies: any[]; }>(); @@ -328,6 +338,8 @@ export class DiscussionsService { content: comment.content, author: comment.pubkey, createdAt: comment.created_at, + kind: comment.kind, + pubkey: comment.pubkey, parentId, replies: [] }); @@ -353,6 +365,8 @@ export class DiscussionsService { content: comment.content, author: comment.author, createdAt: comment.createdAt, + kind: comment.kind, + pubkey: comment.pubkey, replies: comment.replies.length > 0 ? comment.replies.map(formatComment) : undefined }; }; @@ -390,6 +404,8 @@ export class DiscussionsService { content: thread.content, author: thread.pubkey, createdAt: thread.created_at, + kind: thread.kind, + pubkey: thread.pubkey, comments: commentTree }); } @@ -416,7 +432,9 @@ export class DiscussionsService { id: c.id, content: c.content, author: c.pubkey, - createdAt: c.created_at + createdAt: c.created_at, + kind: c.kind, + pubkey: c.pubkey })) }); } diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index b55b760..709dc6e 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -6,6 +6,7 @@ import PRDetail from '$lib/components/PRDetail.svelte'; import UserBadge from '$lib/components/UserBadge.svelte'; import ForwardingConfig from '$lib/components/ForwardingConfig.svelte'; + import EventCopyButton from '$lib/components/EventCopyButton.svelte'; import { getPublicKeyWithNIP07, isNIP07Available, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; import { NostrClient } from '$lib/services/nostr/nostr-client.js'; import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js'; @@ -144,7 +145,7 @@ let loadingVerification = $state(false); // Issues - let issues = $state>([]); + let issues = $state>([]); let loadingIssues = $state(false); let showCreateIssueDialog = $state(false); let newIssueSubject = $state(''); @@ -152,7 +153,7 @@ let newIssueLabels = $state(['']); // Pull Requests - let prs = $state>([]); + let prs = $state>([]); let loadingPRs = $state(false); let showCreatePRDialog = $state(false); let newPRSubject = $state(''); @@ -176,8 +177,8 @@ // Thread replies let expandedThreads = $state>(new Set()); let showReplyDialog = $state(false); - let replyingToThreadId = $state(null); - let replyingToCommentId = $state(null); // For replying to comments + let replyingToThread = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null); + let replyingToComment = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null); let replyContent = $state(''); let creatingReply = $state(false); @@ -188,22 +189,30 @@ title: string; content: string; author: string; - createdAt: number; + createdAt: number; + kind?: number; + pubkey?: string; comments?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; replies?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; replies?: Array<{ id: string; content: string; author: string; createdAt: number; + kind?: number; + pubkey?: string; }>; }>; }> @@ -725,6 +734,8 @@ content: entry.content, author: entry.author, createdAt: entry.createdAt, + kind: entry.kind, + pubkey: entry.pubkey, comments: entry.comments })); } catch (err) { @@ -735,6 +746,7 @@ } } + async function createDiscussionThread() { if (!userPubkey || !userPubkeyHex) { error = 'You must be logged in to create a discussion thread'; @@ -831,7 +843,7 @@ } } - async function createThreadReply(threadId: string | null, commentId: string | null) { + async function createThreadReply() { if (!userPubkey || !userPubkeyHex) { error = 'You must be logged in to reply'; return; @@ -842,7 +854,7 @@ return; } - if (!threadId && !commentId) { + if (!replyingToThread && !replyingToComment) { error = 'Must reply to either a thread or a comment'; return; } @@ -899,76 +911,34 @@ let parentKind: number; let parentPubkey: string; - if (commentId) { - // Replying to a comment - get the comment event - const commentEvents = await client.fetchEvents([ - { - kinds: [KIND.COMMENT], - ids: [commentId], - limit: 1 - } - ]); - - if (commentEvents.length === 0) { - throw new Error('Comment not found'); - } - - const commentEvent = commentEvents[0]; - - // Find root event (E tag) or use thread ID if replying to thread comment - const ETag = commentEvent.tags.find(t => t[0] === 'E'); - const KTag = commentEvent.tags.find(t => t[0] === 'K'); - const PTag = commentEvent.tags.find(t => t[0] === 'P'); + if (replyingToComment) { + // Replying to a comment - use the comment object we already have + const comment = replyingToComment; - if (ETag && KTag) { - // Comment has root tags, use them - rootEventId = ETag[1]; - rootKind = parseInt(KTag[1]); - rootPubkey = PTag?.[1] || commentEvent.pubkey; - } else if (threadId) { - // Replying to a comment in a thread, use thread as root - const threadEvents = await client.fetchEvents([ - { - kinds: [KIND.THREAD], - ids: [threadId], - limit: 1 - } - ]); - if (threadEvents.length === 0) { - throw new Error('Thread not found'); - } - rootEventId = threadId; - rootKind = KIND.THREAD; - rootPubkey = threadEvents[0].pubkey; + // Determine root: if we have a thread, use it as root; otherwise use announcement + if (replyingToThread) { + rootEventId = replyingToThread.id; + rootKind = replyingToThread.kind || KIND.THREAD; + rootPubkey = replyingToThread.pubkey || replyingToThread.author; } else { - throw new Error('Cannot determine root event'); + // Comment is directly on announcement (in "Comments" pseudo-thread) + rootEventId = announcement.id; + rootKind = KIND.REPO_ANNOUNCEMENT; + rootPubkey = announcement.pubkey; } // Parent is the comment we're replying to - parentEventId = commentId; - parentKind = KIND.COMMENT; - parentPubkey = commentEvent.pubkey; - } else if (threadId) { - // Replying directly to a thread - const threadEvents = await client.fetchEvents([ - { - kinds: [KIND.THREAD], - ids: [threadId], - limit: 1 - } - ]); - - if (threadEvents.length === 0) { - throw new Error('Thread not found'); - } - - const threadEvent = threadEvents[0]; - rootEventId = threadId; - rootKind = KIND.THREAD; - rootPubkey = threadEvent.pubkey; - parentEventId = threadId; - parentKind = KIND.THREAD; - parentPubkey = threadEvent.pubkey; + parentEventId = comment.id; + parentKind = comment.kind || KIND.COMMENT; + parentPubkey = comment.pubkey || comment.author; + } else if (replyingToThread) { + // Replying directly to a thread - use the thread object we already have + rootEventId = replyingToThread.id; + rootKind = replyingToThread.kind || KIND.THREAD; + rootPubkey = replyingToThread.pubkey || replyingToThread.author; + parentEventId = replyingToThread.id; + parentKind = replyingToThread.kind || KIND.THREAD; + parentPubkey = replyingToThread.pubkey || replyingToThread.author; } else { throw new Error('Must specify thread or comment to reply to'); } @@ -1000,18 +970,21 @@ throw new Error('Failed to publish reply to all relays'); } + // Save thread ID before clearing (for expanding after reload) + const threadIdToExpand = replyingToThread?.id; + // Clear form and close dialog replyContent = ''; showReplyDialog = false; - replyingToThreadId = null; - replyingToCommentId = null; + replyingToThread = null; + replyingToComment = null; // Reload discussions to show the new reply await loadDiscussions(); // Expand the thread if we were replying to a thread - if (threadId) { - expandedThreads.add(threadId); + if (threadIdToExpand) { + expandedThreads.add(threadIdToExpand); expandedThreads = new Set(expandedThreads); // Trigger reactivity } } catch (err) { @@ -1994,13 +1967,14 @@ }); if (response.ok) { const data = await response.json(); - issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number }) => ({ + issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; kind?: number }) => ({ id: issue.id, subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', content: issue.content, status: issue.status || 'open', author: issue.pubkey, - created_at: issue.created_at + created_at: issue.created_at, + kind: issue.kind || KIND.ISSUE })); } } catch (err) { @@ -2070,14 +2044,15 @@ }); if (response.ok) { const data = await response.json(); - prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string }) => ({ + prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string; kind?: number }) => ({ id: pr.id, subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', content: pr.content, status: pr.status || 'open', author: pr.pubkey, created_at: pr.created_at, - commitId: pr.tags.find((t: string[]) => t[0] === 'c')?.[1] + commitId: pr.tags.find((t: string[]) => t[0] === 'c')?.[1], + kind: pr.kind || KIND.PULL_REQUEST })); } } catch (err) { @@ -2576,6 +2551,7 @@
#{issue.id.slice(0, 7)} {new Date(issue.created_at * 1000).toLocaleDateString()} +
{/each} @@ -2616,6 +2592,7 @@ Commit: {pr.commitId.slice(0, 7)} {/if} {new Date(pr.created_at * 1000).toLocaleDateString()} + {/each} @@ -2906,11 +2883,13 @@ Comments {/if} Created {new Date(discussion.createdAt * 1000).toLocaleString()} + {#if discussion.type === 'thread' && userPubkey}