diff --git a/src/components/PostEditor/Mentions.tsx b/src/components/PostEditor/Mentions.tsx index bd64218..19f66fa 100644 --- a/src/components/PostEditor/Mentions.tsx +++ b/src/components/PostEditor/Mentions.tsx @@ -36,7 +36,8 @@ export default function Mentions({ if (_parentEventPubkey) { potentialMentions.push(_parentEventPubkey) } - setPotentialMentions(potentialMentions) + // Deduplicate the potential mentions array + setPotentialMentions(Array.from(new Set(potentialMentions))) setRemovedPubkeys((pubkeys) => { return Array.from( new Set( diff --git a/src/components/PostEditor/PostRelaySelector.tsx b/src/components/PostEditor/PostRelaySelector.tsx index 142f45f..01a4b6b 100644 --- a/src/components/PostEditor/PostRelaySelector.tsx +++ b/src/components/PostEditor/PostRelaySelector.tsx @@ -27,13 +27,15 @@ export default function PostRelaySelector({ }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() - const { relayUrls } = useCurrentRelays() + useCurrentRelays() // Keep this hook call for any side effects const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() const { pubkey, relayList } = useNostr() const [selectedRelayUrls, setSelectedRelayUrls] = useState([]) const [selectableRelays, setSelectableRelays] = useState([]) const [description, setDescription] = useState('') const [isLoading, setIsLoading] = useState(true) + const [hasManualSelection, setHasManualSelection] = useState(false) + const [previousSelectableCount, setPreviousSelectableCount] = useState(0) // Use centralized relay selection service useEffect(() => { @@ -41,7 +43,7 @@ export default function PostRelaySelector({ setIsLoading(true) try { const result = await relaySelectionService.selectRelays({ - userWriteRelays: relayList?.write || relayUrls, + userWriteRelays: relayList?.write || [], userReadRelays: relayList?.read || [], favoriteRelays, blockedRelays, @@ -53,32 +55,59 @@ export default function PostRelaySelector({ openFrom }) + const newSelectableCount = result.selectableRelays.length + const selectableRelaysChanged = newSelectableCount !== previousSelectableCount + setSelectableRelays(result.selectableRelays) - setSelectedRelayUrls(result.selectedRelays) - setDescription(result.description) + setPreviousSelectableCount(newSelectableCount) + + // Only update selected relays if: + // 1. User hasn't manually modified them, OR + // 2. New mention relays were added (selectable count changed) + if (!hasManualSelection || selectableRelaysChanged) { + setSelectedRelayUrls(result.selectedRelays) + setDescription(result.description) + // Reset manual selection flag if mentions changed + if (selectableRelaysChanged && hasManualSelection) { + setHasManualSelection(false) + } + } console.log('PostRelaySelector: Updated relay selection:', result) } catch (error) { console.error('Failed to update relay selection:', error) setSelectableRelays([]) - setSelectedRelayUrls([]) - setDescription('No relays selected') + if (!hasManualSelection) { + setSelectedRelayUrls([]) + setDescription('No relays selected') + } } finally { setIsLoading(false) } } updateRelaySelection() - }, [openFrom, _parentEvent, relayUrls, favoriteRelays, blockedRelays, relaySets, isPublicMessage, postContent, pubkey, relayList]) + }, [openFrom, _parentEvent, favoriteRelays, blockedRelays, relaySets, isPublicMessage, postContent, pubkey, relayList, hasManualSelection, previousSelectableCount]) + + // 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`) + } + }, [selectedRelayUrls, hasManualSelection, isLoading]) // Update parent component with selected relays useEffect(() => { - const isProtectedEvent = selectedRelayUrls.length > 0 && !selectedRelayUrls.some(url => relayUrls.includes(url)) + // An event is "protected" if we have selected relays that aren't the default user write relays + const userWriteRelays = relayList?.write || [] + const isProtectedEvent = selectedRelayUrls.length > 0 && !selectedRelayUrls.every(url => userWriteRelays.includes(url)) setIsProtectedEvent(isProtectedEvent) setAdditionalRelayUrls(selectedRelayUrls) - }, [selectedRelayUrls, relayUrls, setIsProtectedEvent, setAdditionalRelayUrls]) + }, [selectedRelayUrls, relayList, setIsProtectedEvent, setAdditionalRelayUrls]) const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => { + setHasManualSelection(true) if (checked) { setSelectedRelayUrls(prev => [...prev, url]) } else { @@ -87,10 +116,12 @@ export default function PostRelaySelector({ }, []) const handleSelectAll = useCallback(() => { + setHasManualSelection(true) setSelectedRelayUrls([...selectableRelays]) }, [selectableRelays]) const handleClearAll = useCallback(() => { + setHasManualSelection(true) setSelectedRelayUrls([]) }, []) diff --git a/src/constants.ts b/src/constants.ts index 3d30903..19882b3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -66,7 +66,8 @@ export const BIG_RELAY_URLS = [ 'wss://nostr.land', 'wss://nostr.wine', 'wss://nostr.sovbit.host', - 'wss://nostr21.com' + 'wss://nostr21.com', + 'wss://thecitadel.nostr1.com' ] // Optimized relay list for read operations (includes aggregator) @@ -74,6 +75,7 @@ export const FAST_READ_RELAY_URLS = [ 'wss://theforest.nostr1.com', 'wss://orly-relay.imwald.eu', 'wss://nostr.wine', + 'wss://thecitadel.nostr1.com', 'wss://aggr.nostr.land' ] diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index 27e4c81..bb4c72b 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -99,6 +99,7 @@ class RelaySelectionService { const { parentEvent, isPublicMessage, content, userPubkey } = context const contextualRelays = new Set() + try { // For replies (any kind) and public messages if (parentEvent || isPublicMessage) { @@ -116,17 +117,32 @@ class RelaySelectionService { eventHints.forEach(url => contextualRelays.add(url)) } - // For public messages, also get mentioned users' read relays - if (isPublicMessage && content && userPubkey) { - const mentions = await this.extractMentions(content, parentEvent) + // For replies and public messages, get mentioned users' relays + if (userPubkey) { + let mentions: string[] = [] + + // Always include parent event author for replies + if (parentEvent) { + mentions.push(parentEvent.pubkey) + } + + // Extract additional mentions from content if available + if (content) { + const contentMentions = await this.extractMentions(content, parentEvent) + mentions = [...new Set([...mentions, ...contentMentions])] // deduplicate + } + const mentionedPubkeys = mentions.filter(p => p !== userPubkey) + if (mentionedPubkeys.length > 0) { const mentionRelayLists = await Promise.all( mentionedPubkeys.map(async (pubkey) => { try { const relayList = await client.fetchRelayList(pubkey) - return relayList?.read || [] + // Use write relays for replies, read relays for public messages + const relayType = isPublicMessage ? 'read' : 'write' + return relayList?.[relayType] || [] } catch (error) { console.warn(`Failed to fetch relay list for ${pubkey}:`, error) return [] @@ -155,7 +171,9 @@ class RelaySelectionService { userWriteRelays, parentEvent, isPublicMessage, - openFrom + openFrom, + content, + userPubkey } = context let selectedRelays: string[] = [] @@ -177,7 +195,43 @@ class RelaySelectionService { } // For regular replies, use user's write relays + mention relays else if (parentEvent && this.isRegularReply(parentEvent)) { - selectedRelays = await this.getRegularReplyRelays(context) + // Get user's write relays + const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS + selectedRelays = userRelays.map(url => normalizeUrl(url) || url).filter(Boolean) + + // Add mention relays + if (userPubkey) { + let mentions: string[] = [] + + // Always include parent event author for replies + if (parentEvent) { + mentions.push(parentEvent.pubkey) + } + + // Extract additional mentions from content if available + if (content) { + const contentMentions = await this.extractMentions(content, parentEvent) + mentions = [...new Set([...mentions, ...contentMentions])] // deduplicate + } + + const mentionedPubkeys = mentions.filter(p => p !== userPubkey) + + if (mentionedPubkeys.length > 0) { + const mentionRelayLists = await Promise.all( + mentionedPubkeys.map(async (pubkey) => { + try { + const relayList = await client.fetchRelayList(pubkey) + return relayList?.write || [] + } catch (error) { + console.warn(`Failed to fetch relay list for ${pubkey}:`, error) + return [] + } + }) + ) + const mentionRelays = mentionRelayLists.flat().map(url => normalizeUrl(url) || url).filter(Boolean) + selectedRelays = [...selectedRelays, ...mentionRelays] + } + } } // Default: user's write relays (or fallback to fast write relays if no user relays) else { @@ -239,44 +293,6 @@ class RelaySelectionService { return Array.from(relays) } - /** - * Get relays for regular replies: user's write relays + mention relays - */ - private async getRegularReplyRelays(context: RelaySelectionContext): Promise { - const { userWriteRelays, parentEvent, content, userPubkey } = context - const relays = new Set() - - try { - // Add user's write relays - fallback to fast write relays if no user relays - const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS - userRelays.forEach(url => relays.add(normalizeUrl(url) || url)) - - // Add mentioned users' write relays - if (content && userPubkey) { - const mentions = await this.extractMentions(content, parentEvent) - const mentionedPubkeys = mentions.filter(p => p !== userPubkey) - - if (mentionedPubkeys.length > 0) { - const mentionRelayLists = await Promise.all( - mentionedPubkeys.map(async (pubkey) => { - try { - const relayList = await client.fetchRelayList(pubkey) - return relayList?.write || [] - } catch (error) { - console.warn(`Failed to fetch relay list for ${pubkey}:`, error) - return [] - } - }) - ) - mentionRelayLists.flat().forEach(url => relays.add(normalizeUrl(url) || url)) - } - } - } catch (error) { - console.error('Failed to get regular reply relays:', error) - } - - return Array.from(relays) - } /** * Check if this is a regular reply (Kind 1 or Kind 1111, not to Kind 11) @@ -329,6 +345,7 @@ class RelaySelectionService { /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g ) + if (matches) { for (const match of matches) { try {