Browse Source

bug-fix

imwald
Silberengel 1 week ago
parent
commit
00991164ac
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 10
      src/components/PostEditor/PostEditorAdvancedPanel.tsx
  4. 60
      src/components/PostEditor/PostRelaySelector.tsx
  5. 6
      src/constants.ts
  6. 22
      src/services/relay-selection.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.19.1", "version": "23.19.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.19.1", "version": "23.19.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "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", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

10
src/components/PostEditor/PostEditorAdvancedPanel.tsx

@ -76,19 +76,22 @@ export default function PostEditorAdvancedPanel({
setAddClientTag(storage.getAddClientTag()) setAddClientTag(storage.getAddClientTag())
}, [setAddClientTag]) }, [setAddClientTag])
if (!show) return null
const onAddClientTagChange = (checked: boolean) => { const onAddClientTagChange = (checked: boolean) => {
storage.setAddClientTag(checked) storage.setAddClientTag(checked)
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 ( return (
<div className={cn(!show && 'hidden')} aria-hidden={!show}>
<div className="space-y-4 rounded-lg border border-border bg-muted/25 p-3"> <div className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
{show ? (
<div> <div>
<p className="text-sm font-medium">{t('Advanced')}</p> <p className="text-sm font-medium">{t('Advanced')}</p>
<p className="text-xs text-muted-foreground mt-0.5">{t('Post editor advanced hint')}</p> <p className="text-xs text-muted-foreground mt-0.5">{t('Post editor advanced hint')}</p>
</div> </div>
) : null}
{showMentionsPicker && setMentions ? ( {showMentionsPicker && setMentions ? (
<div className="space-y-2"> <div className="space-y-2">
@ -142,6 +145,7 @@ export default function PostEditorAdvancedPanel({
</div> </div>
) : null} ) : null}
{show ? (
<div className="space-y-4 pt-1 border-t border-border"> <div className="space-y-4 pt-1 border-t border-border">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -184,6 +188,8 @@ export default function PostEditorAdvancedPanel({
/> />
</div> </div>
</div> </div>
) : null}
</div>
</div> </div>
) )
} }

60
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. */ /** Auto-picked relays from {@link relaySelectionService}; used to detect manual relay-picker changes. */
const autoSelectedRelayUrlsRef = useRef<string[]>([]) const autoSelectedRelayUrlsRef = useRef<string[]>([])
const [previousSelectableCount, setPreviousSelectableCount] = useState(0) const [previousSelectableCount, setPreviousSelectableCount] = useState(0)
// Generation counter: incremented every time the effect fires; async callback checks whether const hasManualSelectionRef = useRef(false)
// it's still the latest invocation before committing state, preventing stale races. const previousSelectableCountRef = useRef(0)
const selectionGenRef = useRef(0) const publicLivelyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
return nip66Service.subscribePublicLivelyUpdated(() => { hasManualSelectionRef.current = hasManualSelection
setPublicLivelyRevision((v) => v + 1) }, [hasManualSelection])
})
}, []) useEffect(() => {
previousSelectableCountRef.current = previousSelectableCount
}, [previousSelectableCount])
useEffect(() => { useEffect(() => {
void nip66Service.getPublicLivelyRelayUrls().then(() => { 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) setPublicLivelyRevision((v) => v + 1)
}, 600)
}) })
}, []) }, [])
@ -159,6 +166,12 @@ export default function PostRelaySelector({
return [...new Set(matches)].sort().join('\n') return [...new Set(matches)].sort().join('\n')
}, [postContent, isDiscussionReply, isPublicMessage, mentions]) }, [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 // Memoize arrays to prevent unnecessary re-renders
const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays]) const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays])
const memoizedBlockedRelays = useMemo(() => { const memoizedBlockedRelays = useMemo(() => {
@ -175,13 +188,13 @@ export default function PostRelaySelector({
const memoizedRelaySets = useMemo(() => relaySets, [relaySets]) const memoizedRelaySets = useMemo(() => relaySets, [relaySets])
const memoizedOpenFrom = useMemo(() => openFrom, [openFrom]) const memoizedOpenFrom = useMemo(() => openFrom, [openFrom])
// Single relay-selection effect. The generation counter (selectionGenRef) guards against // Single relay-selection effect. Cleanup sets `active = false` so superseded runs never
// stale async completions: if a newer invocation has started, the older one discards its results. // commit stale state; only the latest run clears the loading indicator.
useEffect(() => { useEffect(() => {
const gen = ++selectionGenRef.current let active = true
setIsLoading(true)
const updateRelaySelection = async () => { const updateRelaySelection = async () => {
setIsLoading(true)
try { try {
let userWriteRelays: string[] = [] let userWriteRelays: string[] = []
if (pubkey && relayList) { if (pubkey && relayList) {
@ -203,41 +216,43 @@ export default function PostRelaySelector({
openFrom: memoizedOpenFrom openFrom: memoizedOpenFrom
}) })
// Discard results from a superseded invocation if (!active) return
if (gen !== selectionGenRef.current) return
const newSelectableCount = result.selectableRelays.length const newSelectableCount = result.selectableRelays.length
const selectableRelaysChanged = newSelectableCount !== previousSelectableCount const selectableRelaysChanged = newSelectableCount !== previousSelectableCountRef.current
setSelectableRelays(result.selectableRelays) setSelectableRelays(result.selectableRelays)
setRelayTypes(result.relayTypes ?? {}) setRelayTypes(result.relayTypes ?? {})
setPreviousSelectableCount(newSelectableCount) setPreviousSelectableCount(newSelectableCount)
if (!hasManualSelection || selectableRelaysChanged) { if (!hasManualSelectionRef.current || selectableRelaysChanged) {
const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url))
const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays]))
const capped = capAutoSelectedRelays(result.selectableRelays, selectedWithCache) const capped = capAutoSelectedRelays(result.selectableRelays, selectedWithCache)
autoSelectedRelayUrlsRef.current = capped autoSelectedRelayUrlsRef.current = capped
setSelectedRelayUrls(capped) setSelectedRelayUrls(capped)
setDescription(describeRelaySelection(capped)) setDescription(describeRelaySelection(capped))
if (selectableRelaysChanged && hasManualSelection) { if (selectableRelaysChanged && hasManualSelectionRef.current) {
setHasManualSelection(false) setHasManualSelection(false)
} }
} }
} catch (error) { } catch (error) {
if (gen !== selectionGenRef.current) return if (!active) return
logger.error('Failed to update relay selection', { error }) logger.error('Failed to update relay selection', { error })
setSelectableRelays([]) setSelectableRelays([])
if (!hasManualSelection) { if (!hasManualSelectionRef.current) {
setSelectedRelayUrls([]) setSelectedRelayUrls([])
setDescription(t('No relays selected')) setDescription(t('No relays selected'))
} }
} finally { } finally {
if (gen === selectionGenRef.current) setIsLoading(false) if (active) setIsLoading(false)
} }
} }
updateRelaySelection() void updateRelaySelection()
return () => {
active = false
}
}, [ }, [
memoizedOpenFrom, memoizedOpenFrom,
_parentEvent, _parentEvent,
@ -247,9 +262,10 @@ export default function PostRelaySelector({
isPublicMessage, isPublicMessage,
pubkey, pubkey,
relayList, relayList,
userReadRelaysForSelection,
isDiscussionReply, isDiscussionReply,
contentRelaySignature, contentRelaySignature,
mentions, mentionsRelaySignature,
describeRelaySelection, describeRelaySelection,
addRandomRelaysToPublish, addRandomRelaysToPublish,
publicLivelyRevision, publicLivelyRevision,

6
src/constants.ts

@ -476,7 +476,8 @@ export const READ_ONLY_RELAY_URLS = [
'wss://filter.nostr.wine', 'wss://filter.nostr.wine',
'wss://primus.nostr1.com', 'wss://primus.nostr1.com',
'wss://feeds.nostrarchives.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://profiles.nostr1.com',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://thecitadel.nostr1.com', 'wss://thecitadel.nostr1.com',
'wss://indexer.coracle.social/' 'wss://indexer.coracle.social/',
'wss://purplepag.es'
] ]
export const FOLLOWS_HISTORY_RELAY_URLS = [ export const FOLLOWS_HISTORY_RELAY_URLS = [

22
src/services/relay-selection.service.ts

@ -1,5 +1,5 @@
import { Event, kinds } from 'nostr-tools' 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 { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { collectRecipientInboxUrls, collectSenderOutboxUrls } from '@/lib/public-message-publish-relays' import { collectRecipientInboxUrls, collectSenderOutboxUrls } from '@/lib/public-message-publish-relays'
import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes' 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 no cached relay list event, fetch from relays (which will also cache it)
if (!relayListEvent) { if (!relayListEvent) {
try { try {
relayList = await client.fetchRelayList(pubkey) // Keep using client for relay list merging relayList = await Promise.race([
client.fetchRelayList(pubkey),
new Promise<TRelayList>((resolve) =>
setTimeout(
() =>
resolve(
mergeKind10243({
write: [],
read: [],
originalRelays: [],
httpRead: [],
httpWrite: [],
httpOriginalRelays: []
})
),
PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS
)
)
])
} catch (error) { } catch (error) {
logger.warn('Failed to fetch relay list from relays', { error, pubkey }) logger.warn('Failed to fetch relay list from relays', { error, pubkey })
relayList = mergeKind10243({ relayList = mergeKind10243({

Loading…
Cancel
Save