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,