Browse Source

cleaned up replies and mentions

imwald
Silberengel 5 months ago
parent
commit
7a4e270ff0
  1. 76
      src/components/PostEditor/PostContent.tsx
  2. 123
      src/components/PostEditor/PostRelaySelector.tsx
  3. 79
      src/components/RelayStatusDisplay/index.tsx
  4. 4
      src/lib/publishing-feedback.tsx
  5. 14
      src/providers/NostrProvider/index.tsx
  6. 76
      src/services/client.service.ts
  7. 1
      src/types/index.d.ts

76
src/components/PostEditor/PostContent.tsx

@ -59,7 +59,7 @@ export default function PostContent({
const [isNsfw, setIsNsfw] = useState(false) const [isNsfw, setIsNsfw] = useState(false)
const [isPoll, setIsPoll] = useState(false) const [isPoll, setIsPoll] = useState(false)
const [isPublicMessage, setIsPublicMessage] = useState(false) const [isPublicMessage, setIsPublicMessage] = useState(false)
const [publicMessageRecipients, setPublicMessageRecipients] = useState<string[]>([]) const [extractedMentions, setExtractedMentions] = useState<string[]>([])
const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([]) const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [isHighlight, setIsHighlight] = useState(false) const [isHighlight, setIsHighlight] = useState(false)
@ -82,7 +82,7 @@ export default function PostContent({
!posting && !posting &&
!uploadProgresses.length && !uploadProgresses.length &&
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) && (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) &&
(!isPublicMessage || publicMessageRecipients.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) && (!isPublicMessage || extractedMentions.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) &&
(!isProtectedEvent || additionalRelayUrls.length > 0) && (!isProtectedEvent || additionalRelayUrls.length > 0) &&
(!isHighlight || highlightData.sourceValue.trim() !== '') (!isHighlight || highlightData.sourceValue.trim() !== '')
) )
@ -96,7 +96,7 @@ export default function PostContent({
isPoll, isPoll,
pollCreateData, pollCreateData,
isPublicMessage, isPublicMessage,
publicMessageRecipients, extractedMentions,
parentEvent?.kind, parentEvent?.kind,
isProtectedEvent, isProtectedEvent,
additionalRelayUrls, additionalRelayUrls,
@ -151,25 +151,20 @@ export default function PostContent({
// For now, we'll use the nostr mentions and show that we detected @ mentions // For now, we'll use the nostr mentions and show that we detected @ mentions
// In a real implementation, you'd resolve @ mentions to pubkeys // In a real implementation, you'd resolve @ mentions to pubkeys
setPublicMessageRecipients(nostrPubkeys) setExtractedMentions(nostrPubkeys)
} catch (error) { } catch (error) {
console.error('Error extracting mentions:', error) console.error('Error extracting mentions:', error)
setPublicMessageRecipients([]) setExtractedMentions([])
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!isPublicMessage) {
setPublicMessageRecipients([])
return
}
if (!text) { if (!text) {
setPublicMessageRecipients([]) setExtractedMentions([])
return return
} }
// Debounce the mention extraction // Debounce the mention extraction for all posts (not just public messages)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
extractMentionsFromContent(text) extractMentionsFromContent(text)
}, 300) }, 300)
@ -177,7 +172,7 @@ export default function PostContent({
return () => { return () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
} }
}, [text, isPublicMessage, extractMentionsFromContent]) }, [text, extractMentionsFromContent])
const post = async (e?: React.MouseEvent) => { const post = async (e?: React.MouseEvent) => {
e?.stopPropagation() e?.stopPropagation()
@ -197,9 +192,11 @@ export default function PostContent({
// }) // })
setPosting(true) setPosting(true)
let draftEvent: any = null
let newEvent: any = null
try { try {
let draftEvent
if (isHighlight) { if (isHighlight) {
// For highlights, pass the original sourceValue which contains the full identifier // For highlights, pass the original sourceValue which contains the full identifier
// The createHighlightDraftEvent function will parse it correctly // The createHighlightDraftEvent function will parse it correctly
@ -215,7 +212,7 @@ export default function PostContent({
} }
) )
} else if (isPublicMessage) { } else if (isPublicMessage) {
draftEvent = await createPublicMessageDraftEvent(text, publicMessageRecipients, { draftEvent = await createPublicMessageDraftEvent(text, extractedMentions, {
addClientTag, addClientTag,
isNsfw isNsfw
}) })
@ -245,10 +242,11 @@ export default function PostContent({
} }
// console.log('Publishing draft event:', draftEvent) // console.log('Publishing draft event:', draftEvent)
const newEvent = await publish(draftEvent, { newEvent = await publish(draftEvent, {
specifiedRelayUrls: additionalRelayUrls.length > 0 ? additionalRelayUrls : undefined, specifiedRelayUrls: additionalRelayUrls.length > 0 ? additionalRelayUrls : undefined,
additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls, additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls,
minPow minPow,
disableFallbacks: additionalRelayUrls.length > 0 // Don't use fallbacks if user explicitly selected relays
}) })
// console.log('Published event:', newEvent) // console.log('Published event:', newEvent)
@ -283,6 +281,7 @@ export default function PostContent({
showSimplePublishSuccess(parentEvent ? t('Reply published') : t('Post published')) showSimplePublishSuccess(parentEvent ? t('Reply published') : t('Post published'))
} }
// Full success - clean up and close
postEditorCache.clearPostCache({ defaultContent, parentEvent }) postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent) deleteDraftEventCache(draftEvent)
addReplies([newEvent]) addReplies([newEvent])
@ -313,6 +312,15 @@ export default function PostContent({
(parentEvent ? t('Failed to publish reply') : t('Failed to publish post')), (parentEvent ? t('Failed to publish reply') : t('Failed to publish post')),
duration: 6000 duration: 6000
}) })
// Handle partial success
if (successCount > 0) {
// Clean up and close on partial success
postEditorCache.clearPostCache({ defaultContent, parentEvent })
if (draftEvent) deleteDraftEventCache(draftEvent)
if (newEvent) addReplies([newEvent])
close()
}
} else { } else {
// Use standard publishing error feedback for cases without relay statuses // Use standard publishing error feedback for cases without relay statuses
if (error instanceof AggregateError) { if (error instanceof AggregateError) {
@ -323,6 +331,7 @@ export default function PostContent({
} else { } else {
showPublishingError('Failed to publish') showPublishingError('Failed to publish')
} }
// Don't close form on complete failure - let user try again
} }
} finally { } finally {
setPosting(false) setPosting(false)
@ -443,12 +452,12 @@ export default function PostContent({
<Mentions <Mentions
content={text} content={text}
parentEvent={undefined} parentEvent={undefined}
mentions={publicMessageRecipients} mentions={extractedMentions}
setMentions={setPublicMessageRecipients} setMentions={setExtractedMentions}
/> />
{publicMessageRecipients.length > 0 ? ( {extractedMentions.length > 0 ? (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{t('Recipients detected from your message:')} {publicMessageRecipients.length} {t('Recipients detected from your message:')} {extractedMentions.length}
</div> </div>
) : ( ) : (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
@ -486,14 +495,23 @@ export default function PostContent({
</div> </div>
))} ))}
{!isPoll && ( {!isPoll && (
<PostRelaySelector <>
setIsProtectedEvent={setIsProtectedEvent} {console.log('PostContent: Rendering PostRelaySelector with:', {
setAdditionalRelayUrls={setAdditionalRelayUrls} extractedMentions,
parentEvent={parentEvent} isPublicMessage,
openFrom={openFrom} isPoll,
content={text} textLength: text.length
isPublicMessage={isPublicMessage} })}
/> <PostRelaySelector
setIsProtectedEvent={setIsProtectedEvent}
setAdditionalRelayUrls={setAdditionalRelayUrls}
parentEvent={parentEvent}
openFrom={openFrom}
content={text}
isPublicMessage={isPublicMessage}
mentions={extractedMentions}
/>
</>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">

123
src/components/PostEditor/PostRelaySelector.tsx

@ -5,7 +5,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Check } from 'lucide-react' import { Check } from 'lucide-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' import { Dispatch, SetStateAction, useCallback, useEffect, useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon' import RelayIcon from '../RelayIcon'
import relaySelectionService from '@/services/relay-selection.service' import relaySelectionService from '@/services/relay-selection.service'
@ -16,7 +16,8 @@ export default function PostRelaySelector({
setIsProtectedEvent, setIsProtectedEvent,
setAdditionalRelayUrls, setAdditionalRelayUrls,
content: postContent = '', content: postContent = '',
isPublicMessage = false isPublicMessage = false,
mentions = []
}: { }: {
parentEvent?: NostrEvent parentEvent?: NostrEvent
openFrom?: string[] openFrom?: string[]
@ -24,6 +25,7 @@ export default function PostRelaySelector({
setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>> setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
content?: string content?: string
isPublicMessage?: boolean isPublicMessage?: boolean
mentions?: string[]
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -36,8 +38,38 @@ export default function PostRelaySelector({
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [hasManualSelection, setHasManualSelection] = useState(false) const [hasManualSelection, setHasManualSelection] = useState(false)
const [previousSelectableCount, setPreviousSelectableCount] = useState(0) const [previousSelectableCount, setPreviousSelectableCount] = useState(0)
const [previousMentions, setPreviousMentions] = useState<string[]>([])
// Use centralized relay selection service // Initialize previousMentions with the initial mentions value
useEffect(() => {
setPreviousMentions(mentions)
}, []) // Only run once on mount
// For discussion replies, content doesn't affect relay selection
// Check if this is a reply to a discussion by looking for "K" tag with "11"
const isDiscussionReply = useMemo(() => {
if (!_parentEvent) return false
// Direct reply to discussion
if (_parentEvent.kind === 11) return true
// Check if parent event has "K" tag containing "11" (discussion root kind)
const eventTags = _parentEvent.tags || []
const kindTag = eventTags.find(([tagName]) => tagName === 'K')
if (kindTag && kindTag[1] === '11') {
return true
}
return false
}, [_parentEvent])
// Memoize arrays to prevent unnecessary re-renders
const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays])
const memoizedBlockedRelays = useMemo(() => blockedRelays, [blockedRelays])
const memoizedRelaySets = useMemo(() => relaySets, [relaySets])
const memoizedOpenFrom = useMemo(() => openFrom, [openFrom])
// Use centralized relay selection service - only for non-content dependencies
useEffect(() => { useEffect(() => {
const updateRelaySelection = async () => { const updateRelaySelection = async () => {
setIsLoading(true) setIsLoading(true)
@ -45,14 +77,14 @@ export default function PostRelaySelector({
const result = await relaySelectionService.selectRelays({ const result = await relaySelectionService.selectRelays({
userWriteRelays: relayList?.write || [], userWriteRelays: relayList?.write || [],
userReadRelays: relayList?.read || [], userReadRelays: relayList?.read || [],
favoriteRelays, favoriteRelays: memoizedFavoriteRelays,
blockedRelays, blockedRelays: memoizedBlockedRelays,
relaySets, relaySets: memoizedRelaySets,
parentEvent: _parentEvent, parentEvent: _parentEvent,
isPublicMessage, isPublicMessage,
content: postContent, content: isDiscussionReply ? '' : postContent, // Don't use content for discussion replies
userPubkey: pubkey || undefined, userPubkey: pubkey || undefined,
openFrom openFrom: memoizedOpenFrom
}) })
const newSelectableCount = result.selectableRelays.length const newSelectableCount = result.selectableRelays.length
@ -63,17 +95,16 @@ export default function PostRelaySelector({
// Only update selected relays if: // Only update selected relays if:
// 1. User hasn't manually modified them, OR // 1. User hasn't manually modified them, OR
// 2. New mention relays were added (selectable count changed) // 2. Selectable relays changed
if (!hasManualSelection || selectableRelaysChanged) { if (!hasManualSelection || selectableRelaysChanged) {
setSelectedRelayUrls(result.selectedRelays) setSelectedRelayUrls(result.selectedRelays)
setDescription(result.description) setDescription(result.description)
// Reset manual selection flag if mentions changed // Reset manual selection flag if relays changed
if (selectableRelaysChanged && hasManualSelection) { if (selectableRelaysChanged && hasManualSelection) {
setHasManualSelection(false) setHasManualSelection(false)
} }
} }
console.log('PostRelaySelector: Updated relay selection:', result)
} catch (error) { } catch (error) {
console.error('Failed to update relay selection:', error) console.error('Failed to update relay selection:', error)
setSelectableRelays([]) setSelectableRelays([])
@ -87,7 +118,75 @@ export default function PostRelaySelector({
} }
updateRelaySelection() updateRelaySelection()
}, [openFrom, _parentEvent, favoriteRelays, blockedRelays, relaySets, isPublicMessage, postContent, pubkey, relayList]) }, [memoizedOpenFrom, _parentEvent, memoizedFavoriteRelays, memoizedBlockedRelays, memoizedRelaySets, isPublicMessage, pubkey, relayList, isDiscussionReply])
// Separate effect for mention changes in non-discussion replies
useEffect(() => {
console.log('PostRelaySelector: Mentions effect triggered', {
mentions,
previousMentions,
isDiscussionReply,
mentionsLength: mentions.length,
previousMentionsLength: previousMentions.length
})
if (isDiscussionReply) {
console.log('PostRelaySelector: Skipping mention update - is discussion reply')
return // Skip for discussion replies
}
const mentionsChanged = JSON.stringify(mentions) !== JSON.stringify(previousMentions)
console.log('PostRelaySelector: Mentions changed?', mentionsChanged)
if (mentionsChanged) {
console.log('PostRelaySelector: Updating relay selection due to mention changes')
setPreviousMentions(mentions)
// Update relay selection when mentions change
const updateRelaySelection = async () => {
setIsLoading(true)
try {
const result = await relaySelectionService.selectRelays({
userWriteRelays: relayList?.write || [],
userReadRelays: relayList?.read || [],
favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets,
parentEvent: _parentEvent,
isPublicMessage,
content: postContent,
userPubkey: pubkey || undefined,
openFrom: memoizedOpenFrom
})
const newSelectableCount = result.selectableRelays.length
const selectableRelaysChanged = newSelectableCount !== previousSelectableCount
setSelectableRelays(result.selectableRelays)
setPreviousSelectableCount(newSelectableCount)
// Only update selected relays if:
// 1. User hasn't manually modified them, OR
// 2. Selectable relays changed
if (!hasManualSelection || selectableRelaysChanged) {
setSelectedRelayUrls(result.selectedRelays)
setDescription(result.description)
// Reset manual selection flag if relays changed
if (selectableRelaysChanged && hasManualSelection) {
setHasManualSelection(false)
}
}
} catch (error) {
console.error('Failed to update relay selection:', error)
} finally {
setIsLoading(false)
}
}
updateRelaySelection()
}
}, [mentions, isDiscussionReply, memoizedFavoriteRelays, memoizedBlockedRelays, memoizedRelaySets, _parentEvent, isPublicMessage, pubkey, relayList, memoizedOpenFrom, previousSelectableCount, hasManualSelection, postContent])
// Update description when selected relays change due to manual selection // Update description when selected relays change due to manual selection
useEffect(() => { useEffect(() => {

79
src/components/RelayStatusDisplay/index.tsx

@ -1,6 +1,49 @@
import { Check, X } from 'lucide-react' import { Check, X } from 'lucide-react'
import { simplifyUrl } from '@/lib/url' import { simplifyUrl } from '@/lib/url'
/**
* Format relay error messages to be more user-friendly
*/
function formatRelayError(error: string): string {
const lowerError = error.toLowerCase()
// Handle confusing relay error messages
if (lowerError.includes('blocked') && lowerError.includes('event marked as protected')) {
return 'Relay rejected this content (may be due to content policy)'
}
if (lowerError.includes('blocked')) {
return 'Relay blocked this content'
}
if (lowerError.includes('rate limit') || lowerError.includes('rate-limit')) {
return 'Rate limited - please wait before trying again'
}
if (lowerError.includes('auth') && lowerError.includes('required')) {
return 'Authentication required'
}
if (lowerError.includes('writes disabled') || lowerError.includes('write disabled')) {
return 'Relay has temporarily disabled writes'
}
if (lowerError.includes('invalid key')) {
return 'Authentication failed - invalid key'
}
if (lowerError.includes('timeout')) {
return 'Request timed out'
}
if (lowerError.includes('connection') && lowerError.includes('refused')) {
return 'Connection refused by relay'
}
// Return original error if no specific formatting applies
return error
}
interface RelayStatus { interface RelayStatus {
url: string url: string
success: boolean success: boolean
@ -31,13 +74,13 @@ export default function RelayStatusDisplay({
Published to {successCount} of {totalCount} relays Published to {successCount} of {totalCount} relays
</div> </div>
<div className="space-y-1"> <div className="space-y-1 max-w-full">
{relayStatuses.map((status, index) => ( {relayStatuses.map((status, index) => (
<div <div
key={index} key={index}
className="flex items-center gap-2 text-sm" className="flex items-start gap-2 text-sm min-w-0"
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0 mt-0.5">
{status.success ? ( {status.success ? (
<Check className="h-4 w-4 text-green-500" /> <Check className="h-4 w-4 text-green-500" />
) : ( ) : (
@ -45,23 +88,25 @@ export default function RelayStatusDisplay({
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-2"> <div className="flex flex-col gap-1">
<span className="font-mono text-xs truncate"> <div className="flex items-center gap-2 min-w-0">
{simplifyUrl(status.url)} <span className="font-mono text-xs break-all">
</span> {simplifyUrl(status.url)}
{status.authAttempted && !status.success && (
<span className="text-xs text-red-600 dark:text-red-400">
(auth failed)
</span> </span>
{status.authAttempted && !status.success && (
<span className="text-xs text-red-600 dark:text-red-400 flex-shrink-0">
(auth failed)
</span>
)}
</div>
{!status.success && status.error && (
<div className="text-xs text-red-600 dark:text-red-400 break-words">
{formatRelayError(status.error)}
</div>
)} )}
</div> </div>
{!status.success && status.error && (
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
{status.error}
</div>
)}
</div> </div>
</div> </div>
))} ))}

4
src/lib/publishing-feedback.tsx

@ -43,7 +43,7 @@ export function showPublishingFeedback(
const toastFunction = isSuccess ? toast.success : toast.error const toastFunction = isSuccess ? toast.success : toast.error
toastFunction( toastFunction(
<div className="w-full"> <div className="w-full min-w-0">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<CheckCircle2 className={`w-5 h-5 ${isSuccess ? 'text-green-500' : 'text-red-500'}`} /> <CheckCircle2 className={`w-5 h-5 ${isSuccess ? 'text-green-500' : 'text-red-500'}`} />
<div className="font-semibold">{message}</div> <div className="font-semibold">{message}</div>
@ -59,7 +59,7 @@ export function showPublishingFeedback(
</div>, </div>,
{ {
duration, duration,
className: 'max-w-md' className: 'max-w-lg w-full'
} }
) )
} }

14
src/providers/NostrProvider/index.tsx

@ -772,13 +772,25 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const relays = await client.determineTargetRelays(event, options) const relays = await client.determineTargetRelays(event, options)
try { try {
const publishResult = await client.publishEvent(relays, event) const publishResult = await client.publishEvent(relays, event, {
disableFallbacks: options.disableFallbacks
})
// Store relay status for display // Store relay status for display
if (publishResult.relayStatuses.length > 0) { if (publishResult.relayStatuses.length > 0) {
(event as any).relayStatuses = publishResult.relayStatuses (event as any).relayStatuses = publishResult.relayStatuses
} }
// If publishing failed completely, throw an error so the form doesn't close
if (!publishResult.success) {
const error = new AggregateError(
publishResult.relayStatuses.map(s => new Error(s.error || 'Failed')),
'Failed to publish to any relay'
)
;(error as any).relayStatuses = publishResult.relayStatuses
throw error
}
return event return event
} catch (error) { } catch (error) {
// Check for authentication-related errors // Check for authentication-related errors

76
src/services/client.service.ts

@ -100,10 +100,16 @@ class ClientService extends EventTarget {
if (specifiedRelayUrls?.length && (event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT)) { if (specifiedRelayUrls?.length && (event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT)) {
// For discussion replies, try ONLY the specified relay first // For discussion replies, try ONLY the specified relay first
// The fallback will be handled in the publishing logic if this fails // The fallback will be handled in the publishing logic if this fails
relays = specifiedRelayUrls // But still filter blocked relays from specified relays
if (this.pubkey) {
const blockedRelays = await this.fetchBlockedRelays(this.pubkey)
relays = this.filterBlockedRelays(specifiedRelayUrls, blockedRelays)
} else {
relays = specifiedRelayUrls
}
return relays return relays
} else if (specifiedRelayUrls?.length) { } else if (specifiedRelayUrls?.length) {
// For non-discussion events, use specified relays as-is // For non-discussion events, use specified relays (will be filtered below)
relays = specifiedRelayUrls relays = specifiedRelayUrls
} else { } else {
const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
@ -155,10 +161,16 @@ class ClientService extends EventTarget {
relays.push(...FAST_WRITE_RELAY_URLS) relays.push(...FAST_WRITE_RELAY_URLS)
} }
// Filter out blocked relays
if (this.pubkey) {
const blockedRelays = await this.fetchBlockedRelays(this.pubkey)
relays = this.filterBlockedRelays(relays, blockedRelays)
}
return relays return relays
} }
async publishEvent(relayUrls: string[], event: NEvent): Promise<{ async publishEvent(relayUrls: string[], event: NEvent, options: { disableFallbacks?: boolean } = {}): Promise<{
success: boolean success: boolean
relayStatuses: Array<{ relayStatuses: Array<{
url: string url: string
@ -170,7 +182,8 @@ class ClientService extends EventTarget {
totalCount: number totalCount: number
}> { }> {
// Special handling for discussion events: try relay hint first, then fallback // Special handling for discussion events: try relay hint first, then fallback
if ((event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT) && relayUrls.length === 1) { // BUT: if disableFallbacks is true (user explicitly selected relays), don't use fallbacks
if ((event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT) && relayUrls.length === 1 && !options.disableFallbacks) {
try { try {
// Try publishing to the relay hint first // Try publishing to the relay hint first
const result = await this._publishToRelays(relayUrls, event) const result = await this._publishToRelays(relayUrls, event)
@ -180,9 +193,11 @@ class ClientService extends EventTarget {
return result return result
} }
// If failed, try fallback relays // If failed, try fallback relays (filtering out blocked relays)
const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] } const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] }
const fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS const blockedRelays = this.pubkey ? await this.fetchBlockedRelays(this.pubkey) : []
let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays)
console.log('Relay hint failed, trying fallback relays:', fallbackRelays) console.log('Relay hint failed, trying fallback relays:', fallbackRelays)
const fallbackResult = await this._publishToRelays(fallbackRelays, event) const fallbackResult = await this._publishToRelays(fallbackRelays, event)
@ -208,7 +223,9 @@ class ClientService extends EventTarget {
} }
const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] } const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] }
const fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS const blockedRelays = this.pubkey ? await this.fetchBlockedRelays(this.pubkey) : []
let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays)
console.log('Trying fallback relays:', fallbackRelays) console.log('Trying fallback relays:', fallbackRelays)
const fallbackResult = await this._publishToRelays(fallbackRelays, event) const fallbackResult = await this._publishToRelays(fallbackRelays, event)
@ -243,6 +260,13 @@ class ClientService extends EventTarget {
}> { }> {
const uniqueRelayUrls = this.optimizeRelaySelection(Array.from(new Set(relayUrls))) const uniqueRelayUrls = this.optimizeRelaySelection(Array.from(new Set(relayUrls)))
// Handle case where no relays are available (all filtered out)
if (uniqueRelayUrls.length === 0) {
const error = new Error('No relays available for publishing - all relays may be blocked or unavailable')
;(error as any).relayStatuses = []
throw error
}
const relayStatuses: Array<{ const relayStatuses: Array<{
url: string url: string
success: boolean success: boolean
@ -1515,6 +1539,44 @@ class ClientService extends EventTarget {
await this.updateReplaceableEventFromBigRelaysCache(event) await this.updateReplaceableEventFromBigRelaysCache(event)
} }
/**
* Fetch blocked relays from IndexedDB
*/
async fetchBlockedRelays(pubkey: string): Promise<string[]> {
try {
const blockedRelaysEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.BLOCKED_RELAYS)
if (!blockedRelaysEvent) {
return []
}
// Extract relay URLs from the relay tags
const relayUrls = blockedRelaysEvent.tags
.filter(([tagName]) => tagName === 'relay')
.map(([, url]) => url)
.filter(Boolean)
return relayUrls
} catch (error) {
console.error('Failed to fetch blocked relays:', error)
return []
}
}
/**
* Filter out blocked relays from a relay list
*/
private filterBlockedRelays(relays: string[], blockedRelays: string[]): string[] {
if (!blockedRelays || blockedRelays.length === 0) {
return relays
}
const normalizedBlocked = blockedRelays.map(url => normalizeUrl(url) || url)
return relays.filter(relay => {
const normalizedRelay = normalizeUrl(relay) || relay
return !normalizedBlocked.includes(normalizedRelay)
})
}
/** =========== Replaceable event from big relays dataloader =========== */ /** =========== Replaceable event from big relays dataloader =========== */
private replaceableEventFromBigRelaysDataloader = new DataLoader< private replaceableEventFromBigRelaysDataloader = new DataLoader<

1
src/types/index.d.ts vendored

@ -123,6 +123,7 @@ export type TPublishOptions = {
specifiedRelayUrls?: string[] specifiedRelayUrls?: string[]
additionalRelayUrls?: string[] additionalRelayUrls?: string[]
minPow?: number minPow?: number
disableFallbacks?: boolean // If true, don't use fallback relays when publishing fails
} }
export type TNoteListMode = 'posts' | 'postsAndReplies' | 'you' export type TNoteListMode = 'posts' | 'postsAndReplies' | 'you'

Loading…
Cancel
Save