diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl
index 2f5601d..d256f57 100644
--- a/nostr/commit-signatures.jsonl
+++ b/nostr/commit-signatures.jsonl
@@ -126,3 +126,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772298906,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"6aa4dcd1b3d8a933710a6eb43321aa4faaba56598c735a634069c882c83b4f03","sig":"80ce253e890e8e84c8138e004bc2aaea402379d9aa67f62793ac7a4b344de6a7223f46fc733b240215a983a3a9b574ea8d0858a184f06df58ee66212ba58ee53"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772299137,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","more-muted replyt-to"]],"content":"Signed commit: more-muted replyt-to","id":"fc0a91b526083b640d8116592fcac064fcf3cec9625b48dbd41c3877b2fe5444","sig":"998273d70d827ffbb939b4c149ff88e11c9f3aae3c5ddee78d860710f7fbff42c5ceed9433367b530bdc2869f9d382eb449537f813cf745c49f1a87a36926502"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772299733,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","make relay timeouts more efficient\nsuppress diff on initial commit"]],"content":"Signed commit: make relay timeouts more efficient\nsuppress diff on initial commit","id":"5fbc2dfb13acab011df5a394a022267e69bbe908e696c4389e0c07ba83d58a0d","sig":"daf46d563c413e2481be2cbd2b00d3015cf601e19fe0a191ffbb18c2c07508b17e34ebda5c903a1391914f991cecd7a7a4e809fcba45e1f14ebab674117eb53c"}
+{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772300935,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"3618c494d594408165ebf461e290676817ab6cc8b0b076ccc02b35a487ae8da1","sig":"d9106ce1318e703df1c366835be69c0cab12fba3a9be0fed944b17e55fd3b44f3fc9d45b32366d1d237452f099ab3b07f8ad6199660972ce571ec23ae264e873"}
diff --git a/src/app.html b/src/app.html
index 4f01d56..df1ae5c 100644
--- a/src/app.html
+++ b/src/app.html
@@ -39,6 +39,100 @@
}
// Other unhandled rejections will still be logged by the browser
});
+
+ // Suppress WebSocket connection errors for nostr relays
+ // These errors occur when relays are down or unreachable, which is expected behavior
+ // SimplePool from nostr-tools will retry connections, causing repeated console errors
+ (function() {
+ // List of known nostr relay URLs to suppress errors for
+ const nostrRelayPatterns = [
+ 'wss://orly-relay.imwald.eu',
+ 'wss://nostr.sovbit.host',
+ 'wss://nostr21.com',
+ 'wss://theforest.nostr1.com',
+ 'wss://nostr.land',
+ 'wss://relay.damus.io',
+ 'wss://thecitadel.nostr1.com',
+ 'wss://freelay.sovbit.host',
+ 'wss://bevos.nostr1.com',
+ 'wss://relay.primal.net',
+ 'wss://nostr.mom',
+ 'wss://relay.snort.social',
+ 'wss://aggr.nostr.land'
+ ];
+
+ // Helper function to check if an error message is a nostr relay connection error
+ function isNostrRelayConnectionError(message) {
+ if (typeof message !== 'string') return false;
+
+ return nostrRelayPatterns.some(pattern =>
+ message.includes(pattern) && (
+ message.includes('Verbindung') || // German: "connection"
+ message.includes('connection') ||
+ message.includes('kann keine Verbindung') || // German: "cannot establish connection"
+ message.includes('cannot establish') ||
+ message.includes('WebSocket') ||
+ message.includes('wss://') ||
+ message.includes('aufbauen') // German: "establish"
+ )
+ );
+ }
+
+ // Store original console methods
+ const originalConsoleError = console.error;
+ const originalConsoleWarn = console.warn;
+
+ // Override console.error to filter out WebSocket connection errors for nostr relays
+ console.error = function(...args) {
+ const message = args.join(' ');
+
+ // Suppress nostr relay connection errors (they're expected when relays are down)
+ if (isNostrRelayConnectionError(message)) {
+ // Optionally log at debug level instead of error level
+ if (typeof console.debug === 'function') {
+ console.debug('[Nostr Relay] Connection failed (expected):', ...args);
+ }
+ return; // Don't log the error
+ }
+
+ // For all other errors, use the original console.error
+ originalConsoleError.apply(console, args);
+ };
+
+ // Also override console.warn for consistency
+ console.warn = function(...args) {
+ const message = args.join(' ');
+
+ // Suppress nostr relay connection warnings too
+ if (isNostrRelayConnectionError(message)) {
+ if (typeof console.debug === 'function') {
+ console.debug('[Nostr Relay] Connection warning (expected):', ...args);
+ }
+ return;
+ }
+
+ originalConsoleWarn.apply(console, args);
+ };
+
+ // Handle window.onerror for global error catching
+ const originalOnError = window.onerror;
+ window.onerror = function(message, source, lineno, colno, error) {
+ const errorMessage = String(message || '');
+
+ // Suppress nostr relay connection errors
+ if (isNostrRelayConnectionError(errorMessage)) {
+ // Return true to prevent default error handling
+ return true;
+ }
+
+ // For other errors, call original handler if it exists
+ if (originalOnError) {
+ return originalOnError.call(this, message, source, lineno, colno, error);
+ }
+
+ return false;
+ };
+ })();
%sveltekit.head%
diff --git a/src/lib/components/CommentRenderer.svelte b/src/lib/components/CommentRenderer.svelte
index 4e43b09..9d58ab0 100644
--- a/src/lib/components/CommentRenderer.svelte
+++ b/src/lib/components/CommentRenderer.svelte
@@ -15,7 +15,7 @@
import EventCopyButton from '$lib/components/EventCopyButton.svelte';
import {
processContentWithNostrLinks,
- getReferencedEventFromDiscussion,
+ getReferencedEventWithTagType,
formatDiscussionTime,
type ProcessedContentPart
} from '$lib/utils/nostr-links.js';
@@ -42,9 +42,16 @@
nested = false
}: Props = $props();
- const referencedEvent = $derived(commentEvent
- ? getReferencedEventFromDiscussion(commentEvent, eventCache)
+ const referencedEventWithTag = $derived(commentEvent
+ ? getReferencedEventWithTagType(commentEvent, eventCache)
: undefined);
+
+ const referencedEvent = $derived(referencedEventWithTag?.event);
+ const referencedTagType = $derived(referencedEventWithTag?.tagType);
+
+ const referencedLabel = $derived(
+ referencedTagType === 'q' ? 'Quoting:' : 'Reply-To:'
+ );
const contentParts = $derived(processContentWithNostrLinks(comment.content, eventCache, profileCache));
@@ -68,7 +75,7 @@
{#if referencedEvent}
diff --git a/src/lib/utils/nostr-links.ts b/src/lib/utils/nostr-links.ts
index c394c39..664de58 100644
--- a/src/lib/utils/nostr-links.ts
+++ b/src/lib/utils/nostr-links.ts
@@ -381,11 +381,18 @@ export function getReferencedEventFromDiscussion(
event: NostrEvent,
eventCache: Map
): NostrEvent | undefined {
+ return getReferencedEventWithTagType(event, eventCache)?.event;
+}
+
+export function getReferencedEventWithTagType(
+ event: NostrEvent,
+ eventCache: Map
+): { event: NostrEvent; tagType: 'e' | 'a' | 'q' } | undefined {
// Check e-tag
const eTag = event.tags.find(t => t[0] === 'e' && t[1])?.[1];
if (eTag) {
const referenced = eventCache.get(eTag);
- if (referenced) return referenced;
+ if (referenced) return { event: referenced, tagType: 'e' };
}
// Check a-tag
@@ -396,18 +403,20 @@ export function getReferencedEventFromDiscussion(
const kind = parseInt(parts[0]);
const pubkey = parts[1];
const dTag = parts[2];
- return Array.from(eventCache.values()).find(e =>
+ const referenced = Array.from(eventCache.values()).find(e =>
e.kind === kind &&
e.pubkey === pubkey &&
e.tags.find(t => t[0] === 'd' && t[1] === dTag)
);
+ if (referenced) return { event: referenced, tagType: 'a' };
}
}
// Check q-tag
const qTag = event.tags.find(t => t[0] === 'q' && t[1])?.[1];
if (qTag) {
- return eventCache.get(qTag);
+ const referenced = eventCache.get(qTag);
+ if (referenced) return { event: referenced, tagType: 'q' };
}
return undefined;
diff --git a/src/routes/users/[npub]/+page.svelte b/src/routes/users/[npub]/+page.svelte
index 62e1040..51fd51f 100644
--- a/src/routes/users/[npub]/+page.svelte
+++ b/src/routes/users/[npub]/+page.svelte
@@ -15,6 +15,7 @@
import { combineRelays } from '$lib/config.js';
import { KIND, isEphemeralKind, isReplaceableKind } from '$lib/types/nostr.js';
import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
+ import { eventCache } from '$lib/services/nostr/event-cache.js';
const npub = ($page.params as { npub?: string }).npub || '';
@@ -60,6 +61,48 @@
return quotedEvents.find(e => e.id === eventId);
};
+ // Helper to get author name from pubkey
+ function getAuthorName(pubkey: string): string {
+ // Try to find profile event in nostrLinkEvents cache
+ // Check both by profile key and by iterating values
+ const profileByKey = nostrLinkEvents.get(`profile:${pubkey}`);
+ let profileEvent = profileByKey || Array.from(nostrLinkEvents.values()).find(
+ e => e.kind === 0 && e.pubkey === pubkey
+ );
+
+ // If not found in nostrLinkEvents, try global eventCache
+ if (!profileEvent) {
+ try {
+ const cachedProfile = eventCache.getProfile(pubkey);
+ if (cachedProfile) {
+ profileEvent = cachedProfile;
+ }
+ } catch {
+ // eventCache not available, continue
+ }
+ }
+
+ if (profileEvent) {
+ try {
+ const profile = JSON.parse(profileEvent.content);
+ const name = profile.display_name || profile.name;
+ if (name && name.trim()) return name.trim();
+ } catch {
+ // Try tags if JSON parse fails
+ const nameTag = profileEvent.tags.find(t => t[0] === 'name' || t[0] === 'display_name')?.[1];
+ if (nameTag && nameTag.trim()) return nameTag.trim();
+ }
+ }
+
+ // Fallback to shortened pubkey
+ try {
+ const npub = nip19.npubEncode(pubkey);
+ return npub.slice(0, 12) + '...';
+ } catch {
+ return pubkey.slice(0, 8) + '...';
+ }
+ }
+
// Referenced events cache for activity (a-tags and e-tags) - use array for better reactivity
let referencedEvents = $state([]);
@@ -163,6 +206,7 @@
}
// Fetch events
+ const loadedEvents: NostrEvent[] = [];
if (eventIds.length > 0) {
try {
const events = await Promise.race([
@@ -172,6 +216,7 @@
for (const event of events) {
nostrLinkEvents.set(event.id, event);
+ loadedEvents.push(event);
}
} catch {
// Ignore fetch errors
@@ -194,6 +239,7 @@
if (events.length > 0) {
nostrLinkEvents.set(events[0].id, events[0]);
+ loadedEvents.push(events[0]);
}
} catch {
// Ignore fetch errors
@@ -201,6 +247,38 @@
}
}
}
+
+ // Load profiles for authors of loaded events
+ if (loadedEvents.length > 0) {
+ const authorPubkeys = new Set();
+ for (const event of loadedEvents) {
+ if (event.pubkey) {
+ authorPubkeys.add(event.pubkey);
+ }
+ }
+
+ // Fetch profiles for all authors
+ if (authorPubkeys.size > 0) {
+ try {
+ const profiles = await Promise.race([
+ nostrClient.fetchEvents([{ kinds: [0], authors: Array.from(authorPubkeys), limit: authorPubkeys.size }]),
+ new Promise((resolve) => setTimeout(() => resolve([]), 10000))
+ ]);
+
+ // Store profiles in cache (use a special key format: `profile:${pubkey}`)
+ for (const profile of profiles) {
+ // Store with a key that includes the pubkey so we can find it
+ nostrLinkEvents.set(`profile:${profile.pubkey}`, profile);
+ // Also store by ID if it has one
+ if (profile.id) {
+ nostrLinkEvents.set(profile.id, profile);
+ }
+ }
+ } catch {
+ // Ignore profile fetch errors
+ }
+ }
+ }
}
// Get event from nostr: link
@@ -1925,7 +2003,8 @@ i *
{#if quotedEvent}
{quotedEvent.content || '(No content)'}
@@ -1984,7 +2063,7 @@ i *
{:else if part.type === 'event' && part.event}
{part.event.content || getEventContext(part.event)}
@@ -2039,13 +2118,22 @@ i *
{zapData.amount}
{#if zapData.senderPubkey}
-
From
+
+ From
+ {getAuthorName(zapData.senderPubkey)}
+
{/if}
{#if zapData.recipientPubkey && zapData.recipientPubkey !== profileOwnerPubkeyHex}
-
To
+
+ To
+ {getAuthorName(zapData.recipientPubkey)}
+
{/if}
{#if zapData.eventId}
-
on event {zapData.eventId.slice(0, 8)}...
+
+ on event
+ {zapData.eventId.slice(0, 8)}...
+
{/if}
{#if zapData.comment}
@@ -2078,7 +2166,7 @@ i *
{:else if part.type === 'event' && part.event}
{part.event.content || getEventContext(part.event)}
@@ -2153,7 +2241,7 @@ i *
{:else if part.type === 'event' && part.event}
{part.event.content || getEventContext(part.event)}
@@ -3038,17 +3126,33 @@ i *
.message-icon {
width: 1.25rem;
height: 1.25rem;
- filter: var(--icon-filter, none);
+ filter: var(--icon-filter, brightness(0) saturate(100%) invert(1));
+ opacity: 0.8;
+ }
+
+ :global([data-theme="light"]) .message-icon {
+ filter: brightness(0) saturate(100%);
+ opacity: 0.7;
+ }
+
+ :global([data-theme="dark"]) .message-icon,
+ :global([data-theme="black"]) .message-icon {
+ filter: brightness(0) saturate(100%) invert(1);
+ opacity: 0.8;
+ }
+
+ .message-action-button:hover .message-icon {
+ opacity: 1;
}
.quoted-event {
- margin-bottom: 1rem;
- padding: 0.75rem;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-left: 3px solid var(--accent, #007bff);
- border-radius: 0.375rem;
- font-size: 0.875rem;
+ margin-bottom: 0.75rem;
+ padding: 0.5rem;
+ background: var(--bg-secondary, var(--bg-primary));
+ color: var(--text-muted, var(--text-secondary));
+ border-radius: 4px;
+ border-left: 2px solid var(--border-light, var(--border-color));
+ opacity: 0.8;
}
:global([data-theme="light"]) .quoted-event {
@@ -3069,50 +3173,35 @@ i *
.quoted-event-header {
display: flex;
align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
- flex-wrap: wrap;
+ gap: 0.375rem;
+ margin-bottom: 0.25rem;
+ font-size: 0.75rem;
+ color: var(--text-muted, var(--text-secondary));
+ }
+
+ .quoted-event-label {
+ font-weight: 500;
+ color: var(--text-muted, var(--text-secondary));
}
.quoted-event-time {
- font-size: 0.75rem;
- color: var(--text-muted);
+ color: var(--text-muted, var(--text-secondary));
+ font-size: 0.7rem;
margin-left: auto;
}
.quoted-event-content {
- color: var(--text-secondary);
+ font-size: 0.8rem;
+ color: var(--text-muted, var(--text-secondary));
+ line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
- line-height: 1.5;
max-height: 8rem;
overflow: hidden;
position: relative;
}
- .quoted-event-content::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 2rem;
- background: linear-gradient(to bottom, transparent, var(--bg-secondary));
- pointer-events: none;
- }
-
- :global([data-theme="light"]) .quoted-event-content::after {
- background: linear-gradient(to bottom, transparent, #f5f5f5);
- }
-
- :global([data-theme="dark"]) .quoted-event-content::after {
- background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.05));
- }
-
- :global([data-theme="black"]) .quoted-event-content::after {
- background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.03));
- }
.quoted-event-loading {
opacity: 0.6;
@@ -3331,27 +3420,39 @@ i *
.zap-details {
display: flex;
- flex-direction: column;
- gap: 0.25rem;
- font-size: 0.875rem;
- color: var(--text-primary);
- line-height: 1.5;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ font-size: 0.7rem;
+ color: var(--text-muted, var(--text-secondary));
+ line-height: 1.4;
+ margin-top: 0.25rem;
}
- .zap-details span {
- display: flex;
+ .zap-detail-item {
+ display: inline-flex;
align-items: center;
- gap: 0.5rem;
+ gap: 0.25rem;
+ }
+
+ .zap-detail-label {
+ color: var(--text-muted, var(--text-secondary));
+ font-weight: 500;
+ }
+
+ .zap-detail-value {
+ color: var(--text-muted, var(--text-secondary));
}
.zap-comment {
- margin-top: 0.5rem;
- padding: 0.75rem;
- background: var(--bg-secondary);
- border-radius: 0.5rem;
- border-left: 2px solid var(--accent);
+ margin-top: 0.375rem;
+ padding: 0.375rem 0.5rem;
+ background: var(--bg-secondary, var(--bg-primary));
+ border-radius: 4px;
+ border-left: 2px solid var(--border-light, var(--border-color));
font-style: italic;
- color: var(--text-secondary);
+ font-size: 0.75rem;
+ color: var(--text-muted, var(--text-secondary));
+ opacity: 0.8;
}
.zap-receipt {
@@ -3509,11 +3610,20 @@ i *
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
+ line-height: 1;
+ font-size: 0.75rem;
+ color: var(--text-muted, var(--text-secondary));
+ }
+
+ .nostr-link-event-author {
+ font-weight: 500;
+ color: var(--text-muted, var(--text-secondary));
}
.nostr-link-event-time {
- font-size: 0.75rem;
- color: var(--text-muted);
+ font-size: 0.7rem;
+ color: var(--text-muted, var(--text-secondary));
+ margin-left: auto;
}
.nostr-link-event-content {