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}