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. 19
      src/hooks/usePublicationSectionLoader.ts
  4. 13
      src/pages/secondary/NotePage/NotFound.tsx
  5. 19
      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 @@ -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() { @@ -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() { @@ -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)

17
src/components/ClientSelect/index.tsx

@ -77,6 +77,17 @@ function isRawHexEventId(id: string): boolean { @@ -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({ @@ -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({ @@ -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) {

19
src/hooks/usePublicationSectionLoader.ts

@ -251,7 +251,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication @@ -251,7 +251,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
}
if (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
// current section relay sets so one dead hinted relay cannot force a false miss.
const relaysToTry = dedupeRelayUrls(
@ -277,7 +277,22 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication @@ -277,7 +277,22 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
.then((arr) => arr[0]),
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 {
// unresolved single-ref fallback

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

@ -11,6 +11,11 @@ import { useEffect, useState } from 'react' @@ -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({ @@ -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({ @@ -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({ @@ -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

19
src/providers/NostrProvider/index.tsx

@ -961,6 +961,20 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -961,6 +961,20 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
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)
if (!account) {
return null
@ -995,9 +1009,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -995,9 +1009,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return login(browserNsecSigner, account)
}
} else if (account.signerType === 'nip-07') {
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)

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

@ -6,7 +6,9 @@ export class Nip07Signer implements ISigner { @@ -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) {

Loading…
Cancel
Save