From fb4ad32c1d0aa93435c4e249eb47c1046bd8b9de Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 15 May 2026 16:24:21 +0200 Subject: [PATCH] fix cache relay --- package.json | 1 + scripts/start-orly-cache-relay.sh | 18 +++ src/PageManager.tsx | 8 +- .../CacheBrowser/CacheBrowserDialog.tsx | 133 ++++++++++++++++-- src/components/Relay/index.tsx | 7 +- src/i18n/locales/cs.ts | 9 ++ src/i18n/locales/de.ts | 9 ++ src/i18n/locales/en.ts | 10 ++ src/i18n/locales/es.ts | 9 ++ src/i18n/locales/fr.ts | 9 ++ src/i18n/locales/nl.ts | 9 ++ src/i18n/locales/pl.ts | 9 ++ src/i18n/locales/ru.ts | 9 ++ src/i18n/locales/tr.ts | 9 ++ src/i18n/locales/zh.ts | 9 ++ src/lib/event-metadata.ts | 21 ++- src/providers/NostrProvider/index.tsx | 18 ++- src/services/client.service.ts | 11 ++ src/services/indexed-db.service.ts | 3 +- vite.config.ts | 36 ++++- 20 files changed, 328 insertions(+), 19 deletions(-) create mode 100755 scripts/start-orly-cache-relay.sh diff --git a/package.json b/package.json index 4535eb80..576b04b2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "homepage": "https://github.com/Silberengel/jumble", "scripts": { "dev": "vite --host", + "orly:relay": "bash scripts/start-orly-cache-relay.sh", "dev:all": "bash scripts/dev-all-local.sh", "stack:remote": "bash scripts/stack-remote.sh", "dev:refresh": "rm -rf node_modules/.vite && vite --host", diff --git a/scripts/start-orly-cache-relay.sh b/scripts/start-orly-cache-relay.sh new file mode 100755 index 00000000..78b5671c --- /dev/null +++ b/scripts/start-orly-cache-relay.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Start a local ORLY Nostr relay for Jumble dev/testing (sibling repo ../next.orly.dev). +# Default: ws://127.0.0.1:4869/ — add that URL as a cache relay in settings. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +ORLY_ROOT="${ORLY_ROOT:-$ROOT/../next.orly.dev}" +export ORLY_PORT="${ORLY_PORT:-4869}" +export ORLY_ACL_MODE="${ORLY_ACL_MODE:-none}" +export ORLY_DATA_DIR="${ORLY_DATA_DIR:-${TMPDIR:-/tmp}/orly-jumble-relay-$ORLY_PORT}" +mkdir -p "$ORLY_DATA_DIR" +if [[ ! -d "$ORLY_ROOT/cmd/orly" ]]; then + echo "ORLY repo not found at: $ORLY_ROOT" >&2 + echo "Set ORLY_ROOT to your next.orly.dev checkout." >&2 + exit 1 +fi +echo "Orly: ws://127.0.0.1:${ORLY_PORT}/ (ORLY_DATA_DIR=$ORLY_DATA_DIR)" +cd "$ORLY_ROOT" +exec go run ./cmd/orly diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 2f98be16..412915ce 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -1935,7 +1935,13 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const pushSecondaryPage = (url: string, index?: number) => { logger.component('PageManager', 'pushSecondaryPage called', { url }) - + + // Small screens render either the primary overlay OR the secondary stack — not both. + // Clear overlays (e.g. full-screen note) so pushes from Seen-on, settings deep links, etc. show the target page. + if (isSmallScreen && primaryNoteView) { + setPrimaryNoteView(null) + } + // Save tab state before navigating const currentTab = currentTabStateRef.current.get(currentPrimaryPage) diff --git a/src/components/CacheBrowser/CacheBrowserDialog.tsx b/src/components/CacheBrowser/CacheBrowserDialog.tsx index ceb7e24b..0fe0e75c 100644 --- a/src/components/CacheBrowser/CacheBrowserDialog.tsx +++ b/src/components/CacheBrowser/CacheBrowserDialog.tsx @@ -2,10 +2,10 @@ import { Button } from '@/components/ui/button' import logger from '@/lib/logger' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Trash2, RefreshCw, Database, WrapText, Search, X, TriangleAlert, Copy } from 'lucide-react' +import { Trash2, RefreshCw, Database, WrapText, Search, X, TriangleAlert, Copy, Radio } from 'lucide-react' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' -import indexedDb, { StoreNames, type TCachedEventSearchHit } from '@/services/indexed-db.service' +import indexedDb, { isLikelyCachedNostrEvent, StoreNames, type TCachedEventSearchHit } from '@/services/indexed-db.service' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' @@ -13,6 +13,9 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider' import { toast } from 'sonner' import { Event } from 'nostr-tools' import { cn } from '@/lib/utils' +import { useNostr } from '@/providers/NostrProvider' +import client from '@/services/client.service' +import { toastPublishPromise } from '@/lib/publishing-feedback' const GLOBAL_CACHED_EVENTS_SEARCH_LIMIT = 400 @@ -25,6 +28,8 @@ export default function CacheBrowserDialog({ }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() + const { pubkey: accountPubkey } = useNostr() + const [broadcastingKey, setBroadcastingKey] = useState(null) const [cacheInfo, setCacheInfo] = useState>({}) const [selectedStore, setSelectedStore] = useState(null) const [storeItems, setStoreItems] = useState([]) @@ -228,6 +233,54 @@ export default function CacheBrowserDialog({ } } + const handleBroadcastToMailboxStack = useCallback( + (event: Event, itemKey: string) => { + if (!accountPubkey) { + toast.error(t('Log in to broadcast')) + return + } + if (event.pubkey?.toLowerCase() !== accountPubkey.toLowerCase()) { + toast.error(t('Only the author can broadcast this event')) + return + } + setBroadcastingKey(itemKey) + const promise = (async () => { + try { + const urls = await client.getMailboxStackWriteUrlsForRepublish(accountPubkey) + if (!urls.length) { + throw new Error(t('No mailbox cache or HTTP write relays configured')) + } + const result = await client.publishEvent(urls, event, { skipOutboxRetry: true }) + if (result.successCount < 1) { + throw new Error(t('No relay accepted the event')) + } + return result + } finally { + setBroadcastingKey(null) + } + })() + toastPublishPromise(promise, { + loading: t('Broadcasting to outbox HTTP and cache relays…'), + success: () => t('Broadcast to mailbox stack finished'), + error: (err) => + t('Failed to broadcast: {{error}}', { + error: err instanceof Error ? err.message : String(err) + }) + }) + }, + [accountPubkey, t] + ) + + /** Same predicate as full-text cache search — avoids hiding broadcast on valid rows that fail stricter checks. */ + const rowLooksLikeNostrEventForBroadcast = useCallback((v: unknown, storeName?: string | null): v is Event => { + if (storeName === 'rssFeedItems' || storeName === StoreNames.PIPER_TTS_CACHE) return false + return isLikelyCachedNostrEvent(v) + }, []) + + const nostrEventHasPublishableSig = useCallback((e: Event): boolean => { + return typeof e.sig === 'string' && e.sig.trim().length > 0 + }, []) + const isInvalidEvent = useCallback( (item: { key: string; value: any; addedAt: number }, storeName?: string | null): boolean => { if (!item) return true @@ -240,9 +293,9 @@ export default function CacheBrowserDialog({ } if (!item.value) return true const event = item.value as Event - if (!event.pubkey || !event.kind || typeof event.created_at !== 'number') return true + if (!event.pubkey || typeof event.kind !== 'number' || typeof event.created_at !== 'number') return true if (!event.tags || !Array.isArray(event.tags)) return true - if (!event.id || !event.sig) return true + if (!event.id || typeof event.sig !== 'string' || !event.sig.trim()) return true return false }, [] @@ -254,11 +307,11 @@ export default function CacheBrowserDialog({ const event = item.value as Event const missing: string[] = [] if (!event.pubkey) missing.push(t('pubkey')) - if (!event.kind) missing.push(t('kind')) + if (typeof event.kind !== 'number') missing.push(t('kind')) if (typeof event.created_at !== 'number') missing.push(t('created_at')) if (!event.tags || !Array.isArray(event.tags)) missing.push(t('tags')) if (!event.id) missing.push(t('id')) - if (!event.sig) missing.push(t('sig')) + if (typeof event.sig !== 'string' || !event.sig.trim()) missing.push(t('sig')) if (missing.length > 0) return t('Event is missing required fields: {{fields}}', { fields: missing.join(', ') }) return t('Event appears to be invalid or corrupted') }, @@ -292,9 +345,25 @@ export default function CacheBrowserDialog({ {t('Showing first {{count}} cached event matches.', { count: GLOBAL_CACHED_EVENTS_SEARCH_LIMIT })}

)} - {globalSearchHits.map((hit) => ( + {globalSearchHits.map((hit) => { + const hitRowKey = `${hit.storeName}:${hit.key}:${hit.value.id}` + const hitEvent = hit.value as Event + const showBroadcast = rowLooksLikeNostrEventForBroadcast(hit.value, hit.storeName) + const hasSig = nostrEventHasPublishableSig(hitEvent) + const authorOk = + !!accountPubkey && hitEvent.pubkey?.toLowerCase() === accountPubkey.toLowerCase() + const broadcastDisabled = + !accountPubkey || !authorOk || broadcastingKey !== null || !hasSig + const broadcastTitle = !accountPubkey + ? t('Log in to broadcast') + : !hasSig + ? t('Event must be signed to broadcast') + : !authorOk + ? t('Only the author can broadcast this event') + : t('Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays') + return (
@@ -324,9 +393,24 @@ export default function CacheBrowserDialog({ {t('Copy event JSON')} + {showBroadcast && ( + + )}
- ))} + ) + })} ) } @@ -396,6 +480,20 @@ export default function CacheBrowserDialog({ const nestedCount = (item as any).nestedCount const invalid = isInvalidEvent(item, selectedStore) const invalidExplanation = invalid ? getInvalidEventExplanation(item) : '' + const showBroadcast = rowLooksLikeNostrEventForBroadcast(item.value, selectedStore) + const rowEvent = item.value as Event + const hasSig = nostrEventHasPublishableSig(rowEvent) + const authorMatches = + !!accountPubkey && rowEvent.pubkey?.toLowerCase() === accountPubkey.toLowerCase() + const broadcastDisabled = + !accountPubkey || !authorMatches || broadcastingKey !== null || !hasSig + const broadcastTitle = !accountPubkey + ? t('Log in to broadcast') + : !hasSig + ? t('Event must be signed to broadcast') + : !authorMatches + ? t('Only the author can broadcast this event') + : t('Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays') return (
)} + {showBroadcast && ( + + )}
-
+
{item.key} {typeof nestedCount === 'number' && nestedCount > 0 && ( diff --git a/src/components/Relay/index.tsx b/src/components/Relay/index.tsx index 79c96541..69e029a3 100644 --- a/src/components/Relay/index.tsx +++ b/src/components/Relay/index.tsx @@ -5,7 +5,7 @@ import SearchInput from '@/components/SearchInput' import { useFetchRelayInfo } from '@/hooks' import type { TPrimaryPageName } from '@/PageManager' import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants' -import { normalizeAnyRelayUrl } from '@/lib/url' +import { isLocalNetworkUrl, normalizeAnyRelayUrl } from '@/lib/url' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import client from '@/services/client.service' import type { TFeedSubRequest } from '@/types' @@ -87,11 +87,14 @@ const Relay = forwardRef< const shouldHideEventNotFromThisRelay = useCallback( (ev: Event) => { if (!relaySeenMatchKey) return false + // LAN/loopback: REQ already targets this relay; "seen on" often lists another URL first + // (favorites merge, localhost vs 127.0.0.1, etc.) — hiding would empty the relay-only feed. + if (normalizedUrl && isLocalNetworkUrl(normalizedUrl)) return false const seen = client.getSeenEventRelayUrls(ev.id) if (seen.length === 0) return false return !seen.some((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase() === relaySeenMatchKey) }, - [relaySeenMatchKey] + [relaySeenMatchKey, normalizedUrl] ) if (!normalizedUrl) { diff --git a/src/i18n/locales/cs.ts b/src/i18n/locales/cs.ts index 659df9a5..689c1d78 100644 --- a/src/i18n/locales/cs.ts +++ b/src/i18n/locales/cs.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 0bc91c48..88e08b85 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -1385,6 +1385,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index e43c8fe9..48bfde4a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1431,6 +1431,16 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "Event must be signed to broadcast": "Event must be signed to broadcast", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index c3f7e782..8c19dcde 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index c2993687..c8884019 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts index 055f6626..6414f81e 100644 --- a/src/i18n/locales/nl.ts +++ b/src/i18n/locales/nl.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 4bf5a99b..b90c1847 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 7ed7fc7e..85935cf1 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts index adee87d9..2b23eb27 100644 --- a/src/i18n/locales/tr.ts +++ b/src/i18n/locales/tr.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 098636bd..8303b5b8 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -1369,6 +1369,15 @@ export default { "Open in store": "Open in store", "Browse cache root description": "View IndexedDB stores, or search all cached Nostr-like events (content, tags, id, pubkey, kind) across stores.", "Copy event JSON": "Copy event JSON", + "Broadcast to mailbox stack": "Broadcast to mailbox stack", + "Broadcasting to outbox HTTP and cache relays…": "Broadcasting to outbox, HTTP, and cache relays…", + "Broadcast to mailbox stack finished": "Broadcast to mailbox stack finished", + "Failed to broadcast: {{error}}": "Failed to broadcast: {{error}}", + "Log in to broadcast": "Log in to broadcast", + "Only the author can broadcast this event": "Only the author can broadcast this event", + "No mailbox cache or HTTP write relays configured": "No mailbox, cache, or HTTP write relays configured", + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays": + "Publish this event to your NIP-65 outbox, HTTP write relays, and cache relays", "C-Tag": "C-Tag", "Cache Relays": "Cache Relays", "Cache cleared successfully": "Cache cleared successfully", diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts index 32cdbf17..c64814bd 100644 --- a/src/lib/event-metadata.ts +++ b/src/lib/event-metadata.ts @@ -2,7 +2,7 @@ import { ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, POLL_TYPE } import { TEmoji, TMailboxRelay, TPollType, TRelayList, TRelaySet, TPaymentInfo, TProfile } from '@/types' import { Event, kinds } from 'nostr-tools' import { buildATag } from './draft-event' -import { getReplaceableEventIdentifier } from './event' +import { getLatestEvent, getReplaceableEventIdentifier } from './event' import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning' import { formatPubkey, pubkeyToNpub } from './pubkey' import { generateBech32IdFromATag, generateBech32IdFromETag, getImetaInfoFromImetaTag, tagNameEquals } from './tag' @@ -24,6 +24,25 @@ export type GetRelayListFromEventOptions = { globalReadWriteFallback?: boolean } +/** + * Merge kind-10432 (cache relays) from a network fetch with IndexedDB for session hydrate. + * Some mirrors return an empty or malformed 10432 with a newer `created_at` than good local data; prefer any + * candidate that still parses to at least one `r` WebSocket URL, then newest by time. + */ +export function mergeHydratedCacheRelayListEvents( + fetchedEvents: Event[], + stored: Event | undefined | null +): Event | null { + const fromFetch = fetchedEvents.length ? getLatestEvent(fetchedEvents) : undefined + const candidates = [fromFetch, stored ?? undefined].filter((e): e is Event => Boolean(e)) + if (candidates.length === 0) return null + const relayRowCount = (e: Event) => + getRelayListFromEvent(e, undefined, { globalReadWriteFallback: false }).originalRelays.length + const withRelays = candidates.filter((e) => relayRowCount(e) > 0) + const pool = withRelays.length > 0 ? withRelays : candidates + return pool.sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id))[0]! +} + export function getRelayListFromEvent( event?: Event | null, blockedRelays?: string[], diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 26d5d813..00867988 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -23,7 +23,12 @@ import { } from '@/lib/draft-event' import { getLatestEvent, minePow } from '@/lib/event' import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' -import { getHttpRelayListFromEvent, getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' +import { + getHttpRelayListFromEvent, + getProfileFromEvent, + getRelayListFromEvent, + mergeHydratedCacheRelayListEvents +} from '@/lib/event-metadata' import logger from '@/lib/logger' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import { LoginRequiredError } from '@/lib/nostr-errors' @@ -246,6 +251,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setMuteListEvent(null) setBookmarkListEvent(null) setRssFeedListEvent(null) + setCacheRelayListEvent(null) setHttpRelayListEvent(undefined) } @@ -414,6 +420,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setHttpRelayListEvent(storedHttpRelayListEvent ?? null) } + /** Kind 10432: always surface IDB in UI (incl. forced network hydrate); network merge refines below. */ + if (storedCacheRelayListEvent) { + setCacheRelayListEvent(storedCacheRelayListEvent) + } + const lastNetworkHydrateAt = storage.getAccountNetworkHydrateAt(account.pubkey) const hasLocalRelayAndProfile = !!storedRelayListEvent && !!storedProfileEvent const skipNetworkHydrate = @@ -496,7 +507,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { return controller } const relayListEvent = getLatestEvent(relayListEvents) ?? storedRelayListEvent - const cacheRelayListEvent = getLatestEvent(cacheRelayListEvents) ?? storedCacheRelayListEvent + const cacheRelayListEvent = mergeHydratedCacheRelayListEvents( + cacheRelayListEvents, + storedCacheRelayListEvent + ) const httpRelayListEventFetched = getLatestEvent(httpRelayListEvents) ?? storedHttpRelayListEvent ?? null if (relayListEvent) { client.updateRelayListCache(relayListEvent) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 3f0005c2..1a72014b 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -4110,6 +4110,17 @@ class ClientService extends EventTarget { return rl! } + /** + * Write targets for republishing from the cache browser: merged NIP-65 WS outbox + kind 10432 cache relays + + * kind 10243 HTTP write relays (same merge as {@link peekRelayListFromStorage}). No FAST_WRITE padding. + */ + async getMailboxStackWriteUrlsForRepublish(pubkey: string): Promise { + const rl = await this.peekRelayListFromStorage(pubkey) + const ws = (rl.write ?? []).map((u) => normalizeUrl(u) || u).filter((u): u is string => !!u) + const http = (rl.httpWrite ?? []).map((u) => normalizeHttpRelayUrl(u) || u).filter((u): u is string => !!u) + return dedupeNormalizeRelayUrlsOrdered([...http, ...ws]) + } + /** Newest kind 10002 for `pubkey` from IndexedDB and/or session LRU (session may hold a copy not persisted yet). */ private async getKind10002FromIdbOrSession(pubkey: string): Promise { let idb: NEvent | undefined | null diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 8f4d338a..fe0cafc7 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -64,7 +64,8 @@ export type TCachedEventSearchHit = { addedAt: number } -function isLikelyCachedNostrEvent(v: unknown): v is Event { +/** Shape check for persisted rows that look like Nostr events (used by cache search and cache browser). */ +export function isLikelyCachedNostrEvent(v: unknown): v is Event { if (!v || typeof v !== 'object') return false const o = v as Record return ( diff --git a/vite.config.ts b/vite.config.ts index c5ec4c6a..40eb18f8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -119,6 +119,31 @@ function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin { } } +/** + * Loopback / RFC1918 / ULA — mirrors `isLocalNetworkUrl` in `src/lib/url.ts` without importing it + * (Vite's config bundle does not resolve `@/` for transitive deps). + */ +function isLocalNetworkHostForSw(url: URL): boolean { + const hostname = url.hostname + if (hostname === 'localhost' || hostname === '::1') return true + const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) + if (ipv4Match) { + const [, a, b, c, d] = ipv4Match.map(Number) + return ( + a === 10 || + (a === 172 && b >= 16 && b <= 31) || + (a === 192 && b === 168) || + (a === 127 && b === 0 && c === 0 && d === 1) + ) + } + if (hostname.includes(':')) { + const lower = hostname.toLowerCase() + if (lower.startsWith('fe80:')) return true + if (lower.startsWith('fc') || lower.startsWith('fd')) return true + } + return false +} + // https://vite.dev/config/ export default defineConfig(({ mode }) => { // `.env.local` is not on `process.env` when this file is evaluated unless we load it. @@ -500,8 +525,15 @@ export default defineConfig(({ mode }) => { { // NIP-11 relay info documents: short-lived cache so relay metadata is fresh but // the app can render offline or on a slow connection without blocking on network. - urlPattern: ({ request }: { request: Request }) => - request.headers.get('accept')?.includes('application/nostr+json') ?? false, + // Do not intercept loopback/LAN: cross-origin + CORS often yields no cacheable response; + // StaleWhileRevalidate then rejects (Firefox: SW "no-response") and breaks relay pages. + urlPattern: ({ request, url }: { request: Request; url: URL }) => { + if (!(request.headers.get('accept')?.includes('application/nostr+json') ?? false)) { + return false + } + if (isLocalNetworkHostForSw(url)) return false + return true + }, handler: 'StaleWhileRevalidate', options: { cacheName: 'nip11-relay-info',