From a05ba5d92ac871b09d5805ed6a41af283e64f5d3 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 10 Nov 2025 13:58:15 +0100 Subject: [PATCH] fix relay selection when writing --- .../PostEditor/PostRelaySelector.tsx | 67 ++++++++++++- src/services/relay-selection.service.ts | 97 +++++++++++++++++-- 2 files changed, 152 insertions(+), 12 deletions(-) diff --git a/src/components/PostEditor/PostRelaySelector.tsx b/src/components/PostEditor/PostRelaySelector.tsx index 136cac8..7c6ebb2 100644 --- a/src/components/PostEditor/PostRelaySelector.tsx +++ b/src/components/PostEditor/PostRelaySelector.tsx @@ -1,8 +1,11 @@ -import { simplifyUrl, isLocalNetworkUrl } from '@/lib/url' +import { simplifyUrl, isLocalNetworkUrl, normalizeUrl } from '@/lib/url' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useNostr } from '@/providers/NostrProvider' +import { getRelayListFromEvent } from '@/lib/event-metadata' +import indexedDb from '@/services/indexed-db.service' +import { ExtendedKind } from '@/constants' import { Check, ChevronDown, Server } from 'lucide-react' import { NostrEvent } from 'nostr-tools' import { Dispatch, SetStateAction, useCallback, useEffect, useState, useMemo } from 'react' @@ -78,8 +81,37 @@ export default function PostRelaySelector({ const updateRelaySelection = async () => { setIsLoading(true) try { + // Ensure cache relays (kind 10432) are included in userWriteRelays even if relayList hasn't been updated yet + // Get cache relays directly from IndexedDB (don't fetch new every time) + let userWriteRelays = relayList?.write || [] + if (pubkey) { + try { + const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS) + if (cacheRelayListEvent) { + const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent) + // Get all cache relays (they should all be local network URLs) + // Include both write and both-scoped relays (cache relays should be write-capable) + const cacheRelays = [ + ...cacheRelayList.write, + ...cacheRelayList.originalRelays + .filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url)) + .map(relay => relay.url) + ].filter(url => isLocalNetworkUrl(url)) + const existingUrls = new Set(userWriteRelays.map(url => normalizeUrl(url) || url)) + const newCacheRelays = cacheRelays + .map(url => normalizeUrl(url) || url) + .filter((url): url is string => !!url && !existingUrls.has(url)) + if (newCacheRelays.length > 0) { + userWriteRelays = [...newCacheRelays, ...userWriteRelays] + } + } + } catch (error) { + logger.warn('Failed to get cache relays from IndexedDB', { error, pubkey }) + } + } + const result = await relaySelectionService.selectRelays({ - userWriteRelays: relayList?.write || [], + userWriteRelays, userReadRelays: relayList?.read || [], favoriteRelays: memoizedFavoriteRelays, blockedRelays: memoizedBlockedRelays, @@ -140,8 +172,37 @@ export default function PostRelaySelector({ const updateRelaySelection = async () => { setIsLoading(true) try { + // Ensure cache relays (kind 10432) are included in userWriteRelays even if relayList hasn't been updated yet + // Get cache relays directly from IndexedDB (don't fetch new every time) + let userWriteRelays = relayList?.write || [] + if (pubkey) { + try { + const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS) + if (cacheRelayListEvent) { + const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent) + // Get all cache relays (they should all be local network URLs) + // Include both write and both-scoped relays (cache relays should be write-capable) + const cacheRelays = [ + ...cacheRelayList.write, + ...cacheRelayList.originalRelays + .filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url)) + .map(relay => relay.url) + ].filter(url => isLocalNetworkUrl(url)) + const existingUrls = new Set(userWriteRelays.map(url => normalizeUrl(url) || url)) + const newCacheRelays = cacheRelays + .map(url => normalizeUrl(url) || url) + .filter((url): url is string => !!url && !existingUrls.has(url)) + if (newCacheRelays.length > 0) { + userWriteRelays = [...newCacheRelays, ...userWriteRelays] + } + } + } catch (error) { + logger.warn('Failed to get cache relays from IndexedDB', { error, pubkey }) + } + } + const result = await relaySelectionService.selectRelays({ - userWriteRelays: relayList?.write || [], + userWriteRelays, userReadRelays: relayList?.read || [], favoriteRelays: memoizedFavoriteRelays, blockedRelays: memoizedBlockedRelays, diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index da7498d..21d09db 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -3,8 +3,10 @@ import { ExtendedKind } from '@/constants' import { FAST_WRITE_RELAY_URLS } from '@/constants' import client from '@/services/client.service' import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url' -import { TRelaySet } from '@/types' +import { TRelaySet, TRelayList } from '@/types' import logger from '@/lib/logger' +import indexedDb from '@/services/indexed-db.service' +import { getRelayListFromEvent } from '@/lib/event-metadata' export interface RelaySelectionContext { // User's own relays @@ -96,6 +98,11 @@ class RelaySelectionService { const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS userRelays.forEach(addRelay) + // Explicitly ensure cache relays (local network URLs) are included in selectable relays + // This ensures they show up even if there's a timing issue with relay list updates + const cacheRelays = userWriteRelays.filter(url => isLocalNetworkUrl(url)) + cacheRelays.forEach(addRelay) + // Always include favorite relays favoriteRelays.forEach(addRelay) @@ -120,6 +127,72 @@ class RelaySelectionService { return this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays) } + /** + * Get relay list from IndexedDB cache (kind 10002 and 10432 merged) + * If not in cache, fetch from relays before returning empty + * This avoids fetching from relays every time, but ensures we have data when needed + */ + private async getCachedRelayList(pubkey: string): Promise { + try { + // Get both kind 10002 (relay list) and kind 10432 (cache relays) from IndexedDB + const [relayListEvent, cacheRelayListEvent] = await Promise.all([ + indexedDb.getReplaceableEvent(pubkey, kinds.RelayList), + indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS) + ]) + + let relayList: TRelayList + + // If no cached relay list event, fetch from relays (which will also cache it) + if (!relayListEvent) { + try { + relayList = await client.fetchRelayList(pubkey) + } catch (error) { + logger.warn('Failed to fetch relay list from relays', { error, pubkey }) + relayList = { + write: [], + read: [], + originalRelays: [] + } + } + } else { + relayList = getRelayListFromEvent(relayListEvent) + } + + // Merge cache relays (kind 10432) into the relay list + if (cacheRelayListEvent) { + const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent) + + // Merge read relays - cache relays first, then others + const mergedRead = [...cacheRelayList.read, ...relayList.read] + const mergedWrite = [...cacheRelayList.write, ...relayList.write] + const mergedOriginalRelays = new Map() + + // Add cache relay original relays first (prioritized) + cacheRelayList.originalRelays.forEach(relay => { + mergedOriginalRelays.set(relay.url, relay) + }) + // Then add regular relay original relays + relayList.originalRelays.forEach(relay => { + if (!mergedOriginalRelays.has(relay.url)) { + mergedOriginalRelays.set(relay.url, relay) + } + }) + + // Deduplicate while preserving order (cache relays first) + return { + write: Array.from(new Set(mergedWrite)), + read: Array.from(new Set(mergedRead)), + originalRelays: Array.from(mergedOriginalRelays.values()) + } + } + + return relayList + } catch (error) { + logger.warn('Failed to get cached relay list from IndexedDB', { error, pubkey }) + return null + } + } + /** * Get contextual relays based on the type of post */ @@ -132,8 +205,9 @@ class RelaySelectionService { // For replies (any kind) and public messages if (parentEvent || isPublicMessage) { // Get the replied-to author's read relays (filter out their local relays) + // Use cached version from IndexedDB instead of fetching from relays if (parentEvent) { - const authorRelayList = await client.fetchRelayList(parentEvent.pubkey) + const authorRelayList = await this.getCachedRelayList(parentEvent.pubkey) if (authorRelayList?.read) { const filteredRelays = this.filterLocalRelaysFromOthers(authorRelayList.read) filteredRelays.slice(0, 4).forEach(url => contextualRelays.add(url)) @@ -168,14 +242,16 @@ class RelaySelectionService { const mentionRelayLists = await Promise.all( mentionedPubkeys.map(async (pubkey) => { try { - const relayList = await client.fetchRelayList(pubkey) + // Use cached version from IndexedDB instead of fetching from relays + const relayList = await this.getCachedRelayList(pubkey) + if (!relayList) return [] // Use write relays for replies, read relays for public messages const relayType = isPublicMessage ? 'read' : 'write' - const userRelays = relayList?.[relayType] || [] + const userRelays = relayList[relayType] || [] // Filter out local relays from other users return this.filterLocalRelaysFromOthers(userRelays) } catch (error) { - logger.warn('Failed to fetch relay list', { pubkey, error }) + logger.warn('Failed to get cached relay list', { pubkey, error }) return [] } }) @@ -254,12 +330,14 @@ class RelaySelectionService { const mentionRelayLists = await Promise.all( mentionedPubkeys.map(async (pubkey) => { try { - const relayList = await client.fetchRelayList(pubkey) - const userRelays = relayList?.write || [] + // Use cached version from IndexedDB instead of fetching from relays + const relayList = await this.getCachedRelayList(pubkey) + if (!relayList) return [] + const userRelays = relayList.write || [] // Filter out local relays from other users return this.filterLocalRelaysFromOthers(userRelays) } catch (error) { - logger.warn('Failed to fetch relay list', { pubkey, error }) + logger.warn('Failed to get cached relay list', { pubkey, error }) return [] } }) @@ -342,8 +420,9 @@ class RelaySelectionService { } } else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) { // For public message replies, get original sender's read relays (filter out their local relays) + // Use cached version from IndexedDB instead of fetching from relays try { - const senderRelayList = await client.fetchRelayList(parentEvent.pubkey) + const senderRelayList = await this.getCachedRelayList(parentEvent.pubkey) if (senderRelayList?.read) { const filteredRelays = this.filterLocalRelaysFromOthers(senderRelayList.read) filteredRelays.forEach(url => {