From 5f7b5a31f5b28777abbb4563efa314d3dbea80bf Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 23 Mar 2026 21:12:33 +0100 Subject: [PATCH] relay selector --- .../FavoriteRelaysFeedPicker/index.tsx | 117 ++++++++++-------- src/providers/NostrProvider/index.tsx | 17 ++- src/services/client.service.ts | 31 +++-- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/src/components/FavoriteRelaysFeedPicker/index.tsx b/src/components/FavoriteRelaysFeedPicker/index.tsx index 56c45203..5cc7aadf 100644 --- a/src/components/FavoriteRelaysFeedPicker/index.tsx +++ b/src/components/FavoriteRelaysFeedPicker/index.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' import { Select, SelectContent, + SelectGroup, SelectItem, SelectLabel, SelectSeparator, @@ -31,7 +32,7 @@ function selectValueToRelaySetId(v: string) { return decodeURIComponent(v.slice(3)) } -/** Top-of-feed control: all favorites, single relays, and relay sets. */ +/** Top-of-feed control: all favorites, relay sets, then single relays. */ export default function FavoriteRelaysFeedPicker() { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() @@ -71,6 +72,12 @@ export default function FavoriteRelaysFeedPicker() { /** Values that exist in the mobile Select (for controlled `value` validation). */ const selectItems = useMemo(() => { const items: { value: string }[] = [{ value: ALL_FAVORITES_VALUE }] + for (const set of relaySets) { + items.push({ value: relaySetToSelectValue(set.id) }) + } + if (orphanRelaySetId) { + items.push({ value: relaySetToSelectValue(orphanRelaySetId) }) + } for (const url of urls) { items.push({ value: normalizeUrl(url) || url }) } @@ -82,12 +89,6 @@ export default function FavoriteRelaysFeedPicker() { ) { items.push({ value: normalizeUrl(feedInfo.id) || feedInfo.id }) } - for (const set of relaySets) { - items.push({ value: relaySetToSelectValue(set.id) }) - } - if (orphanRelaySetId) { - items.push({ value: relaySetToSelectValue(orphanRelaySetId) }) - } return items }, [ urls, @@ -157,35 +158,42 @@ export default function FavoriteRelaysFeedPicker() { {t('All favorite relays')} - {urls.map((url) => { - const v = normalizeUrl(url) || url - return ( - - {simplifyUrl(url)} - - ) - })} {relaySets.length > 0 || orphanRelaySetId ? ( <> - {t('Relay sets')} - {relaySets.map((set) => ( - - {set.name} - - ))} - {orphanRelaySetId ? ( - - {orphanRelaySetId} - - ) : null} + + {t('Relay sets')} + {relaySets.map((set) => ( + + {set.name} + + ))} + {orphanRelaySetId ? ( + + {orphanRelaySetId} + + ) : null} + + + ) : null} + {urls.length > 0 ? ( + <> + {relaySets.length > 0 || orphanRelaySetId ? : null} + {urls.map((url) => { + const v = normalizeUrl(url) || url + return ( + + {simplifyUrl(url)} + + ) + })} ) : null} @@ -215,26 +223,6 @@ export default function FavoriteRelaysFeedPicker() { > {t('All favorite relays')} - {urls.map((url) => { - const key = normalizeUrl(url) || url - const active = feedInfo.feedType === 'relay' && currentRelayKey === key - return ( - - ) - })} {(relaySets.length > 0 || orphanRelaySetId) && (
)} @@ -270,6 +258,29 @@ export default function FavoriteRelaysFeedPicker() { {orphanRelaySetId} ) : null} + {urls.length > 0 && (relaySets.length > 0 || orphanRelaySetId) && ( +
+ )} + {urls.map((url) => { + const key = normalizeUrl(url) || url + const active = feedInfo.feedType === 'relay' && currentRelayKey === key + return ( + + ) + })}
{editSettingsButton}
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 8170d836..e7cce8d4 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -923,9 +923,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { void replaceableEventService.updateReplaceableEventCache(event).catch(() => {}) } - // If publishing failed completely, throw an error so the form doesn't close - if (!publishResult.success) { - logger.error('[Publish] Publishing failed to all relays!', { + // Replaceable events and notes: cache above uses successCount >= 1. publishEvent still sets + // success only when >=1/3 of relays OK (broad replication). Treat "zero accepts" as failure + // so we don't throw when a few relays worked but many timed out (common with large outbox lists). + if (publishResult.successCount < 1) { + logger.error('[Publish] Publishing failed on every relay', { eventKind: event.kind, eventId: event.id?.substring(0, 8), relayStatuses: publishResult.relayStatuses, @@ -938,9 +940,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { 'Failed to publish to any relay' ) ;(error as any).relayStatuses = publishResult.relayStatuses - if (publishResult.successCount >= 1) (error as any).event = event throw error } + if (!publishResult.success) { + logger.warn('[Publish] Partial publish: some relays failed or timed out', { + eventKind: event.kind, + eventId: event.id?.substring(0, 8), + successCount: publishResult.successCount, + totalCount: publishResult.totalCount + }) + } logger.debug('[Publish] Publishing successful, attaching relayStatuses to event') // Attach relayStatuses only temporarily for UI feedback, then remove it diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 6ddc5179..af7378b7 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -559,10 +559,11 @@ class ClientService extends EventTarget { profileFetchRelays: PROFILE_FETCH_RELAY_URLS, additionalRelayCount: bootstrapExtras.length }) - } else if (event.kind === ExtendedKind.FAVORITE_RELAYS) { - // Use fast write relays for favorite relays to avoid timeouts and payment requirements + } else if (event.kind === ExtendedKind.FAVORITE_RELAYS || event.kind === kinds.Relaysets) { + // Use fast write relays for favorite-relays and kind 30002 relay-set replaceables to avoid + // timeouts and auth-only relays dominating the attempt list. bootstrapExtras.push(...FAST_WRITE_RELAY_URLS) - logger.debug('[DetermineTargetRelays] Favorite relays event detected, adding FAST_WRITE_RELAY_URLS', { + logger.debug('[DetermineTargetRelays] Favorite relays or relay set event, adding FAST_WRITE_RELAY_URLS', { kind: event.kind, fastWriteRelays: FAST_WRITE_RELAY_URLS, additionalRelayCount: bootstrapExtras.length @@ -571,7 +572,11 @@ class ClientService extends EventTarget { bootstrapExtras.push(...FAST_WRITE_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS) } - if (event.kind === kinds.RelayList || event.kind === ExtendedKind.FAVORITE_RELAYS) { + if ( + event.kind === kinds.RelayList || + event.kind === ExtendedKind.FAVORITE_RELAYS || + event.kind === kinds.Relaysets + ) { logger.debug('[DetermineTargetRelays] Fetching user relay list for event publication', { pubkey: event.pubkey, kind: event.kind @@ -587,7 +592,11 @@ class ClientService extends EventTarget { }) relayList = { write: [], read: [], originalRelays: [] } } - if (event.kind === kinds.RelayList || event.kind === ExtendedKind.FAVORITE_RELAYS) { + if ( + event.kind === kinds.RelayList || + event.kind === ExtendedKind.FAVORITE_RELAYS || + event.kind === kinds.Relaysets + ) { logger.debug('[DetermineTargetRelays] User relay list fetched', { hasRelayList: !!relayList, writeRelayCount: relayList?.write?.length ?? 0, @@ -609,7 +618,11 @@ class ClientService extends EventTarget { }), event ) - if (event.kind === kinds.RelayList || event.kind === ExtendedKind.FAVORITE_RELAYS) { + if ( + event.kind === kinds.RelayList || + event.kind === ExtendedKind.FAVORITE_RELAYS || + event.kind === kinds.Relaysets + ) { logger.info('[DetermineTargetRelays] Final relay list for event publication', { kind: event.kind, totalRelayCount: relays.length, @@ -786,7 +799,11 @@ class ClientService extends EventTarget { }) const uniqueRelayUrls = filtered - if (event.kind === kinds.RelayList || event.kind === ExtendedKind.FAVORITE_RELAYS) { + if ( + event.kind === kinds.RelayList || + event.kind === ExtendedKind.FAVORITE_RELAYS || + event.kind === kinds.Relaysets + ) { logger.info('[PublishEvent] Publishing event to relays', { eventId: event.id?.substring(0, 8), kind: event.kind,