Browse Source

relay selector

imwald
Silberengel 1 month ago
parent
commit
5f7b5a31f5
  1. 81
      src/components/FavoriteRelaysFeedPicker/index.tsx
  2. 17
      src/providers/NostrProvider/index.tsx
  3. 31
      src/services/client.service.ts

81
src/components/FavoriteRelaysFeedPicker/index.tsx

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { import {
Select, Select,
SelectContent, SelectContent,
SelectGroup,
SelectItem, SelectItem,
SelectLabel, SelectLabel,
SelectSeparator, SelectSeparator,
@ -31,7 +32,7 @@ function selectValueToRelaySetId(v: string) {
return decodeURIComponent(v.slice(3)) 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() { export default function FavoriteRelaysFeedPicker() {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -71,6 +72,12 @@ export default function FavoriteRelaysFeedPicker() {
/** Values that exist in the mobile Select (for controlled `value` validation). */ /** Values that exist in the mobile Select (for controlled `value` validation). */
const selectItems = useMemo(() => { const selectItems = useMemo(() => {
const items: { value: string }[] = [{ value: ALL_FAVORITES_VALUE }] 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) { for (const url of urls) {
items.push({ value: normalizeUrl(url) || url }) items.push({ value: normalizeUrl(url) || url })
} }
@ -82,12 +89,6 @@ export default function FavoriteRelaysFeedPicker() {
) { ) {
items.push({ value: normalizeUrl(feedInfo.id) || feedInfo.id }) 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 return items
}, [ }, [
urls, urls,
@ -157,17 +158,10 @@ export default function FavoriteRelaysFeedPicker() {
<SelectItem value={ALL_FAVORITES_VALUE} className="text-xs"> <SelectItem value={ALL_FAVORITES_VALUE} className="text-xs">
{t('All favorite relays')} {t('All favorite relays')}
</SelectItem> </SelectItem>
{urls.map((url) => {
const v = normalizeUrl(url) || url
return (
<SelectItem key={v} value={v} className="font-mono text-xs" title={url}>
{simplifyUrl(url)}
</SelectItem>
)
})}
{relaySets.length > 0 || orphanRelaySetId ? ( {relaySets.length > 0 || orphanRelaySetId ? (
<> <>
<SelectSeparator /> <SelectSeparator />
<SelectGroup>
<SelectLabel className="pl-2">{t('Relay sets')}</SelectLabel> <SelectLabel className="pl-2">{t('Relay sets')}</SelectLabel>
{relaySets.map((set) => ( {relaySets.map((set) => (
<SelectItem <SelectItem
@ -186,6 +180,20 @@ export default function FavoriteRelaysFeedPicker() {
{orphanRelaySetId} {orphanRelaySetId}
</SelectItem> </SelectItem>
) : null} ) : null}
</SelectGroup>
</>
) : null}
{urls.length > 0 ? (
<>
{relaySets.length > 0 || orphanRelaySetId ? <SelectSeparator /> : null}
{urls.map((url) => {
const v = normalizeUrl(url) || url
return (
<SelectItem key={v} value={v} className="font-mono text-xs" title={url}>
{simplifyUrl(url)}
</SelectItem>
)
})}
</> </>
) : null} ) : null}
</SelectContent> </SelectContent>
@ -215,26 +223,6 @@ export default function FavoriteRelaysFeedPicker() {
> >
{t('All favorite relays')} {t('All favorite relays')}
</button> </button>
{urls.map((url) => {
const key = normalizeUrl(url) || url
const active = feedInfo.feedType === 'relay' && currentRelayKey === key
return (
<button
key={key}
type="button"
className={cn(
'max-w-[11rem] shrink-0 truncate rounded-full border px-3 py-1 font-mono text-xs font-semibold transition-colors',
active
? 'border-primary bg-primary/15 text-foreground'
: 'border-border bg-muted/40 text-muted-foreground hover:bg-accent'
)}
title={url}
onClick={() => void switchFeed('relay', { relay: url })}
>
{simplifyUrl(url)}
</button>
)
})}
{(relaySets.length > 0 || orphanRelaySetId) && ( {(relaySets.length > 0 || orphanRelaySetId) && (
<div className="mx-0.5 shrink-0 self-stretch border-l border-border/80" aria-hidden /> <div className="mx-0.5 shrink-0 self-stretch border-l border-border/80" aria-hidden />
)} )}
@ -270,6 +258,29 @@ export default function FavoriteRelaysFeedPicker() {
{orphanRelaySetId} {orphanRelaySetId}
</button> </button>
) : null} ) : null}
{urls.length > 0 && (relaySets.length > 0 || orphanRelaySetId) && (
<div className="mx-0.5 shrink-0 self-stretch border-l border-border/80" aria-hidden />
)}
{urls.map((url) => {
const key = normalizeUrl(url) || url
const active = feedInfo.feedType === 'relay' && currentRelayKey === key
return (
<button
key={key}
type="button"
className={cn(
'max-w-[11rem] shrink-0 truncate rounded-full border px-3 py-1 font-mono text-xs font-semibold transition-colors',
active
? 'border-primary bg-primary/15 text-foreground'
: 'border-border bg-muted/40 text-muted-foreground hover:bg-accent'
)}
title={url}
onClick={() => void switchFeed('relay', { relay: url })}
>
{simplifyUrl(url)}
</button>
)
})}
</div> </div>
{editSettingsButton} {editSettingsButton}
</div> </div>

17
src/providers/NostrProvider/index.tsx

@ -923,9 +923,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
void replaceableEventService.updateReplaceableEventCache(event).catch(() => {}) void replaceableEventService.updateReplaceableEventCache(event).catch(() => {})
} }
// If publishing failed completely, throw an error so the form doesn't close // Replaceable events and notes: cache above uses successCount >= 1. publishEvent still sets
if (!publishResult.success) { // success only when >=1/3 of relays OK (broad replication). Treat "zero accepts" as failure
logger.error('[Publish] Publishing failed to all relays!', { // 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, eventKind: event.kind,
eventId: event.id?.substring(0, 8), eventId: event.id?.substring(0, 8),
relayStatuses: publishResult.relayStatuses, relayStatuses: publishResult.relayStatuses,
@ -938,9 +940,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
'Failed to publish to any relay' 'Failed to publish to any relay'
) )
;(error as any).relayStatuses = publishResult.relayStatuses ;(error as any).relayStatuses = publishResult.relayStatuses
if (publishResult.successCount >= 1) (error as any).event = event
throw error 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') logger.debug('[Publish] Publishing successful, attaching relayStatuses to event')
// Attach relayStatuses only temporarily for UI feedback, then remove it // Attach relayStatuses only temporarily for UI feedback, then remove it

31
src/services/client.service.ts

@ -559,10 +559,11 @@ class ClientService extends EventTarget {
profileFetchRelays: PROFILE_FETCH_RELAY_URLS, profileFetchRelays: PROFILE_FETCH_RELAY_URLS,
additionalRelayCount: bootstrapExtras.length additionalRelayCount: bootstrapExtras.length
}) })
} else if (event.kind === ExtendedKind.FAVORITE_RELAYS) { } else if (event.kind === ExtendedKind.FAVORITE_RELAYS || event.kind === kinds.Relaysets) {
// Use fast write relays for favorite relays to avoid timeouts and payment requirements // 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) 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, kind: event.kind,
fastWriteRelays: FAST_WRITE_RELAY_URLS, fastWriteRelays: FAST_WRITE_RELAY_URLS,
additionalRelayCount: bootstrapExtras.length additionalRelayCount: bootstrapExtras.length
@ -571,7 +572,11 @@ class ClientService extends EventTarget {
bootstrapExtras.push(...FAST_WRITE_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS) 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', { logger.debug('[DetermineTargetRelays] Fetching user relay list for event publication', {
pubkey: event.pubkey, pubkey: event.pubkey,
kind: event.kind kind: event.kind
@ -587,7 +592,11 @@ class ClientService extends EventTarget {
}) })
relayList = { write: [], read: [], originalRelays: [] } 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', { logger.debug('[DetermineTargetRelays] User relay list fetched', {
hasRelayList: !!relayList, hasRelayList: !!relayList,
writeRelayCount: relayList?.write?.length ?? 0, writeRelayCount: relayList?.write?.length ?? 0,
@ -609,7 +618,11 @@ class ClientService extends EventTarget {
}), }),
event 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', { logger.info('[DetermineTargetRelays] Final relay list for event publication', {
kind: event.kind, kind: event.kind,
totalRelayCount: relays.length, totalRelayCount: relays.length,
@ -786,7 +799,11 @@ class ClientService extends EventTarget {
}) })
const uniqueRelayUrls = filtered 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', { logger.info('[PublishEvent] Publishing event to relays', {
eventId: event.id?.substring(0, 8), eventId: event.id?.substring(0, 8),
kind: event.kind, kind: event.kind,

Loading…
Cancel
Save