From 6222ba5305da54015ff8fe010b0ba7b61bdcbbf4 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 1 Apr 2026 09:51:16 +0200 Subject: [PATCH] bug-fixes --- src/PageManager.tsx | 49 +++++++++++--- src/components/ClientSelect/index.tsx | 17 ++++- src/hooks/usePublicationSectionLoader.ts | 69 ++++++++++++-------- src/pages/secondary/NotePage/NotFound.tsx | 13 +++- src/providers/NostrProvider/index.tsx | 25 ++++++- src/providers/NostrProvider/nip-07.signer.ts | 4 +- 6 files changed, 136 insertions(+), 41 deletions(-) diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 490d0439..95655293 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -351,24 +351,47 @@ function restoredPrimaryBrowserUrl(pathname: string, fullUrlForQuery: string): s } // Helper function to extract noteId and context from URL -function parseNoteUrl(url: string): { noteId: string; context?: string } { +function extractValidNoteId(raw: string): string | null { + const decoded = (() => { + try { + return decodeURIComponent(raw).trim() + } catch { + return raw.trim() + } + })() + const withoutPrefix = decoded.startsWith('nostr:') ? decoded.slice(6) : decoded + if (/^[0-9a-f]{64}$/i.test(withoutPrefix)) return withoutPrefix.toLowerCase() + const lower = withoutPrefix.toLowerCase() + if ( + lower.startsWith('note1') || + lower.startsWith('nevent1') || + lower.startsWith('naddr1') + ) { + return withoutPrefix + } + return null +} + +function parseNoteUrl(url: string): { noteId: string; context?: string } | null { // Match patterns like /discussions/notes/{noteId} or /notes/{noteId} const contextualMatch = url.match( /\/(discussions|search|profile|home|feed|spells|explore|rss|follows-latest)\/notes\/(.+)$/ ) if (contextualMatch) { - return { noteId: contextualMatch[2], context: contextualMatch[1] } + const noteId = extractValidNoteId(contextualMatch[2]) + if (!noteId) return null + return { noteId, context: contextualMatch[1] } } // Match standard pattern /notes/{noteId} const standardMatch = url.match(/\/notes\/(.+)$/) if (standardMatch) { - return { noteId: standardMatch[1] } + const noteId = extractValidNoteId(standardMatch[1]) + if (!noteId) return null + return { noteId } } - // Fallback: extract from any /notes/ pattern - const fallbackMatch = url.replace(/.*\/notes\//, '') - return { noteId: fallbackMatch || url } + return null } // Fixed: Note navigation uses drawer on mobile/single-pane, secondary panel on double-pane desktop @@ -380,7 +403,12 @@ export function useSmartNoteNavigation() { const navigateToNote = (url: string, event?: Event, relatedEvents?: Event[]) => { // Extract noteId from URL (handles both /notes/{id} and /{context}/notes/{id}) - const { noteId } = parseNoteUrl(url) + const parsed = parseNoteUrl(url) + if (!parsed) { + logger.warn('navigateToNote ignored invalid note URL', { url }) + return + } + const { noteId } = parsed // If event is provided, store it in navigation event store to avoid re-fetching if (event) { @@ -441,7 +469,12 @@ export function useSmartNoteNavigationOptional() { const { current: currentPrimaryPage } = primaryPage const navigateToNote = (url: string, event?: Event, relatedEvents?: Event[]) => { - const { noteId } = parseNoteUrl(url) + const parsed = parseNoteUrl(url) + if (!parsed) { + logger.warn('navigateToNote (optional) ignored invalid note URL', { url }) + return + } + const { noteId } = parsed if (event) { navigationEventStore.setEvent(event) client.addEventToCache(event) diff --git a/src/components/ClientSelect/index.tsx b/src/components/ClientSelect/index.tsx index 5ca579de..dcdda631 100644 --- a/src/components/ClientSelect/index.tsx +++ b/src/components/ClientSelect/index.tsx @@ -77,6 +77,17 @@ function isRawHexEventId(id: string): boolean { return /^[0-9a-f]{64}$/i.test(id) } +function looksLikeNip19Pointer(id: string): boolean { + const v = id.trim().toLowerCase() + return ( + v.startsWith('note1') || + v.startsWith('nevent1') || + v.startsWith('naddr1') || + v.startsWith('npub1') || + v.startsWith('nprofile1') + ) +} + export default function ClientSelect({ event, originalNoteId, @@ -95,13 +106,14 @@ export default function ClientSelect({ if (event) { kind = event.kind } else if (originalNoteId && !isRawHexEventId(originalNoteId)) { + if (!looksLikeNip19Pointer(originalNoteId)) return ['njump'] try { const pointer = nip19.decode(originalNoteId) if (pointer.type === 'naddr') { kind = pointer.data.kind } } catch (error) { - logger.error('Failed to decode NIP-19 pointer', { error, originalNoteId }) + logger.warn('Ignoring invalid NIP-19 pointer for ClientSelect', { error, originalNoteId }) return ['njump'] } } @@ -222,6 +234,9 @@ function RelayBasedGroupChatSelector({ const { relay, id } = useMemo(() => { let relay: string | undefined if (originalNoteId && !isRawHexEventId(originalNoteId)) { + if (!looksLikeNip19Pointer(originalNoteId)) { + return { relay: clientService.getEventHint(event.id), id: getReplaceableEventIdentifier(event) } + } try { const pointer = nip19.decode(originalNoteId) if (pointer.type === 'naddr' && pointer.data.relays?.length) { diff --git a/src/hooks/usePublicationSectionLoader.ts b/src/hooks/usePublicationSectionLoader.ts index cb48e031..60e5d72e 100644 --- a/src/hooks/usePublicationSectionLoader.ts +++ b/src/hooks/usePublicationSectionLoader.ts @@ -251,33 +251,48 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication } if (row.coordinate) { const parsed = parsePublicationATagCoordinate(row.coordinate) - if (!parsed) return - // Relay hints in `a` tags are often stale. Keep the hint first, but also try - // current section relay sets so one dead hinted relay cannot force a false miss. - const relaysToTry = dedupeRelayUrls( - row.relay - ? [row.relay, ...relayUrls, ...fallbackRelayUrls] - : [...relayUrls, ...fallbackRelayUrls] - ) - const ev = await withTimeout( - queryService - .fetchEvents( - relaysToTry, - { - authors: [parsed.pubkey], - kinds: [parsed.kind], - '#d': [parsed.identifier], - limit: 1 - }, - { - globalTimeout: 6_000, - eoseTimeout: 1_500 - } - ) - .then((arr) => arr[0]), - SINGLE_REF_TIMEOUT_MS - ) - if (ev) bySingle.set(row.key, ev) + if (parsed) { + // Relay hints in `a` tags are often stale. Keep the hint first, but also try + // current section relay sets so one dead hinted relay cannot force a false miss. + const relaysToTry = dedupeRelayUrls( + row.relay + ? [row.relay, ...relayUrls, ...fallbackRelayUrls] + : [...relayUrls, ...fallbackRelayUrls] + ) + const ev = await withTimeout( + queryService + .fetchEvents( + relaysToTry, + { + authors: [parsed.pubkey], + kinds: [parsed.kind], + '#d': [parsed.identifier], + limit: 1 + }, + { + globalTimeout: 6_000, + eoseTimeout: 1_500 + } + ) + .then((arr) => arr[0]), + SINGLE_REF_TIMEOUT_MS + ) + if (ev) { + bySingle.set(row.key, ev) + return + } + } + + // Last per-ref fallback for `a` tags: try historical snapshot id (tag[3]). + // Some publication chains point to a specific revision that is fetchable by id + // even when relays don't resolve the coordinate in current indexes. + if (row.eventId) { + const byId = await withTimeout( + eventService.fetchEvent(row.eventId), + SINGLE_REF_TIMEOUT_MS + ) + if (byId) bySingle.set(row.key, byId) + } } } catch { // unresolved single-ref fallback diff --git a/src/pages/secondary/NotePage/NotFound.tsx b/src/pages/secondary/NotePage/NotFound.tsx index b7dcb90d..c703d96e 100644 --- a/src/pages/secondary/NotePage/NotFound.tsx +++ b/src/pages/secondary/NotePage/NotFound.tsx @@ -11,6 +11,11 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import logger from '@/lib/logger' +function looksLikeNip19Pointer(id: string): boolean { + const v = id.trim().toLowerCase() + return v.startsWith('note1') || v.startsWith('nevent1') || v.startsWith('naddr1') +} + export default function NotFound({ bech32Id, onEventFound @@ -36,6 +41,11 @@ export default function NotFound({ // CRITICAL: Parse relay hints from bech32 ID FIRST (highest priority) // These are explicit hints from the bech32 address and should always be used if (!/^[0-9a-f]{64}$/i.test(bech32Id)) { + if (!looksLikeNip19Pointer(bech32Id)) { + setHexEventId(null) + setExternalRelays([]) + return + } try { const { type, data } = nip19.decode(bech32Id) @@ -67,7 +77,7 @@ export default function NotFound({ extractedHexEventId = data } } catch (err) { - logger.error('Failed to parse bech32 ID for relay hints', { error: err, bech32Id }) + logger.warn('Failed to parse bech32 ID for relay hints', { error: err, bech32Id }) } } else { extractedHexEventId = bech32Id.toLowerCase() @@ -147,6 +157,7 @@ export default function NotFound({ hexEventId ?? (/^[0-9a-f]{64}$/i.test(bech32Id) ? bech32Id.toLowerCase() : null) ?? (() => { + if (!looksLikeNip19Pointer(bech32Id)) return null try { const { type, data } = nip19.decode(bech32Id) if (type === 'note') return data as string diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index a39f6174..1de1fc3a 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -961,6 +961,20 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } const loginWithAccountPointer = async (act: TAccountPointer): Promise => { + const fallbackToReadOnlyNpub = (pubkey: string, reason?: unknown): string => { + const npubSigner = new NpubSigner() + const npub = nip19.npubEncode(pubkey) + npubSigner.login(npub) + // Keep this fallback in-memory only; do not rewrite stored account type. + setAccount({ pubkey, signerType: 'npub' }) + setSigner(npubSigner) + logger.warn('[NostrProvider] Signer unavailable during restore; using read-only session', { + pubkeySlice: pubkey.slice(0, 12), + reason: reason instanceof Error ? reason.message : String(reason ?? '') + }) + return pubkey + } + let account = storage.findAccount(act) if (!account) { return null @@ -995,9 +1009,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { return login(browserNsecSigner, account) } } else if (account.signerType === 'nip-07') { - const nip07Signer = new Nip07Signer() - await nip07Signer.init() - return login(nip07Signer, account) + try { + const nip07Signer = new Nip07Signer() + await nip07Signer.init() + await nip07Signer.getPublicKey() + return login(nip07Signer, account) + } catch (err) { + return fallbackToReadOnlyNpub(account.pubkey, err) + } } else if (account.signerType === 'bunker') { if (account.bunker && account.bunkerClientSecretKey) { const bunkerSigner = new BunkerSigner(account.bunkerClientSecretKey) diff --git a/src/providers/NostrProvider/nip-07.signer.ts b/src/providers/NostrProvider/nip-07.signer.ts index 0736bd36..617fed3e 100644 --- a/src/providers/NostrProvider/nip-07.signer.ts +++ b/src/providers/NostrProvider/nip-07.signer.ts @@ -6,7 +6,9 @@ export class Nip07Signer implements ISigner { async init() { const checkInterval = 100 - const maxAttempts = 50 + // Some browser extensions inject `window.nostr` a bit later during startup/reload. + // Keep waiting longer to avoid false "no signer extension" failures on session restore. + const maxAttempts = 120 for (let attempt = 0; attempt < maxAttempts; attempt++) { if (window.nostr) {