From 0b64a5e8b127bebe1b0d4fe288534783a9defded Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 26 Feb 2026 08:17:49 +0100 Subject: [PATCH] refactoring 2 Nostr-Signature: 9375bfe35e0574bc722cad243c22fdf374dcc9016f91f358ff9ddf1d0a03bb50 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 10fbbcbc7cab48dfd2340f0c9eceafe558d893789e4838cbe26493e5c339f7a1f015d1cc4af8bfa51d57e9a9da94bb1bb44841305d5ce7cf92db9938985d0459 --- nostr/commit-signatures.jsonl | 1 + src/lib/components/CommentRenderer.svelte | 218 ++++++ src/lib/components/DiscussionRenderer.svelte | 194 ++++++ src/lib/components/PRDetail.svelte | 69 +- .../components/PublicationIndexViewer.svelte | 16 +- src/lib/services/git/file-manager.ts | 36 +- .../git/file-manager/branch-operations.ts | 2 +- .../git/file-manager/write-operations.ts | 3 +- src/lib/utils/nostr-links.ts | 281 ++++++++ .../repos/[npub]/[repo]/branches/+server.ts | 2 +- src/routes/repos/[npub]/[repo]/+page.svelte | 413 +---------- .../[repo]/components/CommitDialog.svelte | 23 +- .../[repo]/components/DiscussionsTab.svelte | 647 ++++++++++++++++++ .../[npub]/[repo]/components/DocsTab.svelte | 17 +- .../[repo]/components/DocsViewer.svelte | 17 +- .../[repo]/components/FileBrowser.svelte | 26 +- .../[npub]/[repo]/components/FilesTab.svelte | 119 +++- .../[repo]/components/HistoryTab.svelte | 46 +- .../[npub]/[repo]/components/IssuesTab.svelte | 47 +- .../[npub]/[repo]/components/PRsTab.svelte | 41 +- .../[repo]/components/PatchesTab.svelte | 42 +- .../[repo]/components/StatusTabLayout.svelte | 39 +- .../[npub]/[repo]/components/TabLayout.svelte | 17 +- .../[npub]/[repo]/hooks/use-repo-data.ts | 34 +- 24 files changed, 1747 insertions(+), 603 deletions(-) create mode 100644 src/lib/components/CommentRenderer.svelte create mode 100644 src/lib/components/DiscussionRenderer.svelte create mode 100644 src/lib/utils/nostr-links.ts create mode 100644 src/routes/repos/[npub]/[repo]/components/DiscussionsTab.svelte diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 844cfea..f0a49ee 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -89,3 +89,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772009909,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix cli sync and refine commit workflow"]],"content":"Signed commit: fix cli sync and refine commit workflow","id":"ddf0b49bb68139efbdacd6308b95b4a5329a37f479b319d609d712bee83e2d45","sig":"aacc22f02a3129d18cd2bdcfc4e2dda66e9358e552eac507cd4c4808bb47cd582298aed7d28f21b677418e1a91f3f1553c08f02671df8f1f43681cf7b19a744e"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772010107,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix build"]],"content":"Signed commit: fix build","id":"968af17f95f1ba0cf6a4d1f04ce108a6e4eb4ec3a4f72ca6a9d2529dacb92811","sig":"1891b6131effda70ec76577efadd9ea7374ebcbd4d738d0b0650e7dce46c3e7253eccb4b8455690297b63b7c30f61a0c7dcc1af0147b2f5a631bbd91c517c32b"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772011169,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","prevent zombie git processes"]],"content":"Signed commit: prevent zombie git processes","id":"fd370d2613105f16b0cfdd55b33f50c5b724ecef272109036a7cce5477da29bc","sig":"1d3cb4392f722b1b356247bde64691576d41fdb697e8dfe62d5e7ecd5ad8ea35757da2d56db310a2005e4b5528013aa1205256e37fc230f024d3b5a2e26735bf"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772087425,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactoring 1"]],"content":"Signed commit: refactoring 1","id":"533e9f7acbdd4dc16dbe304245469d57d8d37f0c0cce53b60d99719e2acf4502","sig":"0fad2d7c44f086ceb06ce40ea8cea2d4d002ebe8caec7d78e83483348b1404cfb6256d8d3796ebd9ae6f7866a431ec4a1abe84e417d3e238b9b554b4a32481e4"} diff --git a/src/lib/components/CommentRenderer.svelte b/src/lib/components/CommentRenderer.svelte new file mode 100644 index 0000000..84c9d9a --- /dev/null +++ b/src/lib/components/CommentRenderer.svelte @@ -0,0 +1,218 @@ + + +
+
+ + {new Date(comment.createdAt * 1000).toLocaleString()} + + {#if userPubkey && onReply} + + {/if} +
+
+ {#if referencedEvent} +
+
+ + {formatDiscussionTime(referencedEvent.created_at)} +
+
{referencedEvent.content || '(No content)'}
+
+ {/if} +
+ {#each contentParts as part} + {#if part.type === 'text'} + {part.value} + {:else if part.type === 'event' && part.event} + + {:else if part.type === 'profile' && part.pubkey} + + {:else} + {part.value} + {/if} + {/each} +
+
+ {#if comment.replies && comment.replies.length > 0} +
+ {#each comment.replies as reply} + + {/each} +
+ {/if} +
+ + diff --git a/src/lib/components/DiscussionRenderer.svelte b/src/lib/components/DiscussionRenderer.svelte new file mode 100644 index 0000000..c9befa0 --- /dev/null +++ b/src/lib/components/DiscussionRenderer.svelte @@ -0,0 +1,194 @@ + + +
+
+

{discussion.title}

+
+ {#if discussion.type === 'thread'} + Thread + {#if hasComments} + {totalReplies} {totalReplies === 1 ? 'reply' : 'replies'} + {/if} + {:else} + Comments + {/if} + Created {new Date(discussion.createdAt * 1000).toLocaleString()} + + {#if discussion.type === 'thread' && userPubkey && onReplyToThread} + + {/if} +
+
+ {#if discussion.content} +
+

{discussion.content}

+
+ {/if} + {#if discussion.type === 'thread' && hasComments} +
+

Replies ({totalReplies})

+ {#each discussion.comments! as comment} + + {/each} +
+ {:else if discussion.type === 'comments' && hasComments} +
+

Comments ({totalReplies})

+ {#each discussion.comments! as comment} + + {/each} +
+ {/if} +
+ + diff --git a/src/lib/components/PRDetail.svelte b/src/lib/components/PRDetail.svelte index 3bbe5e4..9b11531 100644 --- a/src/lib/components/PRDetail.svelte +++ b/src/lib/components/PRDetail.svelte @@ -9,6 +9,10 @@ import { getPublicKeyWithNIP07 } from '../services/nostr/nip07-signer.js'; import { KIND } from '../types/nostr.js'; import { nip19 } from 'nostr-tools'; + import CommentRenderer from './CommentRenderer.svelte'; + import type { Comment } from './CommentRenderer.svelte'; + import { loadNostrLinks } from '../utils/nostr-links.js'; + import type { NostrEvent } from '../types/nostr.js'; interface Props { pr: { @@ -87,6 +91,10 @@ const highlightsService = new HighlightsService(DEFAULT_NOSTR_RELAYS); const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + + // Event caches for Nostr links + let nostrLinkEvents = $state>(new Map()); + let nostrLinkProfiles = $state>(new Map()); onMount(async () => { await checkAuth(); @@ -115,6 +123,21 @@ const data = await response.json(); highlights = data.highlights || []; comments = data.comments || []; + + // Load Nostr links from all comment content + for (const comment of comments) { + await loadNostrLinks(comment.content, nostrClient, nostrLinkEvents, nostrLinkProfiles); + } + for (const highlight of highlights) { + if (highlight.comment) { + await loadNostrLinks(highlight.comment, nostrClient, nostrLinkEvents, nostrLinkProfiles); + } + if (highlight.comments) { + for (const comment of highlight.comments) { + await loadNostrLinks(comment.content, nostrClient, nostrLinkEvents, nostrLinkProfiles); + } + } + } } } catch (err) { error = err instanceof Error ? err.message : 'Failed to load highlights'; @@ -122,6 +145,17 @@ loading = false; } } + + function convertToCommentFormat(comment: typeof comments[0]): Comment { + return { + id: comment.id, + content: comment.content, + author: comment.pubkey, + createdAt: comment.created_at, + kind: KIND.COMMENT, + pubkey: comment.pubkey + }; + } async function loadPRDiff() { if (!pr.commitId) return; @@ -469,16 +503,13 @@ {:else} {#each comments as comment} -
-
- {formatPubkey(comment.pubkey)} - {new Date(comment.created_at * 1000).toLocaleString()} -
-
{comment.content}
- {#if userPubkey} - - {/if} -
+ startComment(id) : undefined} + /> {/each} @@ -518,16 +549,14 @@ {#if highlight.comments && highlight.comments.length > 0}
{#each highlight.comments as comment} -
-
- {formatPubkey(comment.pubkey)} - {new Date(comment.created_at * 1000).toLocaleString()} -
-
{comment.content}
- {#if userPubkey} - - {/if} -
+ startComment(id) : undefined} + nested={true} + /> {/each}
{/if} diff --git a/src/lib/components/PublicationIndexViewer.svelte b/src/lib/components/PublicationIndexViewer.svelte index 9cee937..d7aa260 100644 --- a/src/lib/components/PublicationIndexViewer.svelte +++ b/src/lib/components/PublicationIndexViewer.svelte @@ -10,10 +10,6 @@ import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import logger from '$lib/services/logger.js'; - export let indexEvent: NostrEvent | null = null; - export let relays: string[] = DEFAULT_NOSTR_RELAYS; - export let onItemClick: ((item: PublicationItem) => void) | null = null; - interface PublicationItem { id: string; title: string; @@ -23,6 +19,18 @@ [key: string]: any; } + interface Props { + indexEvent?: NostrEvent | null; + relays?: string[]; + onItemClick?: ((item: PublicationItem) => void) | null; + } + + let { + indexEvent = null, + relays = DEFAULT_NOSTR_RELAYS, + onItemClick = null + }: Props = $props(); + let items = $state([]); let loading = $state(false); let error = $state(null); diff --git a/src/lib/services/git/file-manager.ts b/src/lib/services/git/file-manager.ts index a4667e7..339c7fa 100644 --- a/src/lib/services/git/file-manager.ts +++ b/src/lib/services/git/file-manager.ts @@ -5,19 +5,19 @@ import { join, resolve } from 'path'; import simpleGit, { type SimpleGit } from 'simple-git'; -import { RepoManager } from '../repo-manager.js'; -import logger from '../../logger.js'; -import { sanitizeError, isValidBranchName } from '../../../utils/security.js'; -import { repoCache, RepoCache } from '../repo-cache.js'; +import { RepoManager } from './repo-manager.js'; +import logger from '$lib/services/logger.js'; +import { sanitizeError, isValidBranchName } from '$lib/utils/security.js'; +import { repoCache, RepoCache } from './repo-cache.js'; // Import modular operations -import { getOrCreateWorktree, removeWorktree } from './worktree-manager.js'; -import { validateFilePath, validateRepoName, validateNpub } from './path-validator.js'; -import { listFiles, getFileContent } from './file-operations.js'; -import { getBranches, validateBranchName } from './branch-operations.js'; -import { writeFile, deleteFile } from './write-operations.js'; -import { getCommitHistory, getDiff } from './commit-operations.js'; -import { createTag, getTags } from './tag-operations.js'; +import { getOrCreateWorktree, removeWorktree } from './file-manager/worktree-manager.js'; +import { validateFilePath, validateRepoName, validateNpub } from './file-manager/path-validator.js'; +import { listFiles, getFileContent } from './file-manager/file-operations.js'; +import { getBranches, validateBranchName } from './file-manager/branch-operations.js'; +import { writeFile, deleteFile } from './file-manager/write-operations.js'; +import { getCommitHistory, getDiff } from './file-manager/commit-operations.js'; +import { createTag, getTags } from './file-manager/tag-operations.js'; // Types are defined below @@ -336,7 +336,7 @@ export class FileManager { npub, repoName, repoPath, - getDefaultBranch: (npub, repoName) => this.getDefaultBranch(npub, repoName) + getDefaultBranch: (npub: string, repoName: string) => this.getDefaultBranch(npub, repoName) }); } @@ -503,11 +503,11 @@ export class FileManager { private async isRepoPrivate(npub: string, repoName: string): Promise { try { - const { requireNpubHex } = await import('../../../utils/npub-utils.js'); + const { requireNpubHex } = await import('$lib/utils/npub-utils.js'); const repoOwnerPubkey = requireNpubHex(npub); - const { NostrClient } = await import('../../nostr/nostr-client.js'); - const { DEFAULT_NOSTR_RELAYS } = await import('../../../config.js'); - const { KIND } = await import('../../../types/nostr.js'); + const { NostrClient } = await import('$lib/services/nostr/nostr-client.js'); + const { DEFAULT_NOSTR_RELAYS } = await import('$lib/config.js'); + const { KIND } = await import('$lib/types/nostr.js'); const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); const events = await nostrClient.fetchEvents([ @@ -521,7 +521,7 @@ export class FileManager { if (events.length === 0) return false; - const { isPrivateRepo: checkIsPrivateRepo } = await import('../../../utils/repo-privacy.js'); + const { isPrivateRepo: checkIsPrivateRepo } = await import('$lib/utils/repo-privacy.js'); return checkIsPrivateRepo(events[0]); } catch (err) { logger.debug({ error: err, npub, repoName }, 'Failed to check repo privacy, defaulting to public'); @@ -569,7 +569,7 @@ export class FileManager { if (!announcementEvent) return null; - const { validateAnnouncementEvent } = await import('../../nostr/repo-verification.js'); + const { validateAnnouncementEvent } = await import('$lib/services/nostr/repo-verification.js'); const validation = validateAnnouncementEvent(announcementEvent, repoName); if (!validation.valid) { diff --git a/src/lib/services/git/file-manager/branch-operations.ts b/src/lib/services/git/file-manager/branch-operations.ts index 42c9bad..8a996c7 100644 --- a/src/lib/services/git/file-manager/branch-operations.ts +++ b/src/lib/services/git/file-manager/branch-operations.ts @@ -34,7 +34,7 @@ export async function getBranches(options: BranchListOptions): Promise } // Check cache first (cache for 2 minutes) - const cacheKey = RepoCache.branchListKey(npub, repoName); + const cacheKey = RepoCache.branchesKey(npub, repoName); const cached = repoCache.get(cacheKey); if (cached !== null) { logger.debug({ npub, repoName, cachedCount: cached.length }, 'Returning cached branch list'); diff --git a/src/lib/services/git/file-manager/write-operations.ts b/src/lib/services/git/file-manager/write-operations.ts index 01936b1..455887d 100644 --- a/src/lib/services/git/file-manager/write-operations.ts +++ b/src/lib/services/git/file-manager/write-operations.ts @@ -270,7 +270,8 @@ export async function deleteFile(options: Omit): Pr throw new Error('Path validation failed: resolved path outside work directory'); } - const { accessSync, constants, unlink } = await import('fs'); + const { accessSync, constants } = await import('fs'); + const { unlink } = await import('fs/promises'); try { accessSync(fullFilePath, constants.F_OK); await unlink(fullFilePath); diff --git a/src/lib/utils/nostr-links.ts b/src/lib/utils/nostr-links.ts new file mode 100644 index 0000000..7d35889 --- /dev/null +++ b/src/lib/utils/nostr-links.ts @@ -0,0 +1,281 @@ +import { nip19 } from 'nostr-tools'; +import type { NostrEvent } from '$lib/types/nostr.js'; +import { NostrClient } from '$lib/services/nostr/nostr-client.js'; + +export interface ParsedNostrLink { + type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'nprofile'; + value: string; + start: number; + end: number; +} + +export interface ProcessedContentPart { + type: 'text' | 'event' | 'profile' | 'placeholder'; + value: string; + event?: NostrEvent; + pubkey?: string; +} + +/** + * Parse nostr: links from content string + */ +export function parseNostrLinks(content: string): ParsedNostrLink[] { + const links: ParsedNostrLink[] = []; + const nostrLinkRegex = /nostr:(nevent1|naddr1|note1|npub1|nprofile1)[a-zA-Z0-9]+/g; + let match; + + while ((match = nostrLinkRegex.exec(content)) !== null) { + const fullMatch = match[0]; + const prefix = match[1]; + let type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'nprofile'; + + if (prefix === 'nevent1') type = 'nevent'; + else if (prefix === 'naddr1') type = 'naddr'; + else if (prefix === 'note1') type = 'note1'; + else if (prefix === 'npub1') type = 'npub'; + else if (prefix === 'nprofile1') type = 'nprofile'; + else continue; + + links.push({ + type, + value: fullMatch, + start: match.index, + end: match.index + fullMatch.length + }); + } + + return links; +} + +/** + * Load events and profiles from nostr: links in content + */ +export async function loadNostrLinks( + content: string, + nostrClient: NostrClient, + eventCache: Map, + profileCache: Map +): Promise { + const links = parseNostrLinks(content); + if (links.length === 0) return; + + const eventIds: string[] = []; + const aTags: string[] = []; + const npubs: string[] = []; + + for (const link of links) { + try { + if (link.type === 'nevent' || link.type === 'note1') { + const decoded = nip19.decode(link.value.replace('nostr:', '')); + if (decoded.type === 'nevent') { + eventIds.push(decoded.data.id); + } else if (decoded.type === 'note') { + eventIds.push(decoded.data as string); + } + } else if (link.type === 'naddr') { + const decoded = nip19.decode(link.value.replace('nostr:', '')); + if (decoded.type === 'naddr') { + const aTag = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`; + aTags.push(aTag); + } + } else if (link.type === 'npub' || link.type === 'nprofile') { + const decoded = nip19.decode(link.value.replace('nostr:', '')); + if (decoded.type === 'npub') { + npubs.push(link.value); + profileCache.set(link.value, decoded.data as string); + } else if (decoded.type === 'nprofile') { + npubs.push(link.value); + profileCache.set(link.value, decoded.data.pubkey as string); + } + } + } catch { + // Invalid nostr link, skip + } + } + + // Fetch events + if (eventIds.length > 0) { + try { + const events = await Promise.race([ + nostrClient.fetchEvents([{ ids: eventIds, limit: eventIds.length }]), + new Promise((resolve) => setTimeout(() => resolve([]), 10000)) + ]); + + for (const event of events) { + eventCache.set(event.id, event); + } + } catch { + // Ignore fetch errors + } + } + + // Fetch a-tag events + if (aTags.length > 0) { + for (const aTag of aTags) { + const parts = aTag.split(':'); + if (parts.length === 3) { + try { + const kind = parseInt(parts[0]); + const pubkey = parts[1]; + const dTag = parts[2]; + const events = await Promise.race([ + nostrClient.fetchEvents([{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }]), + new Promise((resolve) => setTimeout(() => resolve([]), 10000)) + ]); + + if (events.length > 0) { + eventCache.set(events[0].id, events[0]); + } + } catch { + // Ignore fetch errors + } + } + } + } +} + +/** + * Get event from nostr: link + */ +export function getEventFromNostrLink( + link: string, + eventCache: Map +): NostrEvent | undefined { + try { + if (link.startsWith('nostr:nevent1') || link.startsWith('nostr:note1')) { + const decoded = nip19.decode(link.replace('nostr:', '')); + if (decoded.type === 'nevent') { + return eventCache.get(decoded.data.id); + } else if (decoded.type === 'note') { + return eventCache.get(decoded.data as string); + } + } else if (link.startsWith('nostr:naddr1')) { + const decoded = nip19.decode(link.replace('nostr:', '')); + if (decoded.type === 'naddr') { + return Array.from(eventCache.values()).find(e => { + const dTag = e.tags.find(t => t[0] === 'd')?.[1]; + return e.kind === decoded.data.kind && + e.pubkey === decoded.data.pubkey && + dTag === decoded.data.identifier; + }); + } + } + } catch { + // Invalid link + } + return undefined; +} + +/** + * Get pubkey from nostr: npub/profile link + */ +export function getPubkeyFromNostrLink( + link: string, + profileCache: Map +): string | undefined { + return profileCache.get(link); +} + +/** + * Process content with nostr links into parts for rendering + */ +export function processContentWithNostrLinks( + content: string, + eventCache: Map, + profileCache: Map +): ProcessedContentPart[] { + const links = parseNostrLinks(content); + if (links.length === 0) { + return [{ type: 'text', value: content }]; + } + + const parts: ProcessedContentPart[] = []; + let lastIndex = 0; + + for (const link of links) { + // Add text before link + if (link.start > lastIndex) { + const textPart = content.slice(lastIndex, link.start); + if (textPart) { + parts.push({ type: 'text', value: textPart }); + } + } + + // Add link + const event = getEventFromNostrLink(link.value, eventCache); + const pubkey = getPubkeyFromNostrLink(link.value, profileCache); + if (event) { + parts.push({ type: 'event', value: link.value, event }); + } else if (pubkey) { + parts.push({ type: 'profile', value: link.value, pubkey }); + } else { + parts.push({ type: 'placeholder', value: link.value }); + } + + lastIndex = link.end; + } + + // Add remaining text + if (lastIndex < content.length) { + const textPart = content.slice(lastIndex); + if (textPart) { + parts.push({ type: 'text', value: textPart }); + } + } + + return parts; +} + +/** + * Get referenced event from discussion event (via 'e', 'a', or 'q' tag) + */ +export function getReferencedEventFromDiscussion( + event: NostrEvent, + eventCache: Map +): NostrEvent | undefined { + // Check e-tag + const eTag = event.tags.find(t => t[0] === 'e' && t[1])?.[1]; + if (eTag) { + const referenced = eventCache.get(eTag); + if (referenced) return referenced; + } + + // Check a-tag + const aTag = event.tags.find(t => t[0] === 'a' && t[1])?.[1]; + if (aTag) { + const parts = aTag.split(':'); + if (parts.length === 3) { + const kind = parseInt(parts[0]); + const pubkey = parts[1]; + const dTag = parts[2]; + return Array.from(eventCache.values()).find(e => + e.kind === kind && + e.pubkey === pubkey && + e.tags.find(t => t[0] === 'd' && t[1] === dTag) + ); + } + } + + // Check q-tag + const qTag = event.tags.find(t => t[0] === 'q' && t[1])?.[1]; + if (qTag) { + return eventCache.get(qTag); + } + + return undefined; +} + +/** + * Format timestamp for discussion display + */ +export function formatDiscussionTime(timestamp: number): string { + const now = Date.now() / 1000; + const diff = now - timestamp; + + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; + + return new Date(timestamp * 1000).toLocaleDateString(); +} diff --git a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts index 97251cf..335bebe 100644 --- a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts @@ -382,7 +382,7 @@ export const POST: RequestHandler = createRepoPostHandler( } // If repo has no branches, sourceBranch will be undefined/null, which createBranch will handle correctly - await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch, announcement); + await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch); return json({ success: true, message: 'Branch created successfully' }); }, { operation: 'createBranch', requireRepoExists: false } // Allow creating branches in empty repos diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 3213ab3..8dee30b 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -16,6 +16,7 @@ import PRsTab from './components/PRsTab.svelte'; import PatchesTab from './components/PatchesTab.svelte'; import DocsTab from './components/DocsTab.svelte'; + import DiscussionsTab from './components/DiscussionsTab.svelte'; import { downloadRepository as downloadRepoUtil } from './utils/download.js'; import { buildApiHeaders } from './utils/api-client.js'; import '$lib/styles/repo.css'; @@ -6125,67 +6126,14 @@ /> {/if} - + {#if activeTab === 'discussions'} - + {/if} @@ -6271,355 +6219,10 @@ - - - - -
-
-

Highlights & Comments

- {#if userPubkey} - - {/if} -
- {#if loadingPatchHighlights} -
Loading highlights...
- {:else} - - {#each patchComments as comment} -
-
- {formatPubkey(comment.pubkey)} - {new Date(comment.created_at * 1000).toLocaleString()} -
-
{comment.content}
- {#if userPubkey} - - {/if} -
- {/each} - - - {#each patchHighlights.filter(h => !h.sourceEventId || h.sourceEventId === patch.id) as highlight} -
-
- {formatPubkey(highlight.pubkey)} - {new Date(highlight.created_at * 1000).toLocaleString()} - {#if highlight.file} - {highlight.file} - {/if} - {#if highlight.lineStart && highlight.sourceEventId === patch.id} - - {/if} -
-
-
{highlight.highlightedContent || highlight.content}
-
- {#if highlight.comment} -
- Comment - {highlight.comment} -
- {/if} - - - {#if highlight.comments && highlight.comments.length > 0} -
- {#each highlight.comments as comment} -
-
- {formatPubkey(comment.pubkey)} - {new Date(comment.created_at * 1000).toLocaleString()} -
-
{comment.content}
- {#if userPubkey} - - {/if} -
- {/each} -
- {/if} - {#if userPubkey} - - {/if} -
- {/each} - - {#if patchHighlights.filter(h => !h.sourceEventId || h.sourceEventId === patch.id).length === 0 && patchComments.length === 0} -
No highlights or comments yet. Select text in the patch to create a highlight.
- {/if} - {/if} -
- - {/each} - {:else} -
-

Select a patch from the sidebar to view it

-
- {/if} - - {/if} - {#if activeTab === 'discussions'} -
-
- -
- {#if discussions.length === 0} -
-

No discussions found. {#if userPubkey}Create a new discussion thread to get started!{:else}Log in to create a discussion thread.{/if}

-
- {:else if selectedDiscussion} - {#each discussions.filter(d => d.id === selectedDiscussion) as discussion} - {@const isExpanded = discussion.type === 'thread' && expandedThreads.has(discussion.id)} - {@const hasComments = discussion.comments && discussion.comments.length > 0} -
-
-

{discussion.title}

-
- {#if discussion.type === 'thread'} - Thread - {#if hasComments} - {@const totalReplies = countAllReplies(discussion.comments)} - {totalReplies} {totalReplies === 1 ? 'reply' : 'replies'} - {/if} - {:else} - Comments - {/if} - Created {new Date(discussion.createdAt * 1000).toLocaleString()} - - {#if discussion.type === 'thread' && userPubkey} - - {/if} -
-
- {#if discussion.content} -
-

{discussion.content}

-
- {/if} - {#if discussion.type === 'thread' && hasComments} - {@const totalReplies = countAllReplies(discussion.comments)} -
-

Replies ({totalReplies})

- {#each discussion.comments! as comment} -
-
- - {new Date(comment.createdAt * 1000).toLocaleString()} - - {#if userPubkey} - - {/if} -
- {#if true} - {@const commentEvent = getDiscussionEvent(comment.id)} - {@const referencedEvent = commentEvent ? getReferencedEventFromDiscussion(commentEvent) : undefined} - {@const parts = processContentWithNostrLinks(comment.content)} -
- {#if referencedEvent} -
-
- - {formatDiscussionTime(referencedEvent.created_at)} -
-
{referencedEvent.content || '(No content)'}
-
- {/if} -
- {#each parts as part} - {#if part.type === 'text'} - {part.value} - {:else if part.type === 'event' && part.event} - - {:else if part.type === 'profile' && part.pubkey} - - {:else} - {part.value} - {/if} - {/each} -
-
- {/if} - {#if comment.replies && comment.replies.length > 0} -
- {#each comment.replies as reply} -
-
- - {new Date(reply.createdAt * 1000).toLocaleString()} - - {#if userPubkey} - - {/if} -
-
-

{reply.content}

-
- {#if reply.replies && reply.replies.length > 0} -
- {#each reply.replies as nestedReply} -
-
- - {new Date(nestedReply.createdAt * 1000).toLocaleString()} - - {#if userPubkey} - - {/if} -
-
-

{nestedReply.content}

-
-
- {/each} -
- {/if} -
- {/each} -
- {/if} -
- {/each} -
- {:else if discussion.type === 'comments' && hasComments} - {@const totalReplies = countAllReplies(discussion.comments)} -
-

Comments ({totalReplies})

- {#each discussion.comments! as comment} -
-
- - {new Date(comment.createdAt * 1000).toLocaleString()} - - {#if userPubkey} - - {/if} -
- {#if true} - {@const commentEvent = getDiscussionEvent(comment.id)} - {@const referencedEvent = commentEvent ? getReferencedEventFromDiscussion(commentEvent) : undefined} - {@const parts = processContentWithNostrLinks(comment.content)} -
- {#if referencedEvent} -
-
- - {formatDiscussionTime(referencedEvent.created_at)} -
-
{referencedEvent.content || '(No content)'}
-
- {/if} -
- {#each parts as part} - {#if part.type === 'text'} - {part.value} - {:else if part.type === 'event' && part.event} - - {:else if part.type === 'profile' && part.pubkey} - - {:else} - {part.value} - {/if} - {/each} -
-
- {/if} -
- {/each} -
- {/if} -
- {/each} - {:else} -
-

Select a discussion from the sidebar to view it

-
- {/if} -
- {/if} - {/if} diff --git a/src/routes/repos/[npub]/[repo]/components/CommitDialog.svelte b/src/routes/repos/[npub]/[repo]/components/CommitDialog.svelte index 09701a1..3fc568b 100644 --- a/src/routes/repos/[npub]/[repo]/components/CommitDialog.svelte +++ b/src/routes/repos/[npub]/[repo]/components/CommitDialog.svelte @@ -1,10 +1,21 @@ {#if show} diff --git a/src/routes/repos/[npub]/[repo]/components/DiscussionsTab.svelte b/src/routes/repos/[npub]/[repo]/components/DiscussionsTab.svelte new file mode 100644 index 0000000..73f12ba --- /dev/null +++ b/src/routes/repos/[npub]/[repo]/components/DiscussionsTab.svelte @@ -0,0 +1,647 @@ + + + + {#snippet leftPane()} +
+
+

Discussions

+ {#if userPubkey} + + {/if} +
+ {#if loadingDiscussions} +
Loading discussions...
+ {:else if discussions.length > 0} +
    + {#each discussions as discussion} + {@const hasComments = discussion.comments && discussion.comments.length > 0} + {@const totalReplies = hasComments ? countAllReplies(discussion.comments) : 0} +
  • + +
  • + {/each} +
+ {/if} +
+ {/snippet} + + {#snippet rightPanel()} + {#if error} +
{error}
+ {/if} + {#if selectedDiscussion} + {@const discussion = discussions.find(d => d.id === selectedDiscussion)} + {#if discussion} + + {/if} + {:else} +
+

Select a discussion from the sidebar to view it

+
+ {/if} + {/snippet} +
+ + +{#if showCreateThreadDialog && userPubkey} + +{/if} + + +{#if showReplyDialog && userPubkey && replyingToThread} + +{/if} + + diff --git a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte index 03cf8f5..ad19880 100644 --- a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte @@ -12,10 +12,19 @@ import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; import logger from '$lib/services/logger.js'; - export let npub: string = ''; - export let repo: string = ''; - export let currentBranch: string | null = null; - export let relays: string[] = DEFAULT_NOSTR_RELAYS; + interface Props { + npub?: string; + repo?: string; + currentBranch?: string | null; + relays?: string[]; + } + + let { + npub = '', + repo = '', + currentBranch = null, + relays = DEFAULT_NOSTR_RELAYS + }: Props = $props(); let documentationContent = $state(null); let documentationKind = $state<'markdown' | 'asciidoc' | 'text' | '30040' | null>(null); diff --git a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte index 93f1a1b..408355c 100644 --- a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte +++ b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte @@ -10,10 +10,19 @@ import { KIND } from '$lib/types/nostr.js'; import logger from '$lib/services/logger.js'; - export let content: string = ''; - export let contentType: 'markdown' | 'asciidoc' | 'text' | '30040' = 'text'; - export let indexEvent: NostrEvent | null = null; - export let relays: string[] = []; + interface Props { + content?: string; + contentType?: 'markdown' | 'asciidoc' | 'text' | '30040'; + indexEvent?: NostrEvent | null; + relays?: string[]; + } + + let { + content = '', + contentType = 'text', + indexEvent = null, + relays = [] + }: Props = $props(); let renderedContent = $state(''); let loading = $state(false); diff --git a/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte b/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte index b264e9c..a12d6b9 100644 --- a/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte +++ b/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte @@ -1,13 +1,25 @@
diff --git a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte index 09a14f5..be1cb2b 100644 --- a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte @@ -8,44 +8,87 @@ import FileBrowser from './FileBrowser.svelte'; import CodeEditor from '$lib/components/CodeEditor.svelte'; - export let files: Array<{ name: string; path: string; type: 'file' | 'directory'; size?: number }> = []; - export let currentPath: string = ''; - export let currentFile: string | null = null; - export let fileContent: string = ''; - export let fileLanguage: 'markdown' | 'asciidoc' | 'text' = 'text'; - export let editedContent: string = ''; - export let hasChanges: boolean = false; - export let loading: boolean = false; - export let error: string | null = null; - export let pathStack: string[] = []; - export let onFileClick: (file: { name: string; path: string; type: 'file' | 'directory' }) => void = () => {}; - export let onDirectoryClick: (path: string) => void = () => {}; - export let onNavigateBack: () => void = () => {}; - export let onContentChange: (content: string) => void = () => {}; - export let isMaintainer: boolean = false; - export let readmeContent: string | null = null; - export let readmePath: string | null = null; - export let readmeHtml: string | null = null; - export let showFilePreview: boolean = false; - export let fileHtml: string | null = null; - export let highlightedFileContent: string | null = null; - export let isImageFile: boolean = false; - export let imageUrl: string | null = null; - export let wordWrap: boolean = false; - export let supportsPreview: (ext: string) => boolean = () => false; - export let onSave: () => void = () => {}; - export let onTogglePreview: () => void = () => {}; - export let onCopyFileContent: (e: Event) => void = () => {}; - export let onDownloadFile: () => void = () => {}; - export let copyingFile: boolean = false; - export let saving: boolean = false; - export let needsClone: boolean = false; - export let cloneTooltip: string = ''; - export let branches: Array = []; - export let currentBranch: string | null = null; - export let defaultBranch: string | null = null; - export let onBranchChange: (branch: string) => void = () => {}; - export let userPubkey: string | null = null; + interface Props { + files?: Array<{ name: string; path: string; type: 'file' | 'directory'; size?: number }>; + currentPath?: string; + currentFile?: string | null; + fileContent?: string; + fileLanguage?: 'markdown' | 'asciidoc' | 'text'; + editedContent?: string; + hasChanges?: boolean; + loading?: boolean; + error?: string | null; + pathStack?: string[]; + onFileClick?: (file: { name: string; path: string; type: 'file' | 'directory' }) => void; + onDirectoryClick?: (path: string) => void; + onNavigateBack?: () => void; + onContentChange?: (content: string) => void; + isMaintainer?: boolean; + readmeContent?: string | null; + readmePath?: string | null; + readmeHtml?: string | null; + showFilePreview?: boolean; + fileHtml?: string | null; + highlightedFileContent?: string | null; + isImageFile?: boolean; + imageUrl?: string | null; + wordWrap?: boolean; + supportsPreview?: (ext: string) => boolean; + onSave?: () => void; + onTogglePreview?: () => void; + onCopyFileContent?: (e: Event) => void; + onDownloadFile?: () => void; + copyingFile?: boolean; + saving?: boolean; + needsClone?: boolean; + cloneTooltip?: string; + branches?: Array; + currentBranch?: string | null; + defaultBranch?: string | null; + onBranchChange?: (branch: string) => void; + userPubkey?: string | null; + } + + let { + files = [], + currentPath = '', + currentFile = null, + fileContent = '', + fileLanguage = 'text', + editedContent = '', + hasChanges = false, + loading = false, + error = null, + pathStack = [], + onFileClick = () => {}, + onDirectoryClick = () => {}, + onNavigateBack = () => {}, + onContentChange = () => {}, + isMaintainer = false, + readmeContent = null, + readmePath = null, + readmeHtml = null, + showFilePreview = false, + fileHtml = null, + highlightedFileContent = null, + isImageFile = false, + imageUrl = null, + wordWrap = false, + supportsPreview = () => false, + onSave = () => {}, + onTogglePreview = () => {}, + onCopyFileContent = () => {}, + onDownloadFile = () => {}, + copyingFile = false, + saving = false, + needsClone = false, + cloneTooltip = '', + branches = [], + currentBranch = null, + defaultBranch = null, + onBranchChange = () => {}, + userPubkey = null + }: Props = $props(); diff --git a/src/routes/repos/[npub]/[repo]/components/HistoryTab.svelte b/src/routes/repos/[npub]/[repo]/components/HistoryTab.svelte index fd45b2a..03adf77 100644 --- a/src/routes/repos/[npub]/[repo]/components/HistoryTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/HistoryTab.svelte @@ -5,22 +5,36 @@ import TabLayout from './TabLayout.svelte'; - export let commits: Array<{ - hash: string; - message: string; - author: string; - date: string; - files: string[]; - verification?: any; - }> = []; - export let selectedCommit: string | null = null; - export let loading: boolean = false; - export let error: string | null = null; - export let onSelect: (hash: string) => void = () => {}; - export let onVerify: (hash: string) => void = () => {}; - export let verifyingCommits: Set = new Set(); - export let showDiff: boolean = false; - export let diffData: Array<{ file: string; additions: number; deletions: number; diff: string }> = []; + interface Props { + commits?: Array<{ + hash: string; + message: string; + author: string; + date: string; + files: string[]; + verification?: any; + }>; + selectedCommit?: string | null; + loading?: boolean; + error?: string | null; + onSelect?: (hash: string) => void; + onVerify?: (hash: string) => void; + verifyingCommits?: Set; + showDiff?: boolean; + diffData?: Array<{ file: string; additions: number; deletions: number; diff: string }>; + } + + let { + commits = [], + selectedCommit = null, + loading = false, + error = null, + onSelect = () => {}, + onVerify = () => {}, + verifyingCommits = new Set(), + showDiff = false, + diffData = [] + }: Props = $props(); diff --git a/src/routes/repos/[npub]/[repo]/components/IssuesTab.svelte b/src/routes/repos/[npub]/[repo]/components/IssuesTab.svelte index bdef9f6..e8b87a0 100644 --- a/src/routes/repos/[npub]/[repo]/components/IssuesTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/IssuesTab.svelte @@ -5,23 +5,36 @@ import StatusTabLayout from './StatusTabLayout.svelte'; - export let issues: Array<{ - id: string; - subject: string; - content: string; - status: string; - author: string; - created_at: number; - kind: number; - tags?: string[][]; - }> = []; - export let selectedIssue: string | null = null; - export let loading: boolean = false; - export let error: string | null = null; - export let onSelect: (id: string) => void = () => {}; - export let onStatusUpdate: (id: string, status: string) => void = () => {}; - export let issueReplies: Array = []; - export let loadingReplies: boolean = false; + interface Props { + issues: Array<{ + id: string; + subject: string; + content: string; + status: string; + author: string; + created_at: number; + kind: number; + tags?: string[][]; + }>; + selectedIssue?: string | null; + loading?: boolean; + error?: string | null; + onSelect?: (id: string) => void; + onStatusUpdate?: (id: string, status: string) => void; + issueReplies?: Array; + loadingReplies?: boolean; + } + + let { + issues = [], + selectedIssue = null, + loading = false, + error = null, + onSelect = () => {}, + onStatusUpdate = () => {}, + issueReplies = [], + loadingReplies = false + }: Props = $props(); const items = $derived(issues.map(issue => ({ id: issue.id, diff --git a/src/routes/repos/[npub]/[repo]/components/PRsTab.svelte b/src/routes/repos/[npub]/[repo]/components/PRsTab.svelte index d309442..d006198 100644 --- a/src/routes/repos/[npub]/[repo]/components/PRsTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/PRsTab.svelte @@ -5,21 +5,32 @@ import StatusTabLayout from './StatusTabLayout.svelte'; - export let prs: Array<{ - id: string; - subject: string; - content: string; - status: string; - author: string; - created_at: number; - commitId?: string; - kind: number; - }> = []; - export let selectedPR: string | null = null; - export let loading: boolean = false; - export let error: string | null = null; - export let onSelect: (id: string) => void = () => {}; - export let onStatusUpdate: (id: string, status: string) => void = () => {}; + interface Props { + prs: Array<{ + id: string; + subject: string; + content: string; + status: string; + author: string; + created_at: number; + commitId?: string; + kind: number; + }>; + selectedPR?: string | null; + loading?: boolean; + error?: string | null; + onSelect?: (id: string) => void; + onStatusUpdate?: (id: string, status: string) => void; + } + + let { + prs = [], + selectedPR = null, + loading = false, + error = null, + onSelect = () => {}, + onStatusUpdate = () => {} + }: Props = $props(); const items = $derived(prs.map(pr => ({ id: pr.id, diff --git a/src/routes/repos/[npub]/[repo]/components/PatchesTab.svelte b/src/routes/repos/[npub]/[repo]/components/PatchesTab.svelte index 928380d..b8bbe93 100644 --- a/src/routes/repos/[npub]/[repo]/components/PatchesTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/PatchesTab.svelte @@ -5,21 +5,33 @@ import StatusTabLayout from './StatusTabLayout.svelte'; - export let patches: Array<{ - id: string; - subject: string; - content: string; - status: string; - author: string; - created_at: number; - [key: string]: any; - }> = []; - export let selectedPatch: string | null = null; - export let loading: boolean = false; - export let error: string | null = null; - export let onSelect: (id: string) => void = () => {}; - export let onApply: (id: string) => void = () => {}; - export let applying: Record = {}; + interface Props { + patches: Array<{ + id: string; + subject: string; + content: string; + status: string; + author: string; + created_at: number; + [key: string]: any; + }>; + selectedPatch?: string | null; + loading?: boolean; + error?: string | null; + onSelect?: (id: string) => void; + onApply?: (id: string) => void; + applying?: Record; + } + + let { + patches = [], + selectedPatch = null, + loading = false, + error = null, + onSelect = () => {}, + onApply = () => {}, + applying = {} + }: Props = $props(); const items = $derived(patches.map(patch => ({ id: patch.id, diff --git a/src/routes/repos/[npub]/[repo]/components/StatusTabLayout.svelte b/src/routes/repos/[npub]/[repo]/components/StatusTabLayout.svelte index 86ae4b7..cf52df4 100644 --- a/src/routes/repos/[npub]/[repo]/components/StatusTabLayout.svelte +++ b/src/routes/repos/[npub]/[repo]/components/StatusTabLayout.svelte @@ -6,20 +6,31 @@ import TabLayout from './TabLayout.svelte'; - export let items: Array<{ - id: string; - title: string; - status: string; - [key: string]: any; - }> = []; - export let selectedId: string | null = null; - export let loading: boolean = false; - export let error: string | null = null; - export let onSelect: (id: string) => void = () => {}; - export let statusGroups: Array<{ label: string; value: string }> = [ - { label: 'Open', value: 'open' }, - { label: 'Closed', value: 'closed' } - ]; + interface Props { + items?: Array<{ + id: string; + title: string; + status: string; + [key: string]: any; + }>; + selectedId?: string | null; + loading?: boolean; + error?: string | null; + onSelect?: (id: string) => void; + statusGroups?: Array<{ label: string; value: string }>; + } + + let { + items = [], + selectedId = null, + loading = false, + error = null, + onSelect = () => {}, + statusGroups = [ + { label: 'Open', value: 'open' }, + { label: 'Closed', value: 'closed' } + ] + }: Props = $props(); let selectedItem = $derived(items.find(item => item.id === selectedId) || null); diff --git a/src/routes/repos/[npub]/[repo]/components/TabLayout.svelte b/src/routes/repos/[npub]/[repo]/components/TabLayout.svelte index a526bf5..00a6094 100644 --- a/src/routes/repos/[npub]/[repo]/components/TabLayout.svelte +++ b/src/routes/repos/[npub]/[repo]/components/TabLayout.svelte @@ -4,10 +4,19 @@ * Provides left-pane/right-panel structure for all tabs */ - export let leftPane: any = null; - export let rightPanel: any = null; - export let loading: boolean = false; - export let error: string | null = null; + interface Props { + leftPane?: any; + rightPanel?: any; + loading?: boolean; + error?: string | null; + } + + let { + leftPane = null, + rightPanel = null, + loading = false, + error = null + }: Props = $props();
diff --git a/src/routes/repos/[npub]/[repo]/hooks/use-repo-data.ts b/src/routes/repos/[npub]/[repo]/hooks/use-repo-data.ts index 7188bf1..f85f30f 100644 --- a/src/routes/repos/[npub]/[repo]/hooks/use-repo-data.ts +++ b/src/routes/repos/[npub]/[repo]/hooks/use-repo-data.ts @@ -28,11 +28,15 @@ export function useRepoData( if (typeof window === 'undefined' || !state.isMounted) return; try { - const data = $page.data as RepoPageData; - if (data && state.isMounted) { - setPageData(data || {}); - logger.debug({ hasAnnouncement: !!data.announcement }, 'Page data loaded'); - } + page.subscribe(p => { + if (p && state.isMounted) { + const data = p.data as RepoPageData; + if (data) { + setPageData(data || {}); + logger.debug({ hasAnnouncement: !!data.announcement }, 'Page data loaded'); + } + } + }); } catch (err) { if (state.isMounted) { logger.warn({ error: err }, 'Failed to update pageData'); @@ -51,15 +55,19 @@ export function useRepoParams( if (typeof window === 'undefined' || !state.isMounted) return; try { - const params = $page.params as { npub?: string; repo?: string }; - if (params && state.isMounted) { - if (params.npub && params.npub !== state.userPubkey) { - setNpub(params.npub); - } - if (params.repo) { - setRepo(params.repo); + page.subscribe(p => { + if (p && state.isMounted) { + const params = p.params as { npub?: string; repo?: string }; + if (params) { + if (params.npub && params.npub !== state.userPubkey) { + setNpub(params.npub); + } + if (params.repo) { + setRepo(params.repo); + } + } } - } + }); } catch { // If $page.params fails, try to parse from URL path if (!state.isMounted) return;