diff --git a/README.md b/README.md
index 1438b73..7b89069 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ To run the container, in detached mode (-d):
docker run -d --rm --name=gc-alexandria -p 4174:80 gc-alexandria
```
-The container is then viewable on your [local machine](http://localhost:4174).
+The container is then viewable on your [local machine](http://localhost:4173).
If you want to see the container process (assuming it's the last process to start), enter:
diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte
index a82f188..2f28ccc 100644
--- a/src/lib/components/PublicationHeader.svelte
+++ b/src/lib/components/PublicationHeader.svelte
@@ -29,6 +29,7 @@
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null);
let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null);
+ console.log("PublicationHeader event:", event);
{#if title != null && href != null}
diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte
index bbc2b73..f7cc569 100644
--- a/src/lib/components/util/CardActions.svelte
+++ b/src/lib/components/util/CardActions.svelte
@@ -1,17 +1,21 @@
@@ -142,22 +133,18 @@
-
- {#if shareLinkCopied}
- Copied!
- {:else}
- Share via NJump
- {/if}
-
+
-
- {#if eventIdCopied}
- Copied!
- {:else}
- Copy event ID
- {/if}
-
+
@@ -214,7 +201,7 @@
Identifier: {identifier}
{/if}
View Event Details
diff --git a/src/lib/components/util/CopyToClipboard.svelte b/src/lib/components/util/CopyToClipboard.svelte
index d0bbba3..cb49a6b 100644
--- a/src/lib/components/util/CopyToClipboard.svelte
+++ b/src/lib/components/util/CopyToClipboard.svelte
@@ -1,27 +1,41 @@
-
+
{#if copied}
- Copied!
+ Copied!
{:else}
- {displayText}
+ {#if icon}
+
+ {/if}
+ {displayText}
{/if}
diff --git a/src/lib/parser.ts b/src/lib/parser.ts
index d877985..85286d4 100644
--- a/src/lib/parser.ts
+++ b/src/lib/parser.ts
@@ -152,6 +152,10 @@ export default class Pharos {
}
parse(content: string, options?: ProcessorOptions | undefined): void {
+
+ // Ensure the content is valid AsciiDoc and has a header and the doctype book
+ content = ensureAsciiDocHeader(content);
+
try {
this.html = this.asciidoctor.convert(content, {
'extension_registry': this.pharosExtensions,
@@ -1119,3 +1123,44 @@ export const tocUpdate = writable(0);
// Whenever you update the publication tree, call:
tocUpdate.update(n => n + 1);
+
+function ensureAsciiDocHeader(content: string): string {
+ const lines = content.split(/\r?\n/);
+ let headerIndex = -1;
+ let hasDoctype = false;
+
+ // Find the first non-empty line as header
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].trim() === '') continue;
+ if (lines[i].trim().startsWith('=')) {
+ headerIndex = i;
+ console.debug('[Pharos] AsciiDoc document header:', lines[i].trim());
+ break;
+ } else {
+ throw new Error('AsciiDoc document is missing a header at the top.');
+ }
+ }
+
+ if (headerIndex === -1) {
+ throw new Error('AsciiDoc document is missing a header.');
+ }
+
+ // Check for doctype in the next non-empty line after header
+ let nextLine = headerIndex + 1;
+ while (nextLine < lines.length && lines[nextLine].trim() === '') {
+ nextLine++;
+ }
+ if (nextLine < lines.length && lines[nextLine].trim().startsWith(':doctype:')) {
+ hasDoctype = true;
+ }
+
+ // Insert doctype immediately after header if not present
+ if (!hasDoctype) {
+ lines.splice(headerIndex + 1, 0, ':doctype: book');
+ }
+
+ // Log the state of the lines before returning
+ console.debug('[Pharos] AsciiDoc lines after header/doctype normalization:', lines.slice(0, 5));
+
+ return lines.join('\n');
+}
diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts
index 123a19a..6cd5df3 100644
--- a/src/lib/utils/nostrUtils.ts
+++ b/src/lib/utils/nostrUtils.ts
@@ -185,6 +185,55 @@ export async function getNpubFromNip05(nip05: string): Promise {
}
}
+/**
+ * Generic utility function to add a timeout to any promise
+ * Can be used in two ways:
+ * 1. Method style: promise.withTimeout(5000)
+ * 2. Function style: withTimeout(promise, 5000)
+ *
+ * @param thisOrPromise Either the promise to timeout (function style) or the 'this' context (method style)
+ * @param timeoutMsOrPromise Timeout duration in milliseconds (function style) or the promise (method style)
+ * @returns The promise result if completed before timeout, otherwise throws an error
+ * @throws Error with message 'Timeout' if the promise doesn't resolve within timeoutMs
+ */
+export function withTimeout(
+ thisOrPromise: Promise | number,
+ timeoutMsOrPromise?: number | Promise
+): Promise {
+ // Handle method-style call (promise.withTimeout(5000))
+ if (typeof thisOrPromise === 'number') {
+ const timeoutMs = thisOrPromise;
+ const promise = timeoutMsOrPromise as Promise;
+ return Promise.race([
+ promise,
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Timeout')), timeoutMs)
+ )
+ ]);
+ }
+
+ // Handle function-style call (withTimeout(promise, 5000))
+ const promise = thisOrPromise;
+ const timeoutMs = timeoutMsOrPromise as number;
+ return Promise.race([
+ promise,
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Timeout')), timeoutMs)
+ )
+ ]);
+}
+
+// Add the method to Promise prototype
+declare global {
+ interface Promise {
+ withTimeout(timeoutMs: number): Promise;
+ }
+}
+
+Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number): Promise {
+ return withTimeout(timeoutMs, this);
+};
+
/**
* Fetches an event using a two-step relay strategy:
* 1. First tries standard relays with timeout
@@ -196,42 +245,59 @@ export async function fetchEventWithFallback(
filterOrId: string | NDKFilter,
timeoutMs: number = 3000
): Promise {
- const allRelays = Array.from(new Set([...standardRelays, ...bootstrapRelays]));
+ // Get user relays if logged in
+ const userRelays = ndk.activeUser ?
+ Array.from(ndk.pool?.relays.values() || [])
+ .filter(r => r.status === 1) // Only use connected relays
+ .map(r => r.url) :
+ [];
+
+ // Create three relay sets in priority order
const relaySets = [
- NDKRelaySet.fromRelayUrls(standardRelays, ndk),
- NDKRelaySet.fromRelayUrls(allRelays, ndk)
+ NDKRelaySet.fromRelayUrls(standardRelays, ndk), // 1. Standard relays
+ NDKRelaySet.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in)
+ NDKRelaySet.fromRelayUrls(bootstrapRelays, ndk) // 3. Bootstrap relays (last resort)
];
- async function withTimeout(promise: Promise): Promise {
- return Promise.race([
- promise,
- new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs))
- ]);
- }
-
try {
let found: NDKEvent | null = null;
+ const triedRelaySets: string[] = [];
- // Try standard relays first
- if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) {
- found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[0]));
- if (!found) {
- // Fallback to all relays
- found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[1]));
- }
- } else {
- const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId;
- const results = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[0]));
- found = results instanceof Set ? Array.from(results)[0] as NDKEvent : null;
- if (!found) {
- // Fallback to all relays
- const fallbackResults = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[1]));
- found = fallbackResults instanceof Set ? Array.from(fallbackResults)[0] as NDKEvent : null;
+ // Helper function to try fetching from a relay set
+ async function tryFetchFromRelaySet(relaySet: NDKRelaySet, setName: string): Promise {
+ if (relaySet.relays.size === 0) return null;
+ triedRelaySets.push(setName);
+
+ if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) {
+ return await ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySet).withTimeout(timeoutMs);
+ } else {
+ const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId;
+ const results = await ndk.fetchEvents(filter, undefined, relaySet).withTimeout(timeoutMs);
+ return results instanceof Set ? Array.from(results)[0] as NDKEvent : null;
}
}
+ // Try each relay set in order
+ for (const [index, relaySet] of relaySets.entries()) {
+ const setName = index === 0 ? 'standard relays' :
+ index === 1 ? 'user relays' :
+ 'bootstrap relays';
+
+ found = await tryFetchFromRelaySet(relaySet, setName);
+ if (found) break;
+ }
+
if (!found) {
- console.warn('Event not found after timeout. Some relays may be offline or slow.');
+ const timeoutSeconds = timeoutMs / 1000;
+ const relayUrls = relaySets.map((set, i) => {
+ const setName = i === 0 ? 'standard relays' :
+ i === 1 ? 'user relays' :
+ 'bootstrap relays';
+ const urls = Array.from(set.relays).map(r => r.url);
+ return urls.length > 0 ? `${setName} (${urls.join(', ')})` : null;
+ }).filter(Boolean).join(', then ');
+
+ console.warn(`Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`);
return null;
}