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')}
-
-
-
-
- {t('NSFW')}
-
-
-
-
-
- {t('Proof of Work (difficulty {{minPow}})', { minPow })}
-
-
setMinPow(pow)}
- max={28}
- step={1}
- disabled={posting}
- />
+
+
+ {t('Proof of Work (difficulty {{minPow}})', { minPow })}
+
+ 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({