diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl
index 845a6e3..357e15a 100644
--- a/nostr/commit-signatures.jsonl
+++ b/nostr/commit-signatures.jsonl
@@ -103,3 +103,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772112920,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 7"]],"content":"Signed commit: refactor 7","id":"80f54ac61390cfbc8f2496a162d7065c447033a2e085ab5886c8138e337e93f9","sig":"f64bd2c965eff3534ad68e245651c189dc925d9613dd85557d88af8c692361ade13ccdd9deb88ae07eb227aa002af99d525a7fdf6f29eca854b5a02882ef226f"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772130529,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 8"]],"content":"Signed commit: refactor 8","id":"716cfe7b5d8b788e6e24092a6ad7e92de0b3d383c43a343f3c5bec4d2bbdd4b9","sig":"e80ed3d9d471bd6907e212edfd7cf3f6039fa80e4434c35f0591729515eaa98c7a8ac54f2ac6f7a2fefb7846de0e2f0a120543a0dbe862c47c7710a653189b0c"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772131858,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 9"]],"content":"Signed commit: refactor 9","id":"0d92496bc69fe5a2005be0eba26653a729d358f9d9f227e1af01c330eb0c4387","sig":"9203dcc9cfb44804957d37d3b47079ac324bffb42e445a3a79130622e5e20fd10513d987f48edf195514ebf6cb136ba6c5992b39de21b8b47a086194e22cbaeb"}
+{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772136696,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 10"]],"content":"Signed commit: refactor 10","id":"7fb8d54e26ab59486f3b56d97e225ed02f893140025c03ccb95a991e523e6182","sig":"f4bb5a037c48d06854d9346ebf96aa9f65f11d3f96e23d08b7d38d0ebea9bab242ffa917239aa432d83a55f369586d66603f439f40eac8156aeaaf80737b81a1"}
diff --git a/src/lib/components/NostrHtmlRenderer.svelte b/src/lib/components/NostrHtmlRenderer.svelte
new file mode 100644
index 0000000..7db12c1
--- /dev/null
+++ b/src/lib/components/NostrHtmlRenderer.svelte
@@ -0,0 +1,203 @@
+
+
+
+ {#if loading}
+
Loading nostr links...
+ {:else}
+ {#each htmlParts as part}
+ {#if part.type === 'html'}
+ {@html part.content}
+ {:else if part.type === 'profile' && part.pubkey}
+
+ {:else if part.type === 'event' && part.event}
+
+
+
+ {part.event.content || '(No content)'}
+
+
+ {:else}
+
{part.content}
+ {/if}
+ {/each}
+ {/if}
+
+
+
diff --git a/src/lib/utils/nostr-links.ts b/src/lib/utils/nostr-links.ts
index 7d35889..880fd8c 100644
--- a/src/lib/utils/nostr-links.ts
+++ b/src/lib/utils/nostr-links.ts
@@ -1,6 +1,9 @@
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '$lib/types/nostr.js';
import { NostrClient } from '$lib/services/nostr/nostr-client.js';
+import { DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.js';
+import { getUserRelays } from '$lib/services/nostr/user-relays.js';
+import { KIND } from '$lib/types/nostr.js';
export interface ParsedNostrLink {
type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'nprofile';
@@ -21,6 +24,8 @@ export interface ProcessedContentPart {
*/
export function parseNostrLinks(content: string): ParsedNostrLink[] {
const links: ParsedNostrLink[] = [];
+ // Match nostr: links - be more permissive with characters to handle HTML entities
+ // Note: bech32 uses base32 which is a-z, A-Z, 2-7, but we'll be more permissive
const nostrLinkRegex = /nostr:(nevent1|naddr1|note1|npub1|nprofile1)[a-zA-Z0-9]+/g;
let match;
@@ -44,6 +49,10 @@ export function parseNostrLinks(content: string): ParsedNostrLink[] {
});
}
+ if (links.length > 0) {
+ console.log('[parseNostrLinks] Found', links.length, 'nostr links:', links.map(l => l.value));
+ }
+
return links;
}
@@ -60,7 +69,10 @@ export async function loadNostrLinks(
if (links.length === 0) return;
const eventIds: string[] = [];
+ const eventIdToRelays = new Map(); // Map event ID to relay hints
+ const eventIdToPubkey = new Map(); // Map event ID to author pubkey (from nevent)
const aTags: string[] = [];
+ const aTagToRelays = new Map(); // Map a-tag to relay hints
const npubs: string[] = [];
for (const link of links) {
@@ -68,15 +80,29 @@ export async function loadNostrLinks(
if (link.type === 'nevent' || link.type === 'note1') {
const decoded = nip19.decode(link.value.replace('nostr:', ''));
if (decoded.type === 'nevent') {
- eventIds.push(decoded.data.id);
+ const data = decoded.data as { id: string; pubkey?: string; relays?: string[] };
+ eventIds.push(data.id);
+ // Store relay hints if available
+ if (data.relays && data.relays.length > 0) {
+ eventIdToRelays.set(data.id, data.relays);
+ }
+ // Store author pubkey if available (for fetching their relay list)
+ if (data.pubkey) {
+ eventIdToPubkey.set(data.id, data.pubkey);
+ }
} else if (decoded.type === 'note') {
eventIds.push(decoded.data as string);
}
} else if (link.type === 'naddr') {
const decoded = nip19.decode(link.value.replace('nostr:', ''));
if (decoded.type === 'naddr') {
- const aTag = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`;
+ const data = decoded.data as { kind: number; pubkey: string; identifier: string; relays?: string[] };
+ const aTag = `${data.kind}:${data.pubkey}:${data.identifier}`;
aTags.push(aTag);
+ // Store relay hints if available
+ if (data.relays && data.relays.length > 0) {
+ aTagToRelays.set(aTag, data.relays);
+ }
}
} else if (link.type === 'npub' || link.type === 'nprofile') {
const decoded = nip19.decode(link.value.replace('nostr:', ''));
@@ -93,19 +119,104 @@ export async function loadNostrLinks(
}
}
- // Fetch events
+ // Collect all unique relay hints
+ const relayHints = new Set();
+ for (const relays of eventIdToRelays.values()) {
+ relays.forEach(r => relayHints.add(r));
+ }
+ for (const relays of aTagToRelays.values()) {
+ relays.forEach(r => relayHints.add(r));
+ }
+
+ // Collect unique author pubkeys from nevent links
+ const authorPubkeys = new Set();
+ for (const pubkey of eventIdToPubkey.values()) {
+ authorPubkeys.add(pubkey);
+ }
+
+ // Fetch kind 10002 relay lists for all authors
+ const authorRelays = new Set();
+ if (authorPubkeys.size > 0) {
+ console.log('[loadNostrLinks] Fetching relay lists for', authorPubkeys.size, 'authors');
+ // Use a temporary client with search relays to fetch relay lists
+ const searchClient = new NostrClient(DEFAULT_NOSTR_SEARCH_RELAYS);
+ for (const pubkey of authorPubkeys) {
+ try {
+ const userRelays = await getUserRelays(pubkey, searchClient);
+ // Add both inbox and outbox relays (author might have published to either)
+ userRelays.inbox.forEach(r => authorRelays.add(r));
+ userRelays.outbox.forEach(r => authorRelays.add(r));
+ console.log('[loadNostrLinks] Found', userRelays.inbox.length + userRelays.outbox.length, 'relays for author', pubkey.slice(0, 8) + '...');
+ } catch (err) {
+ console.warn('[loadNostrLinks] Error fetching relay list for author', pubkey.slice(0, 8) + '...', err);
+ }
+ }
+ searchClient.close();
+ }
+
+ // Combine ALL relays: hints, author relays, search relays, and default relays
+ const allRelays = new Set();
+ relayHints.forEach(r => allRelays.add(r));
+ authorRelays.forEach(r => allRelays.add(r));
+ DEFAULT_NOSTR_SEARCH_RELAYS.forEach(r => allRelays.add(r));
+ // Also include default relays from the passed client
+ // Note: We can't access the client's relays directly, but we'll use the client itself as fallback
+
+ // Fetch events - try with ALL combined relays first
if (eventIds.length > 0) {
try {
- const events = await Promise.race([
- nostrClient.fetchEvents([{ ids: eventIds, limit: eventIds.length }]),
- new Promise((resolve) => setTimeout(() => resolve([]), 10000))
- ]);
+ console.log('[loadNostrLinks] Fetching events:', eventIds);
+ console.log('[loadNostrLinks] Relay hints:', Array.from(relayHints).length);
+ console.log('[loadNostrLinks] Author relays:', Array.from(authorRelays).length);
+ console.log('[loadNostrLinks] Total unique relays:', allRelays.size);
+
+ let events: NostrEvent[] = [];
+ const foundIds = new Set();
+
+ // Try fetching from ALL combined relays (hints + author relays + search relays)
+ const combinedRelays = Array.from(allRelays);
+ if (combinedRelays.length > 0) {
+ const combinedClient = new NostrClient(combinedRelays);
+ try {
+ const fetched = await Promise.race([
+ combinedClient.fetchEvents([{ ids: eventIds, limit: eventIds.length }]),
+ new Promise((resolve) => setTimeout(() => resolve([]), 20000))
+ ]);
+ combinedClient.close();
+ events.push(...fetched);
+ fetched.forEach(e => foundIds.add(e.id));
+ console.log('[loadNostrLinks] Fetched', fetched.length, 'events from combined relays');
+ } catch (err) {
+ console.warn('[loadNostrLinks] Error fetching from combined relays:', err);
+ combinedClient.close();
+ }
+ }
+
+ // If we didn't get all events, try default client as final fallback
+ const missingIds = eventIds.filter(id => !foundIds.has(id));
+ if (missingIds.length > 0) {
+ console.log('[loadNostrLinks] Fetching', missingIds.length, 'missing events from default client');
+ try {
+ const defaultEvents = await Promise.race([
+ nostrClient.fetchEvents([{ ids: missingIds, limit: missingIds.length }]),
+ new Promise((resolve) => setTimeout(() => resolve([]), 15000))
+ ]);
+ events.push(...defaultEvents);
+ defaultEvents.forEach(e => foundIds.add(e.id));
+ console.log('[loadNostrLinks] Fetched', defaultEvents.length, 'additional events from default client');
+ } catch (err) {
+ console.warn('[loadNostrLinks] Error fetching from default client:', err);
+ }
+ }
+
+ console.log('[loadNostrLinks] Total fetched:', events.length, 'events out of', eventIds.length, 'requested');
for (const event of events) {
eventCache.set(event.id, event);
+ console.log('[loadNostrLinks] Stored event:', event.id);
}
- } catch {
- // Ignore fetch errors
+ } catch (err) {
+ console.error('[loadNostrLinks] Error fetching events:', err);
}
}
@@ -118,10 +229,31 @@ export async function loadNostrLinks(
const kind = parseInt(parts[0]);
const pubkey = parts[1];
const dTag = parts[2];
- const events = await Promise.race([
- nostrClient.fetchEvents([{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }]),
- new Promise((resolve) => setTimeout(() => resolve([]), 10000))
- ]);
+
+ let events: NostrEvent[] = [];
+
+ // Try relay hints first if available
+ const hintRelays = aTagToRelays.get(aTag);
+ if (hintRelays && hintRelays.length > 0) {
+ const hintClient = new NostrClient(hintRelays);
+ try {
+ events = await Promise.race([
+ hintClient.fetchEvents([{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }]),
+ new Promise((resolve) => setTimeout(() => resolve([]), 10000))
+ ]);
+ hintClient.close();
+ } catch {
+ hintClient.close();
+ }
+ }
+
+ // Fallback to default relays if no events found
+ if (events.length === 0) {
+ events = await Promise.race([
+ nostrClient.fetchEvents([{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }]),
+ new Promise((resolve) => setTimeout(() => resolve([]), 10000))
+ ]);
+ }
if (events.length > 0) {
eventCache.set(events[0].id, events[0]);
@@ -145,23 +277,39 @@ export function getEventFromNostrLink(
if (link.startsWith('nostr:nevent1') || link.startsWith('nostr:note1')) {
const decoded = nip19.decode(link.replace('nostr:', ''));
if (decoded.type === 'nevent') {
- return eventCache.get(decoded.data.id);
+ const data = decoded.data as { id: string; relays?: string[] };
+ const eventId = data.id;
+ const event = eventCache.get(eventId);
+ if (!event) {
+ console.log('[getEventFromNostrLink] Event not found in cache:', eventId, 'Cache has:', Array.from(eventCache.keys()));
+ }
+ return event;
} else if (decoded.type === 'note') {
- return eventCache.get(decoded.data as string);
+ const eventId = decoded.data as string;
+ const event = eventCache.get(eventId);
+ if (!event) {
+ console.log('[getEventFromNostrLink] Note event not found in cache:', eventId);
+ }
+ return event;
}
} else if (link.startsWith('nostr:naddr1')) {
const decoded = nip19.decode(link.replace('nostr:', ''));
if (decoded.type === 'naddr') {
- return Array.from(eventCache.values()).find(e => {
+ const data = decoded.data as { kind: number; pubkey: string; identifier: string; relays?: string[] };
+ const event = Array.from(eventCache.values()).find(e => {
const dTag = e.tags.find(t => t[0] === 'd')?.[1];
- return e.kind === decoded.data.kind &&
- e.pubkey === decoded.data.pubkey &&
- dTag === decoded.data.identifier;
+ return e.kind === data.kind &&
+ e.pubkey === data.pubkey &&
+ dTag === data.identifier;
});
+ if (!event) {
+ console.log('[getEventFromNostrLink] Naddr event not found in cache:', data);
+ }
+ return event;
}
}
- } catch {
- // Invalid link
+ } catch (err) {
+ console.error('[getEventFromNostrLink] Error decoding link:', link, err);
}
return undefined;
}
@@ -279,3 +427,54 @@ export function formatDiscussionTime(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleDateString();
}
+
+/**
+ * Process HTML content with nostr links into parts for rendering
+ * Similar to processContentWithNostrLinks but handles HTML properly
+ */
+export function processHtmlWithNostrLinks(
+ html: string,
+ eventCache: Map,
+ profileCache: Map
+): Array<{ type: 'html' | 'event' | 'profile' | 'placeholder'; content: string; event?: NostrEvent; pubkey?: string }> {
+ const links = parseNostrLinks(html);
+ if (links.length === 0) {
+ return [{ type: 'html', content: html }];
+ }
+
+ const parts: Array<{ type: 'html' | 'event' | 'profile' | 'placeholder'; content: string; event?: NostrEvent; pubkey?: string }> = [];
+ let lastIndex = 0;
+
+ for (const link of links) {
+ // Add HTML before link
+ if (link.start > lastIndex) {
+ const htmlPart = html.slice(lastIndex, link.start);
+ if (htmlPart) {
+ parts.push({ type: 'html', content: htmlPart });
+ }
+ }
+
+ // Add link
+ const event = getEventFromNostrLink(link.value, eventCache);
+ const pubkey = getPubkeyFromNostrLink(link.value, profileCache);
+ if (event) {
+ parts.push({ type: 'event', content: link.value, event });
+ } else if (pubkey) {
+ parts.push({ type: 'profile', content: link.value, pubkey });
+ } else {
+ parts.push({ type: 'placeholder', content: link.value });
+ }
+
+ lastIndex = link.end;
+ }
+
+ // Add remaining HTML
+ if (lastIndex < html.length) {
+ const htmlPart = html.slice(lastIndex);
+ if (htmlPart) {
+ parts.push({ type: 'html', content: htmlPart });
+ }
+ }
+
+ return parts;
+}
diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte
index 65a868f..cc4eed3 100644
--- a/src/routes/repos/[npub]/[repo]/+page.svelte
+++ b/src/routes/repos/[npub]/[repo]/+page.svelte
@@ -651,8 +651,13 @@
// README
// Rewrite image paths in HTML to point to repository file API
+ // Uses the same pattern as DocsViewer which works correctly
function rewriteImagePaths(html: string, filePath: string | null): string {
- if (!html || !filePath) return html;
+ if (!html || !filePath) return html || '';
+ if (typeof html !== 'string') {
+ console.error('[rewriteImagePaths] Invalid html parameter:', typeof html, html);
+ return '';
+ }
// Get the directory of the current file
const fileDir = filePath.includes('/')
@@ -663,17 +668,24 @@
// If repo is empty (no branches), use null and let API handle it
const branch = state.git.currentBranch || state.git.defaultBranch || null;
- // Rewrite relative image paths
- return html.replace(/
]*)\ssrc=["']([^"']+)["']([^>]*)>/gi, (match, before, src, after) => {
- // Skip if it's already an absolute URL (http/https/data)
+ // Rewrite relative image paths - handle various img tag formats
+ // Match:
,
,
,
, etc.
+ // Pattern:
]*?)?\s+src\s*=\s*["']([^"']+)["']([^>]*)>/gi;
+ let matchCount = 0;
+ const result = html.replace(imgTagPattern, (match, beforeAttrs, src, afterAttrs) => {
+ matchCount++;
+ console.log('[rewriteImagePaths] Matched img tag:', match.substring(0, 100), 'src:', src);
+ // Skip if it's already an absolute URL (http/https/data) or already an API URL
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:') || src.startsWith('/api/')) {
+ console.log('[rewriteImagePaths] Skipping absolute URL:', src);
return match;
}
// Resolve relative path
let imagePath: string;
if (src.startsWith('/')) {
- // Absolute path from repo root
+ // Absolute path from repo root (remove leading slash)
imagePath = src.substring(1);
} else if (src.startsWith('./')) {
// Relative to current file directory
@@ -700,8 +712,24 @@
const ref = branch || 'HEAD';
const apiUrl = `/api/repos/${state.npub}/${state.repo}/raw?path=${encodeURIComponent(imagePath)}&ref=${encodeURIComponent(ref)}`;
- return `
`;
+ console.log('[rewriteImagePaths] Rewriting:', src, '->', apiUrl);
+ // Reconstruct the img tag with the new src
+ // beforeAttrs might be undefined (if no attributes before src) or contain other attributes
+ const before = beforeAttrs ? beforeAttrs.trim() : '';
+ return `
`;
});
+ if (matchCount === 0) {
+ console.warn('[rewriteImagePaths] No img tags matched in HTML. HTML sample:', html.substring(0, 500));
+ // Try alternative pattern in case the first one didn't match
+ const altPattern = /
]+)>/gi;
+ const altMatches = html.match(altPattern);
+ if (altMatches) {
+ console.log('[rewriteImagePaths] Found img tags with alternative pattern:', altMatches);
+ }
+ } else {
+ console.log('[rewriteImagePaths] Processed', matchCount, 'image tag(s)');
+ }
+ return result;
}
// Fork
@@ -812,9 +840,18 @@
// Render markdown, asciidoc, or HTML files as HTML
async function renderFileAsHtml(content: string, ext: string) {
- await renderFileAsHtmlUtil(content, ext, state.files.currentFile, (html: string) => {
- state.preview.file.html = html;
- });
+ const branch = state.git.currentBranch || state.git.defaultBranch || null;
+ await renderFileAsHtmlUtil(
+ content,
+ ext,
+ state.files.currentFile,
+ (html: string) => {
+ state.preview.file.html = html;
+ },
+ state.npub,
+ state.repo,
+ branch
+ );
}
// CSV and HTML utilities are now imported from utils/file-processing.ts
@@ -2075,8 +2112,9 @@
{/if}
-
-
+
+ {#if repoOwnerPubkeyDerived}
+
{#if state.ui.activeTab === 'files'}
-
+
+ {/if}
diff --git a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte
index 4f4273a..2541568 100644
--- a/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/DocsViewer.svelte
@@ -10,6 +10,7 @@
import { KIND } from '$lib/types/nostr.js';
import logger from '$lib/services/logger.js';
import { renderContent } from '../utils/content-renderer.js';
+ import NostrHtmlRenderer from '$lib/components/NostrHtmlRenderer.svelte';
// Rewrite image paths in HTML to point to repository file API
function rewriteImagePaths(html: string, filePath: string, npub: string, repo: string, branch: string): string {
@@ -154,7 +155,7 @@
/>
{:else if renderedContent}
- {@html renderedContent}
+
{:else}
No content to display
diff --git a/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte b/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte
index 54dfb92..a97210b 100644
--- a/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/FileBrowser.svelte
@@ -89,6 +89,7 @@
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
+ color: var(--text-primary);
}
.nav-back:hover {
diff --git a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte
index 7b9c298..7438344 100644
--- a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte
+++ b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte
@@ -1,12 +1,13 @@