From 57dbb4d7d8896097e27c15dcf8735aa4b75a58ad Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 26 Feb 2026 14:20:54 +0100 Subject: [PATCH] refactor 6 Nostr-Signature: cd9b7e015ee3bd6a4c4ab7d54d90ab411a08c29f249158c4cdea2b12996b6b44 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 2a8fd0a3718169df1517c0b939ec9ea9793da4dc07b20d9366f09fe70b5268e94451916f975e2cea1a3741419440189d31f844e79863e84de00c0be7c449e92a --- nostr/commit-signatures.jsonl | 1 + src/routes/repos/[npub]/[repo]/+page.svelte | 447 +----------------- .../[repo]/services/code-search-operations.ts | 44 ++ .../[npub]/[repo]/services/file-operations.ts | 99 ++++ .../[repo]/services/patch-operations.ts | 174 +++++++ .../[npub]/[repo]/services/repo-operations.ts | 194 ++++++++ 6 files changed, 535 insertions(+), 424 deletions(-) create mode 100644 src/routes/repos/[npub]/[repo]/services/code-search-operations.ts diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 670ac16..7109ae9 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -98,3 +98,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772108817,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix build"]],"content":"Signed commit: fix build","id":"a37754536125d75a5c55f6af3b5521f89839e797ad1bffb69e3d313939cb7b65","sig":"6bcca1a025e4478ae330d3664dd2b9cff55f4bec82065ab2afb5bfb92031f7dde3264657dd892fe844396990117048b19247b0ef7423139f89d4cbf46b47f828"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772109639,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor the refactor"]],"content":"Signed commit: refactor the refactor","id":"62aafbdadfd37b20f1b16742a297e2b17d59dd3d6930e64e75d0d1b6a2f04bd6","sig":"050eaca1703b73443b51fd160932a2edfa04fc0a5efd3b5bb0a1e4c8b944caa60d444b2148c07b74f4ff4a589984fa524a7109a2a89c3eddf6c937b23b18c69b"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772110337,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 4"]],"content":"Signed commit: refactor 4","id":"d330d1a096e5f3951e8b2a66160a23c5ac28aa94313ecd0948c7e50baa60bdbb","sig":"febf4088cca3f7223f55ab300ed7fdb7b333c03d2534b05721dfaf9d9284f4599b385ba54379890fa6b846aed02d656a5e45429a6dd571dddbb997be6d8159b2"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772111536,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 5"]],"content":"Signed commit: refactor 5","id":"47651ed0aee8072f356fbac30b6168f2c985bcca392f9ed7d7c38d9670d90f16","sig":"2ca5d04d4a619dc3e02962249f7c650e3a561315897b329f4493e87148c5dd89fbcb6694515a72d0d17e64c9930e57bd7761e27b353275bb1ada9449330f4e1c"} diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 1b5e9fb..4a46298 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -112,8 +112,14 @@ import { loadPatches as loadPatchesService, createPatch as createPatchService, - updatePatchStatus as updatePatchStatusService + updatePatchStatus as updatePatchStatusService, + loadPatchHighlights as loadPatchHighlightsService, + createPatchHighlight as createPatchHighlightService, + createPatchComment as createPatchCommentService } from './services/patch-operations.js'; + import { + performCodeSearch as performCodeSearchService + } from './services/code-search-operations.js'; import { loadDiscussions as loadDiscussionsService, createDiscussionThread as createDiscussionThreadService, @@ -127,8 +133,15 @@ toggleBookmark as toggleBookmarkService, checkMaintainerStatus as checkMaintainerStatusService, loadAllMaintainers as loadAllMaintainersService, - checkVerification as checkVerificationService + checkVerification as checkVerificationService, + loadBookmarkStatus as loadBookmarkStatusService, + loadCloneUrlReachability as loadCloneUrlReachabilityService, + loadForkInfo as loadForkInfoService, + loadRepoImages as loadRepoImagesService } from './services/repo-operations.js'; + import { + loadReadme as loadReadmeService + } from './services/file-operations.js'; // Consolidated state - all state variables in one object let state = $state(createRepoState()); @@ -772,136 +785,11 @@ // Load clone URL reachability status async function loadCloneUrlReachability(forceRefresh: boolean = false) { - if (!repoCloneUrls || repoCloneUrls.length === 0) { - return; - } - - if (state.loading.reachability) return; - - state.loading.reachability = true; - try { - const response = await fetch( - `/api/repos/${state.npub}/${state.repo}/clone-urls/reachability${forceRefresh ? '?forceRefresh=true' : ''}`, - { - headers: buildApiHeaders() - } - ); - - if (response.ok) { - const data = await response.json(); - const newMap = new Map(); - - if (data.results && Array.isArray(data.results)) { - for (const result of data.results) { - newMap.set(result.url, { - reachable: result.reachable, - error: result.error, - checkedAt: result.checkedAt, - serverType: result.serverType || 'unknown' - }); - } - } - - state.clone.reachability = newMap; - } - } catch (err) { - console.warn('Failed to load clone URL reachability:', err); - } finally { - state.loading.reachability = false; - state.clone.checkingReachability.clear(); - } + await loadCloneUrlReachabilityService(forceRefresh, state, repoCloneUrls); } async function loadReadme() { - if (state.loading.repoNotFound) return; - state.loading.readme = true; - try { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/readme?ref=${state.git.currentBranch}`, { - headers: buildApiHeaders() - }); - if (response.ok) { - const data = await response.json(); - if (data.found) { - state.preview.readme.content = data.content; - state.preview.readme.path = data.path; - state.preview.readme.isMarkdown = data.isMarkdown; - - // Reset preview mode for README - state.preview.file.showPreview = true; - state.preview.readme.html = ''; - - // Render markdown or asciidoc if needed - if (state.preview.readme.content) { - const ext = state.preview.readme.path?.split('.').pop()?.toLowerCase() || ''; - if (state.preview.readme.isMarkdown || ext === 'md' || ext === 'markdown') { - try { - const MarkdownIt = (await import('markdown-it')).default; - const hljsModule = await import('highlight.js'); - const hljs = hljsModule.default || hljsModule; - - const md = new MarkdownIt({ - html: true, // Enable HTML state.git.tags in source - linkify: true, // Autoconvert URL-like text to links - typographer: true, // Enable some language-neutral replacement + quotes beautification - breaks: true, // Convert '\n' in paragraphs into
- highlight: function (str: string, lang: string): string { - if (lang && hljs.getLanguage(lang)) { - try { - return '
' +
-                               hljs.highlight(str, { language: lang }).value +
-                               '
'; - } catch (err) { - // Fallback to escaped HTML if highlighting fails - // This is expected for unsupported languages - } - } - return '
' + md.utils.escapeHtml(str) + '
'; - } - }); - - let rendered = md.render(state.preview.readme.content); - // Rewrite image paths to point to repository API - rendered = rewriteImagePaths(rendered, state.preview.readme.path); - state.preview.readme.html = rendered; - console.log('[README] Markdown rendered successfully, HTML length:', state.preview.readme.html.length); - } catch (err) { - console.error('[README] Error rendering markdown:', err); - state.preview.readme.html = ''; - } - } else if (ext === 'adoc' || ext === 'asciidoc') { - try { - const Asciidoctor = (await import('@asciidoctor/core')).default; - const asciidoctor = Asciidoctor(); - const converted = asciidoctor.convert(state.preview.readme.content, { - safe: 'safe', - attributes: { - 'source-highlighter': 'highlight.js' - } - }); - let rendered = typeof converted === 'string' ? converted : String(converted); - // Rewrite image paths to point to repository API - rendered = rewriteImagePaths(rendered, state.preview.readme.path); - state.preview.readme.html = rendered; - state.preview.readme.isMarkdown = true; // Treat as markdown for display purposes - } catch (err) { - console.error('[README] Error rendering asciidoc:', err); - state.preview.readme.html = ''; - } - } else if (ext === 'html' || ext === 'htm') { - // Rewrite image paths to point to repository API - state.preview.readme.html = rewriteImagePaths(state.preview.readme.content || '', state.preview.readme.path); - state.preview.readme.isMarkdown = true; // Treat as markdown for display purposes - } else { - state.preview.readme.html = ''; - } - } - } - } - } catch (err) { - console.error('Error state.loading.main README:', err); - } finally { - state.loading.readme = false; - } + await loadReadmeService(state, rewriteImagePaths); } // File processing utilities are now imported from utils/file-processing.ts @@ -1011,92 +899,7 @@ } async function loadRepoImages() { - try { - // Get images from page data (loaded from announcement) - // Use $page.data directly to ensure we get the latest data - // Guard against SSR - $page store can only be accessed in component context - if (typeof window === 'undefined') return; - const data = $page.data as typeof state.pageData; - if (data.image) { - state.metadata.image = data.image; - console.log('[Repo Images] Loaded image from pageData:', state.metadata.image); - } - if (data.banner) { - state.metadata.banner = data.banner; - console.log('[Repo Images] Loaded banner from pageData:', state.metadata.banner); - } - - // Also fetch from announcement directly as fallback (only if not private or user has access) - if (!state.metadata.image && !state.metadata.banner) { - // Guard against SSR - $page store can only be accessed in component context - if (typeof window === 'undefined') return; - const data = $page.data as typeof state.pageData; - // Check access for private repos - if (repoIsPrivate) { - const headers: Record = {}; - if (state.user.pubkey) { - try { - const decoded = nip19.decode(state.user.pubkey); - if (decoded.type === 'npub') { - headers['X-User-Pubkey'] = decoded.data as string; - } else { - headers['X-User-Pubkey'] = state.user.pubkey; - } - } catch { - headers['X-User-Pubkey'] = state.user.pubkey; - } - } - - const accessResponse = await fetch(`/api/repos/${state.npub}/${state.repo}/access`, { headers }); - if (!accessResponse.ok) { - // Access check failed, don't fetch images - return; - } - const accessData = await accessResponse.json(); - if (!accessData.canView) { - // User doesn't have access, don't fetch images - return; - } - } - - const decoded = nip19.decode(state.npub); - if (decoded.type === 'npub') { - const repoOwnerPubkey = decoded.data as string; - const client = new NostrClient(DEFAULT_NOSTR_RELAYS); - const events = await client.fetchEvents([ - { - kinds: [30617], // REPO_ANNOUNCEMENT - authors: [repoOwnerPubkeyDerived], - '#d': [state.repo], - limit: 1 - } - ]); - - if (events.length > 0) { - const announcement = events[0]; - const imageTag = announcement.tags.find((t: string[]) => t[0] === 'image'); - const bannerTag = announcement.tags.find((t: string[]) => t[0] === 'banner'); - - if (imageTag?.[1]) { - state.metadata.image = imageTag[1]; - console.log('[Repo Images] Loaded image from announcement:', state.metadata.image); - } - if (bannerTag?.[1]) { - state.metadata.banner = bannerTag[1]; - console.log('[Repo Images] Loaded banner from announcement:', state.metadata.banner); - } - } else { - console.log('[Repo Images] No announcement found'); - } - } - } - - if (!state.metadata.image && !state.metadata.banner) { - console.log('[Repo Images] No images found in announcement'); - } - } catch (err) { - console.error('Error state.loading.main repo images:', err); - } + await loadRepoImagesService(state, repoOwnerPubkeyDerived, repoIsPrivate, $page.data); } // Reactively update images when pageData changes (only once, when data becomes available) @@ -1373,13 +1176,7 @@ async function loadBookmarkStatus() { - if (!state.user.pubkey || !state.metadata.address || !bookmarksService) return; - - try { - state.bookmark.isBookmarked = await bookmarksService.isBookmarked(state.user.pubkey, state.metadata.address); - } catch (err) { - console.warn('Failed to load bookmark status:', err); - } + await loadBookmarkStatusService(state, bookmarksService); } async function toggleBookmark() { @@ -2425,50 +2222,7 @@ } async function performCodeSearch() { - if (!state.codeSearch.query.trim() || state.codeSearch.query.length < 2) { - state.codeSearch.results = []; - return; - } - - state.loading.codeSearch = true; - state.error = null; - - try { - // Get current branch for repo-specific search - const branchParam = state.codeSearch.scope === 'repo' && state.git.currentBranch - ? `&branch=${encodeURIComponent(state.git.currentBranch)}` - : ''; - - // For "All Repositories", don't pass repo filter - let it search all repos - const url = state.codeSearch.scope === 'repo' - ? `/api/repos/${state.npub}/${state.repo}/code-search?q=${encodeURIComponent(state.codeSearch.query.trim())}${branchParam}` - : `/api/code-search?q=${encodeURIComponent(state.codeSearch.query.trim())}`; - - const response = await fetch(url, { - headers: buildApiHeaders() - }); - - if (response.ok) { - const data = await response.json(); - state.codeSearch.results = Array.isArray(data) ? data : []; - } else { - let errorMessage = 'Failed to search code'; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorMessage; - } catch { - errorMessage = `Search failed: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to search code'; - console.error('[Code Search] Error:', err); - state.error = errorMessage; - state.codeSearch.results = []; - } finally { - state.loading.codeSearch = false; - } + await performCodeSearchService(state); } async function loadIssues() { @@ -2524,29 +2278,7 @@ } async function loadPatchHighlights(patchId: string, patchAuthor: string) { - if (!patchId || !patchAuthor) return; - - state.loading.patchHighlights = true; - try { - const decoded = nip19.decode(state.npub); - if (decoded.type !== 'npub') { - throw new Error('Invalid npub format'); - } - const repoOwnerPubkey = decoded.data as string; - - const response = await fetch( - `/api/repos/${state.npub}/${state.repo}/highlights?patchId=${patchId}&patchAuthor=${patchAuthor}` - ); - if (response.ok) { - const data = await response.json(); - state.patchHighlights = data.highlights || []; - state.patchComments = data.comments || []; - } - } catch (err) { - console.error('Failed to load patch highlights:', err); - } finally { - state.loading.patchHighlights = false; - } + await loadPatchHighlightsService(patchId, patchAuthor, state); } function handlePatchCodeSelection( @@ -2567,65 +2299,7 @@ } async function createPatchHighlight() { - if (!state.user.pubkey || !state.forms.patchHighlight.text.trim() || !state.selected.patch) return; - - const patch = state.patches.find(p => p.id === state.selected.patch); - if (!patch) return; - - state.creating.patchHighlight = true; - state.error = null; - - try { - const decoded = nip19.decode(state.npub); - if (decoded.type !== 'npub') { - throw new Error('Invalid npub format'); - } - const repoOwnerPubkey = decoded.data as string; - - const eventTemplate = highlightsService.createHighlightEvent( - state.forms.patchHighlight.text, - patch.id, - patch.author, - repoOwnerPubkey, - state.repo, - KIND.PATCH, // targetKind - undefined, // filePath - state.forms.patchHighlight.startLine, // lineStart - state.forms.patchHighlight.endLine, // lineEnd - undefined, // context - state.forms.patchHighlight.comment.trim() || undefined // comment - ); - - const signedEvent = await signEventWithNIP07(eventTemplate); - - const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS); - const { outbox } = await getUserRelays(state.user.pubkey, tempClient); - const combinedRelays = combineRelays(outbox); - - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/highlights`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: 'highlight', - event: signedEvent, - userPubkey: state.user.pubkey - }) - }); - - if (response.ok) { - state.openDialog = null; - state.forms.patchHighlight.text = ''; - state.forms.patchHighlight.comment = ''; - await loadPatchHighlights(patch.id, patch.author); - } else { - const data = await response.json(); - state.error = data.state.error || 'Failed to create highlight'; - } - } catch (err) { - state.error = err instanceof Error ? err.message : 'Failed to create highlight'; - } finally { - state.creating.patchHighlight = false; - } + await createPatchHighlightService(state, highlightsService, { loadPatches }); } function formatPubkey(pubkey: string): string { @@ -2646,82 +2320,7 @@ } async function createPatchComment() { - if (!state.user.pubkey || !state.forms.patchComment.content.trim() || !state.selected.patch) return; - - const patch = state.patches.find(p => p.id === state.selected.patch); - if (!patch) return; - - state.creating.patchComment = true; - state.error = null; - - try { - const decoded = nip19.decode(state.npub); - if (decoded.type !== 'npub') { - throw new Error('Invalid npub format'); - } - const repoOwnerPubkey = decoded.data as string; - - const rootEventId = state.forms.patchComment.replyingTo || patch.id; - const rootEventKind = state.forms.patchComment.replyingTo ? KIND.COMMENT : KIND.PATCH; - const rootPubkey = state.forms.patchComment.replyingTo ? - (state.patchComments.find(c => c.id === state.forms.patchComment.replyingTo)?.pubkey || patch.author) : - patch.author; - - let parentEventId: string | undefined; - let parentEventKind: number | undefined; - let parentPubkey: string | undefined; - - if (state.forms.patchComment.replyingTo) { - // Reply to a comment - const parentComment = state.patchComments.find(c => c.id === state.forms.patchComment.replyingTo) || - state.patchHighlights.flatMap(h => h.comments || []).find(c => c.id === state.forms.patchComment.replyingTo); - if (parentComment) { - parentEventId = state.forms.patchComment.replyingTo; - parentEventKind = KIND.COMMENT; - parentPubkey = parentComment.pubkey; - } - } - - const eventTemplate = highlightsService.createCommentEvent( - state.forms.patchComment.content.trim(), - rootEventId, - rootEventKind, - rootPubkey, - parentEventId, - parentEventKind, - parentPubkey - ); - - const signedEvent = await signEventWithNIP07(eventTemplate); - - const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS); - const { outbox } = await getUserRelays(state.user.pubkey, tempClient); - const combinedRelays = combineRelays(outbox); - - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/highlights`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: 'comment', - event: signedEvent, - userPubkey: state.user.pubkey - }) - }); - - if (response.ok) { - state.openDialog = null; - state.forms.patchComment.content = ''; - state.forms.patchComment.replyingTo = null; - await loadPatchHighlights(patch.id, patch.author); - } else { - const data = await response.json(); - state.error = data.state.error || 'Failed to create comment'; - } - } catch (err) { - state.error = err instanceof Error ? err.message : 'Failed to create comment'; - } finally { - state.creating.patchComment = false; - } + await createPatchCommentService(state, highlightsService, { loadPatches }); } // Initialize patch highlights effect diff --git a/src/routes/repos/[npub]/[repo]/services/code-search-operations.ts b/src/routes/repos/[npub]/[repo]/services/code-search-operations.ts new file mode 100644 index 0000000..b63be1a --- /dev/null +++ b/src/routes/repos/[npub]/[repo]/services/code-search-operations.ts @@ -0,0 +1,44 @@ +/** + * Code search operations service + * Handles code search functionality + */ + +import type { RepoState } from '../stores/repo-state.js'; +import { apiRequest, buildApiHeaders } from '../utils/api-client.js'; + +/** + * Perform code search + */ +export async function performCodeSearch( + state: RepoState +): Promise { + if (!state.codeSearch.query.trim() || state.codeSearch.query.length < 2) { + state.codeSearch.results = []; + return; + } + + state.loading.codeSearch = true; + state.error = null; + + try { + // Get current branch for repo-specific search + const branchParam = state.codeSearch.scope === 'repo' && state.git.currentBranch + ? `&branch=${encodeURIComponent(state.git.currentBranch)}` + : ''; + + // For "All Repositories", don't pass repo filter - let it search all repos + const url = state.codeSearch.scope === 'repo' + ? `/api/repos/${state.npub}/${state.repo}/code-search?q=${encodeURIComponent(state.codeSearch.query.trim())}${branchParam}` + : `/api/code-search?q=${encodeURIComponent(state.codeSearch.query.trim())}`; + + const data = await apiRequest>(url); + state.codeSearch.results = Array.isArray(data) ? data : []; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to search code'; + console.error('[Code Search] Error:', err); + state.error = errorMessage; + state.codeSearch.results = []; + } finally { + state.loading.codeSearch = false; + } +} diff --git a/src/routes/repos/[npub]/[repo]/services/file-operations.ts b/src/routes/repos/[npub]/[repo]/services/file-operations.ts index 7def54d..9f3e700 100644 --- a/src/routes/repos/[npub]/[repo]/services/file-operations.ts +++ b/src/routes/repos/[npub]/[repo]/services/file-operations.ts @@ -253,3 +253,102 @@ export async function deleteFile( state.saving = false; } } + +/** + * Load README file + */ +export async function loadReadme( + state: RepoState, + rewriteImagePaths: (html: string, filePath: string | null) => string +): Promise { + if (state.loading.repoNotFound) return; + state.loading.readme = true; + try { + const { apiRequest } = await import('../utils/api-client.js'); + const data = await apiRequest<{ + found?: boolean; + content?: string; + path?: string; + isMarkdown?: boolean; + }>(`/api/repos/${state.npub}/${state.repo}/readme?ref=${state.git.currentBranch}`); + + if (data.found) { + state.preview.readme.content = data.content || null; + state.preview.readme.path = data.path || null; + state.preview.readme.isMarkdown = data.isMarkdown || false; + + // Reset preview mode for README + state.preview.file.showPreview = true; + state.preview.readme.html = ''; + + // Render markdown or asciidoc if needed + if (state.preview.readme.content) { + const ext = state.preview.readme.path?.split('.').pop()?.toLowerCase() || ''; + if (state.preview.readme.isMarkdown || ext === 'md' || ext === 'markdown') { + try { + const MarkdownIt = (await import('markdown-it')).default; + const hljsModule = await import('highlight.js'); + const hljs = hljsModule.default || hljsModule; + + const md = new MarkdownIt({ + html: true, // Enable HTML tags in source + linkify: true, // Autoconvert URL-like text to links + typographer: true, // Enable some language-neutral replacement + quotes beautification + breaks: true, // Convert '\n' in paragraphs into
+ highlight: function (str: string, lang: string): string { + if (lang && hljs.getLanguage(lang)) { + try { + return '
' +
+                           hljs.highlight(str, { language: lang }).value +
+                           '
'; + } catch (err) { + // Fallback to escaped HTML if highlighting fails + } + } + return '
' + md.utils.escapeHtml(str) + '
'; + } + }); + + let rendered = md.render(state.preview.readme.content); + // Rewrite image paths to point to repository API + rendered = rewriteImagePaths(rendered, state.preview.readme.path); + state.preview.readme.html = rendered; + console.log('[README] Markdown rendered successfully, HTML length:', state.preview.readme.html.length); + } catch (err) { + console.error('[README] Error rendering markdown:', err); + state.preview.readme.html = ''; + } + } else if (ext === 'adoc' || ext === 'asciidoc') { + try { + const Asciidoctor = (await import('@asciidoctor/core')).default; + const asciidoctor = Asciidoctor(); + const converted = asciidoctor.convert(state.preview.readme.content, { + safe: 'safe', + attributes: { + 'source-highlighter': 'highlight.js' + } + }); + let rendered = typeof converted === 'string' ? converted : String(converted); + // Rewrite image paths to point to repository API + rendered = rewriteImagePaths(rendered, state.preview.readme.path); + state.preview.readme.html = rendered; + state.preview.readme.isMarkdown = true; // Treat as markdown for display purposes + } catch (err) { + console.error('[README] Error rendering asciidoc:', err); + state.preview.readme.html = ''; + } + } else if (ext === 'html' || ext === 'htm') { + // Rewrite image paths to point to repository API + state.preview.readme.html = rewriteImagePaths(state.preview.readme.content || '', state.preview.readme.path); + state.preview.readme.isMarkdown = true; // Treat as markdown for display purposes + } else { + state.preview.readme.html = ''; + } + } + } + } catch (err) { + console.error('Error loading README:', err); + } finally { + state.loading.readme = false; + } +} diff --git a/src/routes/repos/[npub]/[repo]/services/patch-operations.ts b/src/routes/repos/[npub]/[repo]/services/patch-operations.ts index 7ffd7a3..0ea8f16 100644 --- a/src/routes/repos/[npub]/[repo]/services/patch-operations.ts +++ b/src/routes/repos/[npub]/[repo]/services/patch-operations.ts @@ -199,3 +199,177 @@ export async function updatePatchStatus( state.statusUpdates.patch[patchId] = false; } } + +/** + * Load patch highlights and comments + */ +export async function loadPatchHighlights( + patchId: string, + patchAuthor: string, + state: RepoState +): Promise { + if (!patchId || !patchAuthor) return; + + state.loading.patchHighlights = true; + try { + const decoded = nip19.decode(state.npub); + if (decoded.type !== 'npub') { + throw new Error('Invalid npub format'); + } + + const data = await apiRequest<{ + highlights?: Array; + comments?: Array; + }>(`/api/repos/${state.npub}/${state.repo}/highlights?patchId=${patchId}&patchAuthor=${patchAuthor}`); + + state.patchHighlights = data.highlights || []; + state.patchComments = data.comments || []; + } catch (err) { + console.error('Failed to load patch highlights:', err); + } finally { + state.loading.patchHighlights = false; + } +} + +/** + * Create a patch highlight + */ +export async function createPatchHighlight( + state: RepoState, + highlightsService: any, + callbacks: PatchOperationsCallbacks +): Promise { + if (!state.user.pubkey || !state.forms.patchHighlight.text.trim() || !state.selected.patch) return; + + const patch = state.patches.find(p => p.id === state.selected.patch); + if (!patch) return; + + state.creating.patchHighlight = true; + state.error = null; + + try { + const decoded = nip19.decode(state.npub); + if (decoded.type !== 'npub') { + throw new Error('Invalid npub format'); + } + const repoOwnerPubkey = decoded.data as string; + + const eventTemplate = highlightsService.createHighlightEvent( + state.forms.patchHighlight.text, + patch.id, + patch.author, + repoOwnerPubkey, + state.repo, + KIND.PATCH, // targetKind + undefined, // filePath + state.forms.patchHighlight.startLine, // lineStart + state.forms.patchHighlight.endLine, // lineEnd + undefined, // context + state.forms.patchHighlight.comment.trim() || undefined // comment + ); + + const signedEvent = await signEventWithNIP07(eventTemplate); + + const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + const { outbox } = await getUserRelays(state.user.pubkey, tempClient); + const combinedRelays = combineRelays(outbox); + + await apiRequest(`/api/repos/${state.npub}/${state.repo}/highlights`, { + method: 'POST', + body: JSON.stringify({ + type: 'highlight', + event: signedEvent, + userPubkey: state.user.pubkey + }) + } as RequestInit); + + state.openDialog = null; + state.forms.patchHighlight.text = ''; + state.forms.patchHighlight.comment = ''; + await loadPatchHighlights(patch.id, patch.author, state); + } catch (err) { + state.error = err instanceof Error ? err.message : 'Failed to create highlight'; + } finally { + state.creating.patchHighlight = false; + } +} + +/** + * Create a patch comment + */ +export async function createPatchComment( + state: RepoState, + highlightsService: any, + callbacks: PatchOperationsCallbacks +): Promise { + if (!state.user.pubkey || !state.forms.patchComment.content.trim() || !state.selected.patch) return; + + const patch = state.patches.find(p => p.id === state.selected.patch); + if (!patch) return; + + state.creating.patchComment = true; + state.error = null; + + try { + const decoded = nip19.decode(state.npub); + if (decoded.type !== 'npub') { + throw new Error('Invalid npub format'); + } + const repoOwnerPubkey = decoded.data as string; + + const rootEventId = state.forms.patchComment.replyingTo || patch.id; + const rootEventKind = state.forms.patchComment.replyingTo ? KIND.COMMENT : KIND.PATCH; + const rootPubkey = state.forms.patchComment.replyingTo ? + (state.patchComments.find(c => c.id === state.forms.patchComment.replyingTo)?.pubkey || patch.author) : + patch.author; + + let parentEventId: string | undefined; + let parentEventKind: number | undefined; + let parentPubkey: string | undefined; + + if (state.forms.patchComment.replyingTo) { + // Reply to a comment + const parentComment = state.patchComments.find(c => c.id === state.forms.patchComment.replyingTo) || + state.patchHighlights.flatMap(h => h.comments || []).find(c => c.id === state.forms.patchComment.replyingTo); + if (parentComment) { + parentEventId = state.forms.patchComment.replyingTo; + parentEventKind = KIND.COMMENT; + parentPubkey = parentComment.pubkey; + } + } + + const eventTemplate = highlightsService.createCommentEvent( + state.forms.patchComment.content.trim(), + rootEventId, + rootEventKind, + rootPubkey, + parentEventId, + parentEventKind, + parentPubkey + ); + + const signedEvent = await signEventWithNIP07(eventTemplate); + + const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + const { outbox } = await getUserRelays(state.user.pubkey, tempClient); + const combinedRelays = combineRelays(outbox); + + await apiRequest(`/api/repos/${state.npub}/${state.repo}/highlights`, { + method: 'POST', + body: JSON.stringify({ + type: 'comment', + event: signedEvent, + userPubkey: state.user.pubkey + }) + } as RequestInit); + + state.openDialog = null; + state.forms.patchComment.content = ''; + state.forms.patchComment.replyingTo = null; + await loadPatchHighlights(patch.id, patch.author, state); + } catch (err) { + state.error = err instanceof Error ? err.message : 'Failed to create comment'; + } finally { + state.creating.patchComment = false; + } +} diff --git a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts index 5017f53..5233291 100644 --- a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts +++ b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts @@ -363,3 +363,197 @@ export async function checkVerification( console.log('[Verification] Status after check:', state.verification.status); } } + +/** + * Load bookmark status + */ +export async function loadBookmarkStatus( + state: RepoState, + bookmarksService: any +): Promise { + if (!state.user.pubkey || !state.metadata.address || !bookmarksService) return; + + try { + state.bookmark.isBookmarked = await bookmarksService.isBookmarked(state.user.pubkey, state.metadata.address); + } catch (err) { + console.warn('Failed to load bookmark status:', err); + } +} + +/** + * Load clone URL reachability status + */ +export async function loadCloneUrlReachability( + forceRefresh: boolean, + state: RepoState, + repoCloneUrls: string[] | undefined +): Promise { + if (!repoCloneUrls || repoCloneUrls.length === 0) { + return; + } + + if (state.loading.reachability) return; + + state.loading.reachability = true; + try { + const data = await apiRequest<{ + results?: Array<{ + url: string; + reachable: boolean; + error?: string; + checkedAt: number; + serverType?: 'git' | 'grasp' | 'unknown'; + }>; + }>(`/api/repos/${state.npub}/${state.repo}/clone-urls/reachability${forceRefresh ? '?forceRefresh=true' : ''}`); + + const newMap = new Map(); + + if (data.results && Array.isArray(data.results)) { + for (const result of data.results) { + newMap.set(result.url, { + reachable: result.reachable, + error: result.error, + checkedAt: result.checkedAt, + serverType: result.serverType || 'unknown' + }); + } + } + + state.clone.reachability = newMap; + } catch (err) { + console.warn('Failed to load clone URL reachability:', err); + } finally { + state.loading.reachability = false; + state.clone.checkingReachability.clear(); + } +} + +/** + * Load fork information + */ +export async function loadForkInfo( + state: RepoState +): Promise { + try { + const data = await apiRequest<{ + isFork?: boolean; + originalRepo?: { + npub: string; + repo: string; + }; + }>(`/api/repos/${state.npub}/${state.repo}/fork`); + + if (data.isFork && data.originalRepo) { + state.fork.info = { + isFork: true, + originalRepo: data.originalRepo + }; + } else { + state.fork.info = { + isFork: false, + originalRepo: null + }; + } + } catch (err) { + console.error('Failed to load fork info:', err); + state.fork.info = { + isFork: false, + originalRepo: null + }; + } +} + +/** + * Load repository images (image and banner) + */ +export async function loadRepoImages( + state: RepoState, + repoOwnerPubkeyDerived: string | null, + repoIsPrivate: boolean, + pageData: any +): Promise { + try { + // Get images from page data (loaded from announcement) + if (typeof window === 'undefined') return; + if (pageData?.image) { + state.metadata.image = pageData.image; + console.log('[Repo Images] Loaded image from pageData:', state.metadata.image); + } + if (pageData?.banner) { + state.metadata.banner = pageData.banner; + console.log('[Repo Images] Loaded banner from pageData:', state.metadata.banner); + } + + // Also fetch from announcement directly as fallback (only if not private or user has access) + if (!state.metadata.image && !state.metadata.banner && repoOwnerPubkeyDerived) { + if (typeof window === 'undefined') return; + + // Check access for private repos + if (repoIsPrivate) { + const headers: Record = {}; + if (state.user.pubkey) { + try { + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(state.user.pubkey); + if (decoded.type === 'npub') { + headers['X-User-Pubkey'] = decoded.data as string; + } else { + headers['X-User-Pubkey'] = state.user.pubkey; + } + } catch { + headers['X-User-Pubkey'] = state.user.pubkey; + } + } + + const accessData = await apiRequest<{ canView?: boolean }>(`/api/repos/${state.npub}/${state.repo}/access`, { + headers + } as RequestInit); + + if (!accessData.canView) { + // User doesn't have access, don't fetch images + return; + } + } + + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(state.npub); + if (decoded.type === 'npub') { + const repoOwnerPubkey = decoded.data as string; + const { NostrClient } = await import('$lib/services/nostr/nostr-client.js'); + const { DEFAULT_NOSTR_RELAYS } = await import('$lib/config.js'); + const client = new NostrClient(DEFAULT_NOSTR_RELAYS); + const events = await client.fetchEvents([ + { + kinds: [30617], // REPO_ANNOUNCEMENT + authors: [repoOwnerPubkeyDerived], + '#d': [state.repo], + limit: 1 + } + ]); + + if (events.length > 0) { + const announcement = events[0]; + const imageTag = announcement.tags.find((t: string[]) => t[0] === 'image'); + const bannerTag = announcement.tags.find((t: string[]) => t[0] === 'banner'); + + if (imageTag?.[1]) { + state.metadata.image = imageTag[1]; + console.log('[Repo Images] Loaded image from announcement:', state.metadata.image); + } + if (bannerTag?.[1]) { + state.metadata.banner = bannerTag[1]; + console.log('[Repo Images] Loaded banner from announcement:', state.metadata.banner); + } + } else { + console.log('[Repo Images] No announcement found'); + } + } + } + + if (!state.metadata.image && !state.metadata.banner) { + console.log('[Repo Images] No images found in announcement'); + } + } catch (err) { + console.error('Error loading repo images:', err); + } +}