From 00991164ac9107c1af8b97681210f8106027b829 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 5 Jun 2026 12:11:13 +0200 Subject: [PATCH] bug-fix --- package-lock.json | 4 +- package.json | 2 +- .../PostEditor/PostEditorAdvancedPanel.tsx | 86 ++++++++++--------- .../PostEditor/PostRelaySelector.tsx | 62 ++++++++----- src/constants.ts | 6 +- src/services/relay-selection.service.ts | 22 ++++- 6 files changed, 112 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2faf67f1..69c0ca5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.19.1", + "version": "23.19.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.19.1", + "version": "23.19.2", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 804d5b21..1a962630 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.19.1", + "version": "23.19.2", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/PostEditor/PostEditorAdvancedPanel.tsx b/src/components/PostEditor/PostEditorAdvancedPanel.tsx index e4549eb0..9dd00d0d 100644 --- a/src/components/PostEditor/PostEditorAdvancedPanel.tsx +++ b/src/components/PostEditor/PostEditorAdvancedPanel.tsx @@ -76,19 +76,22 @@ export default function PostEditorAdvancedPanel({ setAddClientTag(storage.getAddClientTag()) }, [setAddClientTag]) - if (!show) return null - const onAddClientTagChange = (checked: boolean) => { storage.setAddClientTag(checked) setAddClientTag(checked) } + // Mentions + relay picker must stay mounted when Advanced is collapsed so auto-selection + // effects still run (especially on mobile where users often post without opening Advanced). return ( -
-
-

{t('Advanced')}

-

{t('Post editor advanced hint')}

-
+
+
+ {show ? ( +
+

{t('Advanced')}

+

{t('Post editor advanced hint')}

+
+ ) : null} {showMentionsPicker && setMentions ? (
@@ -142,47 +145,50 @@ export default function PostEditorAdvancedPanel({
) : null} -
-
+ {show ? ( +
+
+
+ + +
+

{t('Show others this was sent via Imwald')}

+
+
-
-

{t('Show others this was sent via Imwald')}

-
- -
- - -
-
- - setMinPow(pow)} - max={28} - step={1} - disabled={posting} - /> +
+ + setMinPow(pow)} + max={28} + step={1} + disabled={posting} + /> +
+ ) : null}
) diff --git a/src/components/PostEditor/PostRelaySelector.tsx b/src/components/PostEditor/PostRelaySelector.tsx index c8060635..4567e7ac 100644 --- a/src/components/PostEditor/PostRelaySelector.tsx +++ b/src/components/PostEditor/PostRelaySelector.tsx @@ -86,19 +86,26 @@ export default function PostRelaySelector({ /** Auto-picked relays from {@link relaySelectionService}; used to detect manual relay-picker changes. */ const autoSelectedRelayUrlsRef = useRef([]) const [previousSelectableCount, setPreviousSelectableCount] = useState(0) - // Generation counter: incremented every time the effect fires; async callback checks whether - // it's still the latest invocation before committing state, preventing stale races. - const selectionGenRef = useRef(0) + const hasManualSelectionRef = useRef(false) + const previousSelectableCountRef = useRef(0) + const publicLivelyDebounceRef = useRef | null>(null) useEffect(() => { - return nip66Service.subscribePublicLivelyUpdated(() => { - setPublicLivelyRevision((v) => v + 1) - }) - }, []) + hasManualSelectionRef.current = hasManualSelection + }, [hasManualSelection]) + + useEffect(() => { + previousSelectableCountRef.current = previousSelectableCount + }, [previousSelectableCount]) useEffect(() => { - void nip66Service.getPublicLivelyRelayUrls().then(() => { - setPublicLivelyRevision((v) => v + 1) + return nip66Service.subscribePublicLivelyUpdated(() => { + if (publicLivelyDebounceRef.current) clearTimeout(publicLivelyDebounceRef.current) + // Debounce: NIP-66 can emit many updates during discovery; batch them so selection + // is not restarted before the prior run finishes (which left "Loading…" stuck). + publicLivelyDebounceRef.current = setTimeout(() => { + setPublicLivelyRevision((v) => v + 1) + }, 600) }) }, []) @@ -159,6 +166,12 @@ export default function PostRelaySelector({ return [...new Set(matches)].sort().join('\n') }, [postContent, isDiscussionReply, isPublicMessage, mentions]) + /** Stable dep for PM recipient changes — raw `mentions` array identity changes every extract. */ + const mentionsRelaySignature = useMemo( + () => (isPublicMessage && mentions.length > 0 ? [...mentions].sort().join('\n') : ''), + [isPublicMessage, mentions] + ) + // Memoize arrays to prevent unnecessary re-renders const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays]) const memoizedBlockedRelays = useMemo(() => { @@ -175,13 +188,13 @@ export default function PostRelaySelector({ const memoizedRelaySets = useMemo(() => relaySets, [relaySets]) const memoizedOpenFrom = useMemo(() => openFrom, [openFrom]) - // Single relay-selection effect. The generation counter (selectionGenRef) guards against - // stale async completions: if a newer invocation has started, the older one discards its results. + // Single relay-selection effect. Cleanup sets `active = false` so superseded runs never + // commit stale state; only the latest run clears the loading indicator. useEffect(() => { - const gen = ++selectionGenRef.current + let active = true + setIsLoading(true) const updateRelaySelection = async () => { - setIsLoading(true) try { let userWriteRelays: string[] = [] if (pubkey && relayList) { @@ -203,41 +216,43 @@ export default function PostRelaySelector({ openFrom: memoizedOpenFrom }) - // Discard results from a superseded invocation - if (gen !== selectionGenRef.current) return + if (!active) return const newSelectableCount = result.selectableRelays.length - const selectableRelaysChanged = newSelectableCount !== previousSelectableCount + const selectableRelaysChanged = newSelectableCount !== previousSelectableCountRef.current setSelectableRelays(result.selectableRelays) setRelayTypes(result.relayTypes ?? {}) setPreviousSelectableCount(newSelectableCount) - if (!hasManualSelection || selectableRelaysChanged) { + if (!hasManualSelectionRef.current || selectableRelaysChanged) { const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) const capped = capAutoSelectedRelays(result.selectableRelays, selectedWithCache) autoSelectedRelayUrlsRef.current = capped setSelectedRelayUrls(capped) setDescription(describeRelaySelection(capped)) - if (selectableRelaysChanged && hasManualSelection) { + if (selectableRelaysChanged && hasManualSelectionRef.current) { setHasManualSelection(false) } } } catch (error) { - if (gen !== selectionGenRef.current) return + if (!active) return logger.error('Failed to update relay selection', { error }) setSelectableRelays([]) - if (!hasManualSelection) { + if (!hasManualSelectionRef.current) { setSelectedRelayUrls([]) setDescription(t('No relays selected')) } } finally { - if (gen === selectionGenRef.current) setIsLoading(false) + if (active) setIsLoading(false) } } - updateRelaySelection() + void updateRelaySelection() + return () => { + active = false + } }, [ memoizedOpenFrom, _parentEvent, @@ -247,9 +262,10 @@ export default function PostRelaySelector({ isPublicMessage, pubkey, relayList, + userReadRelaysForSelection, isDiscussionReply, contentRelaySignature, - mentions, + mentionsRelaySignature, describeRelaySelection, addRandomRelaysToPublish, publicLivelyRevision, diff --git a/src/constants.ts b/src/constants.ts index 42d6b756..fce9fb6d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -476,7 +476,8 @@ export const READ_ONLY_RELAY_URLS = [ 'wss://filter.nostr.wine', 'wss://primus.nostr1.com', 'wss://feeds.nostrarchives.com', - 'wss://spatia-arcana.com' + 'wss://spatia-arcana.com', + 'wss://search.nostrarchives.com' ] /** @@ -572,7 +573,8 @@ export const PROFILE_RELAY_URLS = [ 'wss://profiles.nostr1.com', 'wss://relay.damus.io', 'wss://thecitadel.nostr1.com', - 'wss://indexer.coracle.social/' + 'wss://indexer.coracle.social/', + 'wss://purplepag.es' ] export const FOLLOWS_HISTORY_RELAY_URLS = [ diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index 320b4e7d..86c2cb97 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -1,5 +1,5 @@ import { Event, kinds } from 'nostr-tools' -import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants' +import { ExtendedKind, FAST_WRITE_RELAY_URLS, PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants' import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { collectRecipientInboxUrls, collectSenderOutboxUrls } from '@/lib/public-message-publish-relays' import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes' @@ -277,7 +277,25 @@ class RelaySelectionService { // If no cached relay list event, fetch from relays (which will also cache it) if (!relayListEvent) { try { - relayList = await client.fetchRelayList(pubkey) // Keep using client for relay list merging + relayList = await Promise.race([ + client.fetchRelayList(pubkey), + new Promise((resolve) => + setTimeout( + () => + resolve( + mergeKind10243({ + write: [], + read: [], + originalRelays: [], + httpRead: [], + httpWrite: [], + httpOriginalRelays: [] + }) + ), + PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS + ) + ) + ]) } catch (error) { logger.warn('Failed to fetch relay list from relays', { error, pubkey }) relayList = mergeKind10243({