|
|
|
@ -6,6 +6,7 @@ |
|
|
|
import PRDetail from '$lib/components/PRDetail.svelte'; |
|
|
|
import PRDetail from '$lib/components/PRDetail.svelte'; |
|
|
|
import UserBadge from '$lib/components/UserBadge.svelte'; |
|
|
|
import UserBadge from '$lib/components/UserBadge.svelte'; |
|
|
|
import ForwardingConfig from '$lib/components/ForwardingConfig.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 { getPublicKeyWithNIP07, isNIP07Available, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; |
|
|
|
import { NostrClient } from '$lib/services/nostr/nostr-client.js'; |
|
|
|
import { NostrClient } from '$lib/services/nostr/nostr-client.js'; |
|
|
|
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js'; |
|
|
|
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js'; |
|
|
|
@ -144,7 +145,7 @@ |
|
|
|
let loadingVerification = $state(false); |
|
|
|
let loadingVerification = $state(false); |
|
|
|
|
|
|
|
|
|
|
|
// Issues |
|
|
|
// Issues |
|
|
|
let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number }>>([]); |
|
|
|
let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number }>>([]); |
|
|
|
let loadingIssues = $state(false); |
|
|
|
let loadingIssues = $state(false); |
|
|
|
let showCreateIssueDialog = $state(false); |
|
|
|
let showCreateIssueDialog = $state(false); |
|
|
|
let newIssueSubject = $state(''); |
|
|
|
let newIssueSubject = $state(''); |
|
|
|
@ -152,7 +153,7 @@ |
|
|
|
let newIssueLabels = $state<string[]>(['']); |
|
|
|
let newIssueLabels = $state<string[]>(['']); |
|
|
|
|
|
|
|
|
|
|
|
// Pull Requests |
|
|
|
// Pull Requests |
|
|
|
let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string }>>([]); |
|
|
|
let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]); |
|
|
|
let loadingPRs = $state(false); |
|
|
|
let loadingPRs = $state(false); |
|
|
|
let showCreatePRDialog = $state(false); |
|
|
|
let showCreatePRDialog = $state(false); |
|
|
|
let newPRSubject = $state(''); |
|
|
|
let newPRSubject = $state(''); |
|
|
|
@ -176,8 +177,8 @@ |
|
|
|
// Thread replies |
|
|
|
// Thread replies |
|
|
|
let expandedThreads = $state<Set<string>>(new Set()); |
|
|
|
let expandedThreads = $state<Set<string>>(new Set()); |
|
|
|
let showReplyDialog = $state(false); |
|
|
|
let showReplyDialog = $state(false); |
|
|
|
let replyingToThreadId = $state<string | null>(null); |
|
|
|
let replyingToThread = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null); |
|
|
|
let replyingToCommentId = $state<string | null>(null); // For replying to comments |
|
|
|
let replyingToComment = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null); |
|
|
|
let replyContent = $state(''); |
|
|
|
let replyContent = $state(''); |
|
|
|
let creatingReply = $state(false); |
|
|
|
let creatingReply = $state(false); |
|
|
|
|
|
|
|
|
|
|
|
@ -188,22 +189,30 @@ |
|
|
|
title: string; |
|
|
|
title: string; |
|
|
|
content: string; |
|
|
|
content: string; |
|
|
|
author: string; |
|
|
|
author: string; |
|
|
|
createdAt: number; |
|
|
|
createdAt: number; |
|
|
|
|
|
|
|
kind?: number; |
|
|
|
|
|
|
|
pubkey?: string; |
|
|
|
comments?: Array<{ |
|
|
|
comments?: Array<{ |
|
|
|
id: string; |
|
|
|
id: string; |
|
|
|
content: string; |
|
|
|
content: string; |
|
|
|
author: string; |
|
|
|
author: string; |
|
|
|
createdAt: number; |
|
|
|
createdAt: number; |
|
|
|
|
|
|
|
kind?: number; |
|
|
|
|
|
|
|
pubkey?: string; |
|
|
|
replies?: Array<{ |
|
|
|
replies?: Array<{ |
|
|
|
id: string; |
|
|
|
id: string; |
|
|
|
content: string; |
|
|
|
content: string; |
|
|
|
author: string; |
|
|
|
author: string; |
|
|
|
createdAt: number; |
|
|
|
createdAt: number; |
|
|
|
|
|
|
|
kind?: number; |
|
|
|
|
|
|
|
pubkey?: string; |
|
|
|
replies?: Array<{ |
|
|
|
replies?: Array<{ |
|
|
|
id: string; |
|
|
|
id: string; |
|
|
|
content: string; |
|
|
|
content: string; |
|
|
|
author: string; |
|
|
|
author: string; |
|
|
|
createdAt: number; |
|
|
|
createdAt: number; |
|
|
|
|
|
|
|
kind?: number; |
|
|
|
|
|
|
|
pubkey?: string; |
|
|
|
}>; |
|
|
|
}>; |
|
|
|
}>; |
|
|
|
}>; |
|
|
|
}> |
|
|
|
}> |
|
|
|
@ -725,6 +734,8 @@ |
|
|
|
content: entry.content, |
|
|
|
content: entry.content, |
|
|
|
author: entry.author, |
|
|
|
author: entry.author, |
|
|
|
createdAt: entry.createdAt, |
|
|
|
createdAt: entry.createdAt, |
|
|
|
|
|
|
|
kind: entry.kind, |
|
|
|
|
|
|
|
pubkey: entry.pubkey, |
|
|
|
comments: entry.comments |
|
|
|
comments: entry.comments |
|
|
|
})); |
|
|
|
})); |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
@ -735,6 +746,7 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function createDiscussionThread() { |
|
|
|
async function createDiscussionThread() { |
|
|
|
if (!userPubkey || !userPubkeyHex) { |
|
|
|
if (!userPubkey || !userPubkeyHex) { |
|
|
|
error = 'You must be logged in to create a discussion thread'; |
|
|
|
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) { |
|
|
|
if (!userPubkey || !userPubkeyHex) { |
|
|
|
error = 'You must be logged in to reply'; |
|
|
|
error = 'You must be logged in to reply'; |
|
|
|
return; |
|
|
|
return; |
|
|
|
@ -842,7 +854,7 @@ |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!threadId && !commentId) { |
|
|
|
if (!replyingToThread && !replyingToComment) { |
|
|
|
error = 'Must reply to either a thread or a comment'; |
|
|
|
error = 'Must reply to either a thread or a comment'; |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -899,76 +911,34 @@ |
|
|
|
let parentKind: number; |
|
|
|
let parentKind: number; |
|
|
|
let parentPubkey: string; |
|
|
|
let parentPubkey: string; |
|
|
|
|
|
|
|
|
|
|
|
if (commentId) { |
|
|
|
if (replyingToComment) { |
|
|
|
// Replying to a comment - get the comment event |
|
|
|
// Replying to a comment - use the comment object we already have |
|
|
|
const commentEvents = await client.fetchEvents([ |
|
|
|
const comment = replyingToComment; |
|
|
|
{ |
|
|
|
|
|
|
|
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 (ETag && KTag) { |
|
|
|
// Determine root: if we have a thread, use it as root; otherwise use announcement |
|
|
|
// Comment has root tags, use them |
|
|
|
if (replyingToThread) { |
|
|
|
rootEventId = ETag[1]; |
|
|
|
rootEventId = replyingToThread.id; |
|
|
|
rootKind = parseInt(KTag[1]); |
|
|
|
rootKind = replyingToThread.kind || KIND.THREAD; |
|
|
|
rootPubkey = PTag?.[1] || commentEvent.pubkey; |
|
|
|
rootPubkey = replyingToThread.pubkey || replyingToThread.author; |
|
|
|
} 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; |
|
|
|
|
|
|
|
} else { |
|
|
|
} 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 |
|
|
|
// Parent is the comment we're replying to |
|
|
|
parentEventId = commentId; |
|
|
|
parentEventId = comment.id; |
|
|
|
parentKind = KIND.COMMENT; |
|
|
|
parentKind = comment.kind || KIND.COMMENT; |
|
|
|
parentPubkey = commentEvent.pubkey; |
|
|
|
parentPubkey = comment.pubkey || comment.author; |
|
|
|
} else if (threadId) { |
|
|
|
} else if (replyingToThread) { |
|
|
|
// Replying directly to a thread |
|
|
|
// Replying directly to a thread - use the thread object we already have |
|
|
|
const threadEvents = await client.fetchEvents([ |
|
|
|
rootEventId = replyingToThread.id; |
|
|
|
{ |
|
|
|
rootKind = replyingToThread.kind || KIND.THREAD; |
|
|
|
kinds: [KIND.THREAD], |
|
|
|
rootPubkey = replyingToThread.pubkey || replyingToThread.author; |
|
|
|
ids: [threadId], |
|
|
|
parentEventId = replyingToThread.id; |
|
|
|
limit: 1 |
|
|
|
parentKind = replyingToThread.kind || KIND.THREAD; |
|
|
|
} |
|
|
|
parentPubkey = replyingToThread.pubkey || replyingToThread.author; |
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
throw new Error('Must specify thread or comment to reply to'); |
|
|
|
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'); |
|
|
|
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 |
|
|
|
// Clear form and close dialog |
|
|
|
replyContent = ''; |
|
|
|
replyContent = ''; |
|
|
|
showReplyDialog = false; |
|
|
|
showReplyDialog = false; |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = null; |
|
|
|
replyingToCommentId = null; |
|
|
|
replyingToComment = null; |
|
|
|
|
|
|
|
|
|
|
|
// Reload discussions to show the new reply |
|
|
|
// Reload discussions to show the new reply |
|
|
|
await loadDiscussions(); |
|
|
|
await loadDiscussions(); |
|
|
|
|
|
|
|
|
|
|
|
// Expand the thread if we were replying to a thread |
|
|
|
// Expand the thread if we were replying to a thread |
|
|
|
if (threadId) { |
|
|
|
if (threadIdToExpand) { |
|
|
|
expandedThreads.add(threadId); |
|
|
|
expandedThreads.add(threadIdToExpand); |
|
|
|
expandedThreads = new Set(expandedThreads); // Trigger reactivity |
|
|
|
expandedThreads = new Set(expandedThreads); // Trigger reactivity |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
@ -1994,13 +1967,14 @@ |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (response.ok) { |
|
|
|
if (response.ok) { |
|
|
|
const data = await response.json(); |
|
|
|
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, |
|
|
|
id: issue.id, |
|
|
|
subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', |
|
|
|
subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', |
|
|
|
content: issue.content, |
|
|
|
content: issue.content, |
|
|
|
status: issue.status || 'open', |
|
|
|
status: issue.status || 'open', |
|
|
|
author: issue.pubkey, |
|
|
|
author: issue.pubkey, |
|
|
|
created_at: issue.created_at |
|
|
|
created_at: issue.created_at, |
|
|
|
|
|
|
|
kind: issue.kind || KIND.ISSUE |
|
|
|
})); |
|
|
|
})); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
@ -2070,14 +2044,15 @@ |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (response.ok) { |
|
|
|
if (response.ok) { |
|
|
|
const data = await response.json(); |
|
|
|
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, |
|
|
|
id: pr.id, |
|
|
|
subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', |
|
|
|
subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', |
|
|
|
content: pr.content, |
|
|
|
content: pr.content, |
|
|
|
status: pr.status || 'open', |
|
|
|
status: pr.status || 'open', |
|
|
|
author: pr.pubkey, |
|
|
|
author: pr.pubkey, |
|
|
|
created_at: pr.created_at, |
|
|
|
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) { |
|
|
|
} catch (err) { |
|
|
|
@ -2576,6 +2551,7 @@ |
|
|
|
<div class="issue-meta"> |
|
|
|
<div class="issue-meta"> |
|
|
|
<span>#{issue.id.slice(0, 7)}</span> |
|
|
|
<span>#{issue.id.slice(0, 7)}</span> |
|
|
|
<span>{new Date(issue.created_at * 1000).toLocaleDateString()}</span> |
|
|
|
<span>{new Date(issue.created_at * 1000).toLocaleDateString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</li> |
|
|
|
</li> |
|
|
|
{/each} |
|
|
|
{/each} |
|
|
|
@ -2616,6 +2592,7 @@ |
|
|
|
<span class="pr-commit">Commit: {pr.commitId.slice(0, 7)}</span> |
|
|
|
<span class="pr-commit">Commit: {pr.commitId.slice(0, 7)}</span> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
<span>{new Date(pr.created_at * 1000).toLocaleDateString()}</span> |
|
|
|
<span>{new Date(pr.created_at * 1000).toLocaleDateString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={pr.id} kind={pr.kind} pubkey={pr.author} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</li> |
|
|
|
</li> |
|
|
|
{/each} |
|
|
|
{/each} |
|
|
|
@ -2906,11 +2883,13 @@ |
|
|
|
<span class="discussion-type">Comments</span> |
|
|
|
<span class="discussion-type">Comments</span> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
<span>Created {new Date(discussion.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>Created {new Date(discussion.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={discussion.id} kind={discussion.kind} pubkey={discussion.pubkey} /> |
|
|
|
{#if discussion.type === 'thread' && userPubkey} |
|
|
|
{#if discussion.type === 'thread' && userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = discussion.id; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
|
|
|
|
replyingToComment = null; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -2932,12 +2911,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={comment.author} /> |
|
|
|
<UserBadge pubkey={comment.author} /> |
|
|
|
<span>{new Date(comment.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(comment.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={comment.id} kind={comment.kind} pubkey={comment.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = discussion.id; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = comment.id; |
|
|
|
replyingToComment = { id: comment.id, kind: comment.kind, pubkey: comment.pubkey, author: comment.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -2955,12 +2935,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={reply.author} /> |
|
|
|
<UserBadge pubkey={reply.author} /> |
|
|
|
<span>{new Date(reply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(reply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={reply.id} kind={reply.kind} pubkey={reply.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = discussion.id; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = reply.id; |
|
|
|
replyingToComment = { id: reply.id, kind: reply.kind, pubkey: reply.pubkey, author: reply.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -2978,12 +2959,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={nestedReply.author} /> |
|
|
|
<UserBadge pubkey={nestedReply.author} /> |
|
|
|
<span>{new Date(nestedReply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(nestedReply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={nestedReply.id} kind={nestedReply.kind} pubkey={nestedReply.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = discussion.id; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = nestedReply.id; |
|
|
|
replyingToComment = { id: nestedReply.id, kind: nestedReply.kind, pubkey: nestedReply.pubkey, author: nestedReply.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -3013,12 +2995,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={comment.author} /> |
|
|
|
<UserBadge pubkey={comment.author} /> |
|
|
|
<span>{new Date(comment.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(comment.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={comment.id} kind={comment.kind} pubkey={comment.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = comment.id; |
|
|
|
replyingToComment = { id: comment.id, kind: comment.kind, pubkey: comment.pubkey, author: comment.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -3036,12 +3019,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={reply.author} /> |
|
|
|
<UserBadge pubkey={reply.author} /> |
|
|
|
<span>{new Date(reply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(reply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={reply.id} kind={reply.kind} pubkey={reply.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = reply.id; |
|
|
|
replyingToComment = { id: reply.id, kind: reply.kind, pubkey: reply.pubkey, author: reply.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -3059,12 +3043,13 @@ |
|
|
|
<div class="comment-meta"> |
|
|
|
<div class="comment-meta"> |
|
|
|
<UserBadge pubkey={nestedReply.author} /> |
|
|
|
<UserBadge pubkey={nestedReply.author} /> |
|
|
|
<span>{new Date(nestedReply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
<span>{new Date(nestedReply.createdAt * 1000).toLocaleString()}</span> |
|
|
|
|
|
|
|
<EventCopyButton eventId={nestedReply.id} kind={nestedReply.kind} pubkey={nestedReply.pubkey} /> |
|
|
|
{#if userPubkey} |
|
|
|
{#if userPubkey} |
|
|
|
<button |
|
|
|
<button |
|
|
|
class="btn btn-small" |
|
|
|
class="btn btn-small" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author }; |
|
|
|
replyingToCommentId = nestedReply.id; |
|
|
|
replyingToComment = { id: nestedReply.id, kind: nestedReply.kind, pubkey: nestedReply.pubkey, author: nestedReply.author }; |
|
|
|
showReplyDialog = true; |
|
|
|
showReplyDialog = true; |
|
|
|
}} |
|
|
|
}} |
|
|
|
> |
|
|
|
> |
|
|
|
@ -3290,7 +3275,7 @@ |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
<!-- Reply to Thread/Comment Dialog --> |
|
|
|
<!-- Reply to Thread/Comment Dialog --> |
|
|
|
{#if showReplyDialog && userPubkey && (replyingToThreadId || replyingToCommentId)} |
|
|
|
{#if showReplyDialog && userPubkey && (replyingToThread || replyingToComment)} |
|
|
|
<div |
|
|
|
<div |
|
|
|
class="modal-overlay" |
|
|
|
class="modal-overlay" |
|
|
|
role="dialog" |
|
|
|
role="dialog" |
|
|
|
@ -3298,8 +3283,8 @@ |
|
|
|
aria-label="Reply to thread" |
|
|
|
aria-label="Reply to thread" |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
showReplyDialog = false; |
|
|
|
showReplyDialog = false; |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = null; |
|
|
|
replyingToCommentId = null; |
|
|
|
replyingToComment = null; |
|
|
|
replyContent = ''; |
|
|
|
replyContent = ''; |
|
|
|
}} |
|
|
|
}} |
|
|
|
onkeydown={(e) => e.key === 'Escape' && (showReplyDialog = false)} |
|
|
|
onkeydown={(e) => e.key === 'Escape' && (showReplyDialog = false)} |
|
|
|
@ -3313,9 +3298,9 @@ |
|
|
|
onclick={(e) => e.stopPropagation()} |
|
|
|
onclick={(e) => e.stopPropagation()} |
|
|
|
> |
|
|
|
> |
|
|
|
<h3> |
|
|
|
<h3> |
|
|
|
{#if replyingToCommentId} |
|
|
|
{#if replyingToComment} |
|
|
|
Reply to Comment |
|
|
|
Reply to Comment |
|
|
|
{:else if replyingToThreadId} |
|
|
|
{:else if replyingToThread} |
|
|
|
Reply to Thread |
|
|
|
Reply to Thread |
|
|
|
{:else} |
|
|
|
{:else} |
|
|
|
Reply |
|
|
|
Reply |
|
|
|
@ -3329,8 +3314,8 @@ |
|
|
|
<button |
|
|
|
<button |
|
|
|
onclick={() => { |
|
|
|
onclick={() => { |
|
|
|
showReplyDialog = false; |
|
|
|
showReplyDialog = false; |
|
|
|
replyingToThreadId = null; |
|
|
|
replyingToThread = null; |
|
|
|
replyingToCommentId = null; |
|
|
|
replyingToComment = null; |
|
|
|
replyContent = ''; |
|
|
|
replyContent = ''; |
|
|
|
}} |
|
|
|
}} |
|
|
|
class="cancel-button" |
|
|
|
class="cancel-button" |
|
|
|
@ -3338,7 +3323,7 @@ |
|
|
|
Cancel |
|
|
|
Cancel |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
<button |
|
|
|
onclick={() => createThreadReply(replyingToThreadId, replyingToCommentId)} |
|
|
|
onclick={() => createThreadReply()} |
|
|
|
disabled={!replyContent.trim() || creatingReply} |
|
|
|
disabled={!replyContent.trim() || creatingReply} |
|
|
|
class="save-button" |
|
|
|
class="save-button" |
|
|
|
> |
|
|
|
> |
|
|
|
@ -3553,11 +3538,6 @@ |
|
|
|
border-color: var(--accent); |
|
|
|
border-color: var(--accent); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"] .bookmark-button.bookmarked { |
|
|
|
|
|
|
|
background: var(--accent); |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.bookmark-button.bookmarked:hover:not(:disabled) { |
|
|
|
.bookmark-button.bookmarked:hover:not(:disabled) { |
|
|
|
opacity: 0.9; |
|
|
|
opacity: 0.9; |
|
|
|
transform: scale(1.1); |
|
|
|
transform: scale(1.1); |
|
|
|
@ -3873,21 +3853,12 @@ |
|
|
|
font-weight: 500; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"] .fork-badge { |
|
|
|
|
|
|
|
background: var(--accent); |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.fork-badge a { |
|
|
|
.fork-badge a { |
|
|
|
color: var(--accent-text, #ffffff); |
|
|
|
color: var(--accent-text, #ffffff); |
|
|
|
text-decoration: none; |
|
|
|
text-decoration: none; |
|
|
|
font-weight: 500; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"] .fork-badge a { |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.fork-badge a:hover { |
|
|
|
.fork-badge a:hover { |
|
|
|
text-decoration: underline; |
|
|
|
text-decoration: underline; |
|
|
|
opacity: 0.9; |
|
|
|
opacity: 0.9; |
|
|
|
@ -3944,11 +3915,6 @@ |
|
|
|
border: 1px solid transparent; |
|
|
|
border: 1px solid transparent; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Ensure high contrast in both themes */ |
|
|
|
|
|
|
|
[data-theme="dark"] .topic-tag { |
|
|
|
|
|
|
|
background: var(--accent); |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.repo-website { |
|
|
|
.repo-website { |
|
|
|
margin-top: 0.5rem; |
|
|
|
margin-top: 0.5rem; |
|
|
|
@ -4057,12 +4023,6 @@ |
|
|
|
border-color: #2d3748; |
|
|
|
border-color: #2d3748; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Dark mode adjustments for owner badge */ |
|
|
|
|
|
|
|
[data-theme="dark"] .contributor-badge.owner { |
|
|
|
|
|
|
|
background: #718096; |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
border-color: #a0aec0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.contributor-badge.maintainer { |
|
|
|
.contributor-badge.maintainer { |
|
|
|
/* High contrast colors for light mode */ |
|
|
|
/* High contrast colors for light mode */ |
|
|
|
@ -4071,13 +4031,6 @@ |
|
|
|
border-color: #1a202c; |
|
|
|
border-color: #1a202c; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Dark mode adjustments for maintainer badge */ |
|
|
|
|
|
|
|
[data-theme="dark"] .contributor-badge.maintainer { |
|
|
|
|
|
|
|
background: #48bb78; |
|
|
|
|
|
|
|
color: #1a202c; |
|
|
|
|
|
|
|
border-color: #68d391; |
|
|
|
|
|
|
|
font-weight: 700; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
header h1 { |
|
|
|
header h1 { |
|
|
|
margin: 0; |
|
|
|
margin: 0; |
|
|
|
@ -4624,11 +4577,6 @@ |
|
|
|
color: var(--accent-text, #ffffff); |
|
|
|
color: var(--accent-text, #ffffff); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"] .commit-item.selected .commit-button { |
|
|
|
|
|
|
|
background: var(--accent); |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.commit-hash { |
|
|
|
.commit-hash { |
|
|
|
font-family: 'IBM Plex Mono', monospace; |
|
|
|
font-family: 'IBM Plex Mono', monospace; |
|
|
|
font-size: 0.75rem; |
|
|
|
font-size: 0.75rem; |
|
|
|
@ -5190,12 +5138,6 @@ |
|
|
|
font-weight: 600; |
|
|
|
font-weight: 600; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[data-theme="dark"] .issue-status.open, |
|
|
|
|
|
|
|
[data-theme="dark"] .pr-status.open { |
|
|
|
|
|
|
|
background: var(--accent); |
|
|
|
|
|
|
|
color: #ffffff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.issue-status.closed, .pr-status.closed { |
|
|
|
.issue-status.closed, .pr-status.closed { |
|
|
|
background: var(--error-bg); |
|
|
|
background: var(--error-bg); |
|
|
|
color: var(--error-text); |
|
|
|
color: var(--error-text); |
|
|
|
|