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 @@
{"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":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":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":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 @@
import { import {
loadPatches as loadPatchesService, loadPatches as loadPatchesService,
createPatch as createPatchService, createPatch as createPatchService,
updatePatchStatus as updatePatchStatusService updatePatchStatus as updatePatchStatusService,
loadPatchHighlights as loadPatchHighlightsService,
createPatchHighlight as createPatchHighlightService,
createPatchComment as createPatchCommentService
} from './services/patch-operations.js'; } from './services/patch-operations.js';
import {
performCodeSearch as performCodeSearchService
} from './services/code-search-operations.js';
import { import {
loadDiscussions as loadDiscussionsService, loadDiscussions as loadDiscussionsService,
createDiscussionThread as createDiscussionThreadService, createDiscussionThread as createDiscussionThreadService,
@ -127,8 +133,15 @@
toggleBookmark as toggleBookmarkService, toggleBookmark as toggleBookmarkService,
checkMaintainerStatus as checkMaintainerStatusService, checkMaintainerStatus as checkMaintainerStatusService,
loadAllMaintainers as loadAllMaintainersService, 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'; } from './services/repo-operations.js';
import {
loadReadme as loadReadmeService
} from './services/file-operations.js';
// Consolidated state - all state variables in one object // Consolidated state - all state variables in one object
let state = $state(createRepoState()); let state = $state(createRepoState());
@ -772,136 +785,11 @@
// Load clone URL reachability status // Load clone URL reachability status
async function loadCloneUrlReachability(forceRefresh: boolean = false) { async function loadCloneUrlReachability(forceRefresh: boolean = false) {
if (!repoCloneUrls || repoCloneUrls.length === 0) { await loadCloneUrlReachabilityService(forceRefresh, state, repoCloneUrls);
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();
}
} }
async function loadReadme() { async function loadReadme() {
if (state.loading.repoNotFound) return; await loadReadmeService(state, rewriteImagePaths);
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;
}
} }
// File processing utilities are now imported from utils/file-processing.ts // File processing utilities are now imported from utils/file-processing.ts
@ -1011,92 +899,7 @@
} }
async function loadRepoImages() { async function loadRepoImages() {
try { await loadRepoImagesService(state, repoOwnerPubkeyDerived, repoIsPrivate, $page.data);
// 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);
}
} }
// Reactively update images when pageData changes (only once, when data becomes available) // Reactively update images when pageData changes (only once, when data becomes available)
@ -1373,13 +1176,7 @@
async function loadBookmarkStatus() { async function loadBookmarkStatus() {
if (!state.user.pubkey || !state.metadata.address || !bookmarksService) return; await loadBookmarkStatusService(state, bookmarksService);
try {
state.bookmark.isBookmarked = await bookmarksService.isBookmarked(state.user.pubkey, state.metadata.address);
} catch (err) {
console.warn('Failed to load bookmark status:', err);
}
} }
async function toggleBookmark() { async function toggleBookmark() {
@ -2425,50 +2222,7 @@
} }
async function performCodeSearch() { async function performCodeSearch() {
if (!state.codeSearch.query.trim() || state.codeSearch.query.length < 2) { await performCodeSearchService(state);
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;
}
} }
async function loadIssues() { async function loadIssues() {
@ -2524,29 +2278,7 @@
} }
async function loadPatchHighlights(patchId: string, patchAuthor: string) { async function loadPatchHighlights(patchId: string, patchAuthor: string) {
if (!patchId || !patchAuthor) return; await loadPatchHighlightsService(patchId, patchAuthor, state);
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;
}
} }
function handlePatchCodeSelection( function handlePatchCodeSelection(
@ -2567,65 +2299,7 @@
} }
async function createPatchHighlight() { async function createPatchHighlight() {
if (!state.user.pubkey || !state.forms.patchHighlight.text.trim() || !state.selected.patch) return; await createPatchHighlightService(state, highlightsService, { loadPatches });
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;
}
} }
function formatPubkey(pubkey: string): string { function formatPubkey(pubkey: string): string {
@ -2646,82 +2320,7 @@
} }
async function createPatchComment() { async function createPatchComment() {
if (!state.user.pubkey || !state.forms.patchComment.content.trim() || !state.selected.patch) return; await createPatchCommentService(state, highlightsService, { loadPatches });
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;
}
} }
// Initialize patch highlights effect // Initialize patch highlights effect

44
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<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(
state.saving = false; 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(
state.statusUpdates.patch[patchId] = false; 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(
console.log('[Verification] Status after check:', state.verification.status); 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