Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
6222ba5305
  1. 49
      src/PageManager.tsx
  2. 17
      src/components/ClientSelect/index.tsx
  3. 69
      src/hooks/usePublicationSectionLoader.ts
  4. 13
      src/pages/secondary/NotePage/NotFound.tsx
  5. 25
      src/providers/NostrProvider/index.tsx
  6. 4
      src/providers/NostrProvider/nip-07.signer.ts

49
src/PageManager.tsx

@ -351,24 +351,47 @@ function restoredPrimaryBrowserUrl(pathname: string, fullUrlForQuery: string): s
} }
// Helper function to extract noteId and context from URL // 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} // Match patterns like /discussions/notes/{noteId} or /notes/{noteId}
const contextualMatch = url.match( const contextualMatch = url.match(
/\/(discussions|search|profile|home|feed|spells|explore|rss|follows-latest)\/notes\/(.+)$/ /\/(discussions|search|profile|home|feed|spells|explore|rss|follows-latest)\/notes\/(.+)$/
) )
if (contextualMatch) { 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} // Match standard pattern /notes/{noteId}
const standardMatch = url.match(/\/notes\/(.+)$/) const standardMatch = url.match(/\/notes\/(.+)$/)
if (standardMatch) { if (standardMatch) {
return { noteId: standardMatch[1] } const noteId = extractValidNoteId(standardMatch[1])
if (!noteId) return null
return { noteId }
} }
// Fallback: extract from any /notes/ pattern return null
const fallbackMatch = url.replace(/.*\/notes\//, '')
return { noteId: fallbackMatch || url }
} }
// Fixed: Note navigation uses drawer on mobile/single-pane, secondary panel on double-pane desktop // 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[]) => { const navigateToNote = (url: string, event?: Event, relatedEvents?: Event[]) => {
// Extract noteId from URL (handles both /notes/{id} and /{context}/notes/{id}) // 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 is provided, store it in navigation event store to avoid re-fetching
if (event) { if (event) {
@ -441,7 +469,12 @@ export function useSmartNoteNavigationOptional() {
const { current: currentPrimaryPage } = primaryPage const { current: currentPrimaryPage } = primaryPage
const navigateToNote = (url: string, event?: Event, relatedEvents?: Event[]) => { 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) { if (event) {
navigationEventStore.setEvent(event) navigationEventStore.setEvent(event)
client.addEventToCache(event) client.addEventToCache(event)

17
src/components/ClientSelect/index.tsx

@ -77,6 +77,17 @@ function isRawHexEventId(id: string): boolean {
return /^[0-9a-f]{64}$/i.test(id) 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({ export default function ClientSelect({
event, event,
originalNoteId, originalNoteId,
@ -95,13 +106,14 @@ export default function ClientSelect({
if (event) { if (event) {
kind = event.kind kind = event.kind
} else if (originalNoteId && !isRawHexEventId(originalNoteId)) { } else if (originalNoteId && !isRawHexEventId(originalNoteId)) {
if (!looksLikeNip19Pointer(originalNoteId)) return ['njump']
try { try {
const pointer = nip19.decode(originalNoteId) const pointer = nip19.decode(originalNoteId)
if (pointer.type === 'naddr') { if (pointer.type === 'naddr') {
kind = pointer.data.kind kind = pointer.data.kind
} }
} catch (error) { } 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'] return ['njump']
} }
} }
@ -222,6 +234,9 @@ function RelayBasedGroupChatSelector({
const { relay, id } = useMemo(() => { const { relay, id } = useMemo(() => {
let relay: string | undefined let relay: string | undefined
if (originalNoteId && !isRawHexEventId(originalNoteId)) { if (originalNoteId && !isRawHexEventId(originalNoteId)) {
if (!looksLikeNip19Pointer(originalNoteId)) {
return { relay: clientService.getEventHint(event.id), id: getReplaceableEventIdentifier(event) }
}
try { try {
const pointer = nip19.decode(originalNoteId) const pointer = nip19.decode(originalNoteId)
if (pointer.type === 'naddr' && pointer.data.relays?.length) { if (pointer.type === 'naddr' && pointer.data.relays?.length) {

69
src/hooks/usePublicationSectionLoader.ts

@ -251,33 +251,48 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
} }
if (row.coordinate) { if (row.coordinate) {
const parsed = parsePublicationATagCoordinate(row.coordinate) const parsed = parsePublicationATagCoordinate(row.coordinate)
if (!parsed) return if (parsed) {
// Relay hints in `a` tags are often stale. Keep the hint first, but also try // 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. // current section relay sets so one dead hinted relay cannot force a false miss.
const relaysToTry = dedupeRelayUrls( const relaysToTry = dedupeRelayUrls(
row.relay row.relay
? [row.relay, ...relayUrls, ...fallbackRelayUrls] ? [row.relay, ...relayUrls, ...fallbackRelayUrls]
: [...relayUrls, ...fallbackRelayUrls] : [...relayUrls, ...fallbackRelayUrls]
) )
const ev = await withTimeout( const ev = await withTimeout(
queryService queryService
.fetchEvents( .fetchEvents(
relaysToTry, relaysToTry,
{ {
authors: [parsed.pubkey], authors: [parsed.pubkey],
kinds: [parsed.kind], kinds: [parsed.kind],
'#d': [parsed.identifier], '#d': [parsed.identifier],
limit: 1 limit: 1
}, },
{ {
globalTimeout: 6_000, globalTimeout: 6_000,
eoseTimeout: 1_500 eoseTimeout: 1_500
} }
) )
.then((arr) => arr[0]), .then((arr) => arr[0]),
SINGLE_REF_TIMEOUT_MS SINGLE_REF_TIMEOUT_MS
) )
if (ev) bySingle.set(row.key, ev) 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 { } catch {
// unresolved single-ref fallback // unresolved single-ref fallback

13
src/pages/secondary/NotePage/NotFound.tsx

@ -11,6 +11,11 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger' 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({ export default function NotFound({
bech32Id, bech32Id,
onEventFound onEventFound
@ -36,6 +41,11 @@ export default function NotFound({
// CRITICAL: Parse relay hints from bech32 ID FIRST (highest priority) // CRITICAL: Parse relay hints from bech32 ID FIRST (highest priority)
// These are explicit hints from the bech32 address and should always be used // These are explicit hints from the bech32 address and should always be used
if (!/^[0-9a-f]{64}$/i.test(bech32Id)) { if (!/^[0-9a-f]{64}$/i.test(bech32Id)) {
if (!looksLikeNip19Pointer(bech32Id)) {
setHexEventId(null)
setExternalRelays([])
return
}
try { try {
const { type, data } = nip19.decode(bech32Id) const { type, data } = nip19.decode(bech32Id)
@ -67,7 +77,7 @@ export default function NotFound({
extractedHexEventId = data extractedHexEventId = data
} }
} catch (err) { } 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 { } else {
extractedHexEventId = bech32Id.toLowerCase() extractedHexEventId = bech32Id.toLowerCase()
@ -147,6 +157,7 @@ export default function NotFound({
hexEventId ?? hexEventId ??
(/^[0-9a-f]{64}$/i.test(bech32Id) ? bech32Id.toLowerCase() : null) ?? (/^[0-9a-f]{64}$/i.test(bech32Id) ? bech32Id.toLowerCase() : null) ??
(() => { (() => {
if (!looksLikeNip19Pointer(bech32Id)) return null
try { try {
const { type, data } = nip19.decode(bech32Id) const { type, data } = nip19.decode(bech32Id)
if (type === 'note') return data as string if (type === 'note') return data as string

25
src/providers/NostrProvider/index.tsx

@ -961,6 +961,20 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
const loginWithAccountPointer = async (act: TAccountPointer): Promise<string | null> => { const loginWithAccountPointer = async (act: TAccountPointer): Promise<string | null> => {
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) let account = storage.findAccount(act)
if (!account) { if (!account) {
return null return null
@ -995,9 +1009,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return login(browserNsecSigner, account) return login(browserNsecSigner, account)
} }
} else if (account.signerType === 'nip-07') { } else if (account.signerType === 'nip-07') {
const nip07Signer = new Nip07Signer() try {
await nip07Signer.init() const nip07Signer = new Nip07Signer()
return login(nip07Signer, account) await nip07Signer.init()
await nip07Signer.getPublicKey()
return login(nip07Signer, account)
} catch (err) {
return fallbackToReadOnlyNpub(account.pubkey, err)
}
} else if (account.signerType === 'bunker') { } else if (account.signerType === 'bunker') {
if (account.bunker && account.bunkerClientSecretKey) { if (account.bunker && account.bunkerClientSecretKey) {
const bunkerSigner = new BunkerSigner(account.bunkerClientSecretKey) const bunkerSigner = new BunkerSigner(account.bunkerClientSecretKey)

4
src/providers/NostrProvider/nip-07.signer.ts

@ -6,7 +6,9 @@ export class Nip07Signer implements ISigner {
async init() { async init() {
const checkInterval = 100 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++) { for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (window.nostr) { if (window.nostr) {

Loading…
Cancel
Save