Browse Source

refactor 6

Nostr-Signature: cd9b7e015ee3bd6a4c4ab7d54d90ab411a08c29f249158c4cdea2b12996b6b44 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 2a8fd0a3718169df1517c0b939ec9ea9793da4dc07b20d9366f09fe70b5268e94451916f975e2cea1a3741419440189d31f844e79863e84de00c0be7c449e92a
main
Silberengel 2 weeks ago
parent
commit
57dbb4d7d8
  1. 1
      nostr/commit-signatures.jsonl
  2. 447
      src/routes/repos/[npub]/[repo]/+page.svelte
  3. 44
      src/routes/repos/[npub]/[repo]/services/code-search-operations.ts
  4. 99
      src/routes/repos/[npub]/[repo]/services/file-operations.ts
  5. 174
      src/routes/repos/[npub]/[repo]/services/patch-operations.ts
  6. 194
      src/routes/repos/[npub]/[repo]/services/repo-operations.ts

1
nostr/commit-signatures.jsonl

@ -98,3 +98,4 @@ @@ -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"}

447
src/routes/repos/[npub]/[repo]/+page.svelte

@ -112,8 +112,14 @@ @@ -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 @@ @@ -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 @@ @@ -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<string, { reachable: boolean; error?: string; checkedAt: number; serverType: 'git' | 'grasp' | 'unknown' }>();
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 <br>
highlight: function (str: string, lang: string): string {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value +
'</code></pre>';
} catch (err) {
// Fallback to escaped HTML if highlighting fails
// This is expected for unsupported languages
}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
}
});
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 @@ @@ -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<string, string> = {};
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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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

44
src/routes/repos/[npub]/[repo]/services/code-search-operations.ts

@ -0,0 +1,44 @@ @@ -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<void> {
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<Array<any>>(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;
}
}

99
src/routes/repos/[npub]/[repo]/services/file-operations.ts

@ -253,3 +253,102 @@ export async function deleteFile( @@ -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<void> {
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 <br>
highlight: function (str: string, lang: string): string {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value +
'</code></pre>';
} catch (err) {
// Fallback to escaped HTML if highlighting fails
}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
}
});
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;
}
}

174
src/routes/repos/[npub]/[repo]/services/patch-operations.ts

@ -199,3 +199,177 @@ export async function updatePatchStatus( @@ -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<void> {
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<any>;
comments?: Array<any>;
}>(`/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<void> {
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<void> {
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;
}
}

194
src/routes/repos/[npub]/[repo]/services/repo-operations.ts

@ -363,3 +363,197 @@ export async function checkVerification( @@ -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<void> {
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<void> {
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<string, { reachable: boolean; error?: string; checkedAt: number; serverType: 'git' | 'grasp' | 'unknown' }>();
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<void> {
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<void> {
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<string, string> = {};
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);
}
}

Loading…
Cancel
Save