From 72584f66de3406bc9134ede72f92187080a08ad2 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 28 Mar 2026 09:47:51 +0100 Subject: [PATCH] free session-blocked relays manually session-strike http relays raise max publish relays to 20 correct relay list count bug-fixes --- .../PostEditor/PostRelaySelector.tsx | 127 ++++++++++++++---- src/components/SessionRelaysTab/index.tsx | 45 ++++++- src/constants.ts | 2 +- src/i18n/locales/de.ts | 7 +- src/i18n/locales/en.ts | 7 +- src/lib/draft-event.ts | 30 +++-- src/lib/event.ts | 96 ++++++++++++- src/lib/index-relay-http.ts | 28 +++- src/lib/thread-reply-root-match.ts | 4 +- src/providers/ReplyProvider.tsx | 7 +- src/services/client-query.service.ts | 7 +- src/services/client.service.ts | 48 +++++-- 12 files changed, 339 insertions(+), 69 deletions(-) diff --git a/src/components/PostEditor/PostRelaySelector.tsx b/src/components/PostEditor/PostRelaySelector.tsx index d6cebcb6..2316730f 100644 --- a/src/components/PostEditor/PostRelaySelector.tsx +++ b/src/components/PostEditor/PostRelaySelector.tsx @@ -2,10 +2,12 @@ import { ExtendedKind, isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, + READ_ONLY_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' -import { simplifyUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' +import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' +import { simplifyUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -44,6 +46,16 @@ export default function PostRelaySelector({ mentions?: string[] }) { const { t } = useTranslation() + /** Subtitle + trigger must match {@link selectedRelayUrls} (service description ignored: cache relays are merged in after). */ + const describeRelaySelection = useCallback( + (urls: string[]) => { + const n = urls.length + if (n === 0) return t('No relays selected') + if (n === 1) return simplifyUrl(urls[0]) + return t('{{count}} relays', { count: n }) + }, + [t] + ) const { isSmallScreen } = useScreenSize() useCurrentRelays() // Keep this hook call for any side effects const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() @@ -80,6 +92,62 @@ export default function PostRelaySelector({ return false }, [_parentEvent]) + /** + * Same merge order as {@link ClientService.publishEvent}: NIP-65 write list first, then relays checked here, + * then cap at {@link MAX_PUBLISH_RELAYS}. Drives the cap hint so users see reserved “prepended” slots. + */ + const publishCapPreview = useMemo(() => { + const applySocialOutboxFilter = + !isPublicMessage && + (_parentEvent == null || + isDiscussionReply || + (_parentEvent != null && isSocialKindBlockedKind(_parentEvent.kind))) + + const wsOut = (relayList?.write ?? []) + .map((u) => normalizeUrl(u) || u) + .filter((u): u is string => !!u) + const httpOut = (relayList?.httpWrite ?? []) + .map((u) => normalizeHttpRelayUrl(u) || u) + .filter((u): u is string => !!u) + let outbox = dedupeNormalizeRelayUrlsOrdered([...httpOut, ...wsOut]) + const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u)) + const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) + outbox = dedupeNormalizeRelayUrlsOrdered( + outbox.filter((url) => { + const n = normalizeAnyRelayUrl(url) || url + if (readOnlySet.has(n)) return false + if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false + return true + }) + ) + + const merged = dedupeNormalizeRelayUrlsOrdered([...outbox, ...selectedRelayUrls]) + const capped = merged.slice(0, MAX_PUBLISH_RELAYS) + const outboxNormSet = new Set(outbox) + const outboxSlotsInPublish = capped.filter((u) => outboxNormSet.has(u)).length + const selectedNorm = selectedRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u) + const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length + + const showCapHint = + merged.length > MAX_PUBLISH_RELAYS || + selectedRelayUrls.length >= MAX_PUBLISH_RELAYS || + selectedContacted < selectedRelayUrls.length + + return { + outboxSlotsInPublish, + selectedContacted, + selectedTotal: selectedRelayUrls.length, + showCapHint + } + }, [ + relayList?.write, + relayList?.httpWrite, + selectedRelayUrls, + isPublicMessage, + _parentEvent, + isDiscussionReply + ]) + /** * Relay selection only cares about nostr:… mentions in the draft (see relay-selection.service). * Depending on full `postContent` re-ran the heavy relay effect on every keystroke. @@ -179,7 +247,7 @@ export default function PostRelaySelector({ const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) setSelectedRelayUrls(selectedWithCache) - setDescription(result.description) + setDescription(describeRelaySelection(selectedWithCache)) // Reset manual selection flag if relays changed if (selectableRelaysChanged && hasManualSelection) { setHasManualSelection(false) @@ -191,7 +259,7 @@ export default function PostRelaySelector({ setSelectableRelays([]) if (!hasManualSelection) { setSelectedRelayUrls([]) - setDescription('No relays selected') + setDescription(t('No relays selected')) } } finally { setIsLoading(false) @@ -210,7 +278,9 @@ export default function PostRelaySelector({ relayList, isDiscussionReply, contentRelaySignature, - mentions + mentions, + describeRelaySelection, + t ]) // Separate effect for mention changes in non-discussion replies @@ -285,7 +355,7 @@ export default function PostRelaySelector({ const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) setSelectedRelayUrls(selectedWithCache) - setDescription(result.description) + setDescription(describeRelaySelection(selectedWithCache)) // Reset manual selection flag if relays changed if (selectableRelaysChanged && hasManualSelection) { setHasManualSelection(false) @@ -313,16 +383,16 @@ export default function PostRelaySelector({ relayList, memoizedOpenFrom, previousSelectableCount, - hasManualSelection + hasManualSelection, + describeRelaySelection ]) // Update description when selected relays change due to manual selection useEffect(() => { if (hasManualSelection && !isLoading) { - const count = selectedRelayUrls.length - setDescription(count === 0 ? 'No relays selected' : count === 1 ? simplifyUrl(selectedRelayUrls[0]) : `${count} relays`) + setDescription(describeRelaySelection(selectedRelayUrls)) } - }, [selectedRelayUrls, hasManualSelection, isLoading]) + }, [selectedRelayUrls, hasManualSelection, isLoading, describeRelaySelection]) // Update parent component with selected relays useEffect(() => { @@ -428,6 +498,27 @@ export default function PostRelaySelector({ return t('{{count}} relays', { count: selectedRelayUrls.length }) }, [selectedRelayUrls, isLoading, t]) + const capHintEl = + publishCapPreview.showCapHint && + (publishCapPreview.outboxSlotsInPublish > 0 ? ( + + {t('Publish relay cap hint with outbox first', { + max: MAX_PUBLISH_RELAYS, + reservedSlots: publishCapPreview.outboxSlotsInPublish, + selected: publishCapPreview.selectedTotal, + selectedContacted: publishCapPreview.selectedContacted + })} + + ) : ( + + {t('Publish relay cap hint', { + max: MAX_PUBLISH_RELAYS, + selected: publishCapPreview.selectedTotal, + selectedContacted: publishCapPreview.selectedContacted + })} + + )) + if (isSmallScreen) { return (
@@ -452,14 +543,7 @@ export default function PostRelaySelector({
{t('Select relays')} {description} - {selectedRelayUrls.length >= MAX_PUBLISH_RELAYS && ( - - {t('Publish relay cap hint', { - max: MAX_PUBLISH_RELAYS, - selected: selectedRelayUrls.length - })} - - )} + {capHintEl}
@@ -495,14 +579,7 @@ export default function PostRelaySelector({ {t('Select relays')} {description}
- {selectedRelayUrls.length >= MAX_PUBLISH_RELAYS && ( - - {t('Publish relay cap hint', { - max: MAX_PUBLISH_RELAYS, - selected: selectedRelayUrls.length - })} - - )} + {capHintEl}
{content} diff --git a/src/components/SessionRelaysTab/index.tsx b/src/components/SessionRelaysTab/index.tsx index fdbdf9ed..8dc77cf0 100644 --- a/src/components/SessionRelaysTab/index.tsx +++ b/src/components/SessionRelaysTab/index.tsx @@ -1,7 +1,7 @@ import client from '@/services/client.service' import { useTranslation } from 'react-i18next' import { useCallback, useEffect, useState } from 'react' -import { RefreshCw, CheckCircle2, XCircle, Zap } from 'lucide-react' +import { RefreshCw, CheckCircle2, XCircle, Zap, RotateCcw } from 'lucide-react' import { Button } from '@/components/ui/button' type SessionDebug = { @@ -29,6 +29,11 @@ export default function SessionRelaysTab() { if (debug === null) return null + const clearStrikeForUrl = (url: string) => { + client.clearSessionRelayStrikeForUrl(url) + refresh() + } + const formatUrl = (url: string) => { try { const u = new URL(url) @@ -79,13 +84,26 @@ export default function SessionRelaysTab() {

{t('Session relays preset striked hint')}

-