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 @@ @@ -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",

2
package.json

@ -1,6 +1,6 @@ @@ -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",

10
src/components/PostEditor/PostEditorAdvancedPanel.tsx

@ -76,19 +76,22 @@ export default function PostEditorAdvancedPanel({ @@ -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 (
<div className={cn(!show && 'hidden')} aria-hidden={!show}>
<div className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
{show ? (
<div>
<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>
</div>
) : null}
{showMentionsPicker && setMentions ? (
<div className="space-y-2">
@ -142,6 +145,7 @@ export default function PostEditorAdvancedPanel({ @@ -142,6 +145,7 @@ export default function PostEditorAdvancedPanel({
</div>
) : null}
{show ? (
<div className="space-y-4 pt-1 border-t border-border">
<div className="space-y-2">
<div className="flex items-center space-x-2">
@ -184,6 +188,8 @@ export default function PostEditorAdvancedPanel({ @@ -184,6 +188,8 @@ export default function PostEditorAdvancedPanel({
/>
</div>
</div>
) : null}
</div>
</div>
)
}

60
src/components/PostEditor/PostRelaySelector.tsx

@ -86,19 +86,26 @@ export default function PostRelaySelector({ @@ -86,19 +86,26 @@ export default function PostRelaySelector({
/** Auto-picked relays from {@link relaySelectionService}; used to detect manual relay-picker changes. */
const autoSelectedRelayUrlsRef = useRef<string[]>([])
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<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
return nip66Service.subscribePublicLivelyUpdated(() => {
setPublicLivelyRevision((v) => v + 1)
})
}, [])
hasManualSelectionRef.current = hasManualSelection
}, [hasManualSelection])
useEffect(() => {
previousSelectableCountRef.current = previousSelectableCount
}, [previousSelectableCount])
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)
}, 600)
})
}, [])
@ -159,6 +166,12 @@ export default function PostRelaySelector({ @@ -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({ @@ -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({ @@ -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({ @@ -247,9 +262,10 @@ export default function PostRelaySelector({
isPublicMessage,
pubkey,
relayList,
userReadRelaysForSelection,
isDiscussionReply,
contentRelaySignature,
mentions,
mentionsRelaySignature,
describeRelaySelection,
addRandomRelaysToPublish,
publicLivelyRevision,

6
src/constants.ts

@ -476,7 +476,8 @@ export const READ_ONLY_RELAY_URLS = [ @@ -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 = [ @@ -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 = [

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

@ -1,5 +1,5 @@ @@ -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 { @@ -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<TRelayList>((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({

Loading…
Cancel
Save