Browse Source

added relay publishing info

imwald
Silberengel 5 months ago
parent
commit
a0bc509b10
  1. 133
      src/components/PostEditor/PostContent.tsx
  2. 71
      src/components/RelayStatusDisplay/index.tsx
  3. 12
      src/providers/NostrProvider/index.tsx
  4. 245
      src/services/client.service.ts

133
src/components/PostEditor/PostContent.tsx

@ -27,6 +27,7 @@ import PostOptions from './PostOptions'
import PostRelaySelector from './PostRelaySelector' import PostRelaySelector from './PostRelaySelector'
import PostTextarea, { TPostTextareaHandle } from './PostTextarea' import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
import Uploader from './Uploader' import Uploader from './Uploader'
import RelayStatusDisplay from '@/components/RelayStatusDisplay'
export default function PostContent({ export default function PostContent({
defaultContent = '', defaultContent = '',
@ -64,6 +65,14 @@ export default function PostContent({
relays: [] relays: []
}) })
const [minPow, setMinPow] = useState(0) const [minPow, setMinPow] = useState(0)
const [relayStatuses, setRelayStatuses] = useState<Array<{
url: string
success: boolean
error?: string
authAttempted?: boolean
}>>([])
const [showRelayStatus, setShowRelayStatus] = useState(false)
const [lastPublishedEvent, setLastPublishedEvent] = useState<Event | null>(null)
const isFirstRender = useRef(true) const isFirstRender = useRef(true)
const canPost = useMemo(() => { const canPost = useMemo(() => {
const result = ( const result = (
@ -239,24 +248,86 @@ export default function PostContent({
minPow minPow
}) })
// console.log('Published event:', newEvent) // console.log('Published event:', newEvent)
postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent) // Check if we have relay status information
addReplies([newEvent]) console.log('Published event:', newEvent)
close() console.log('Relay statuses:', (newEvent as any).relayStatuses)
if ((newEvent as any).relayStatuses) {
setRelayStatuses((newEvent as any).relayStatuses)
setLastPublishedEvent(newEvent)
setShowRelayStatus(true)
// Show success message with relay count
const successCount = (newEvent as any).relayStatuses.filter((s: any) => s.success).length
const totalCount = (newEvent as any).relayStatuses.length
toast.success(t('Post successful - published to {{count}} of {{total}} relays', {
count: successCount,
total: totalCount
}), { duration: 4000 })
// Don't close immediately if we have relay status to show
setTimeout(() => {
postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent)
addReplies([newEvent])
close()
}, 8000) // Give user more time to see the relay status
} else {
toast.success(t('Post successful'), { duration: 2000 })
postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent)
addReplies([newEvent])
close()
}
} catch (error) { } catch (error) {
const errors = error instanceof AggregateError ? error.errors : [error] console.error('Publishing error:', error)
errors.forEach((err) => {
toast.error( // Handle different types of errors with user-friendly messages
`${t('Failed to post')}: ${err instanceof Error ? err.message : String(err)}`, let errorMessage = t('Failed to post')
{ duration: 10_000 }
if (error instanceof Error) {
if (error.message.includes('timeout')) {
errorMessage = t('Posting timed out. Your post may have been published to some relays.')
} else if (error.message.includes('auth-required') || error.message.includes('auth required')) {
errorMessage = t('Some relays require authentication. Please try again or use different relays.')
} else if (error.message.includes('blocked')) {
errorMessage = t('You are blocked from posting to some relays.')
} else if (error.message.includes('rate limit')) {
errorMessage = t('Rate limited. Please wait before trying again.')
} else if (error.message.includes('writes disabled')) {
errorMessage = t('Some relays have temporarily disabled writes.')
} else {
errorMessage = `${t('Failed to post')}: ${error.message}`
}
} else if (error instanceof AggregateError) {
// Handle multiple relay failures
const hasAuthErrors = error.errors.some(err =>
err instanceof Error && err.message.includes('auth-required')
) )
console.error(err) const hasBlockedErrors = error.errors.some(err =>
}) err instanceof Error && err.message.includes('blocked')
)
const hasWriteDisabledErrors = error.errors.some(err =>
err instanceof Error && err.message.includes('writes disabled')
)
if (hasAuthErrors) {
errorMessage = t('Some relays require authentication. Your post may have been published to other relays.')
} else if (hasBlockedErrors) {
errorMessage = t('You are blocked from some relays. Your post may have been published to other relays.')
} else if (hasWriteDisabledErrors) {
errorMessage = t('Some relays have disabled writes. Your post may have been published to other relays.')
} else {
errorMessage = t('Failed to publish to some relays. Your post may have been published to other relays.')
}
}
toast.error(errorMessage, { duration: 8000 })
return return
} finally { } finally {
setPosting(false) setPosting(false)
} }
toast.success(t('Post successful'), { duration: 2000 })
}) })
} }
@ -507,6 +578,44 @@ export default function PostContent({
{parentEvent ? t('Reply') : t('Post')} {parentEvent ? t('Reply') : t('Post')}
</Button> </Button>
</div> </div>
{showRelayStatus && relayStatuses.length > 0 && (
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border-2 border-blue-200 dark:border-blue-800">
<div className="mb-3">
<h3 className="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-1">
📡 Publishing Results
</h3>
<p className="text-xs text-blue-600 dark:text-blue-300">
Your post has been published. Here's the status for each relay:
</p>
</div>
<RelayStatusDisplay
relayStatuses={relayStatuses}
successCount={relayStatuses.filter(s => s.success).length}
totalCount={relayStatuses.length}
/>
<div className="mt-3 flex justify-between items-center">
<div className="text-xs text-blue-600 dark:text-blue-300">
This dialog will close automatically in a few seconds
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowRelayStatus(false)
if (lastPublishedEvent) {
postEditorCache.clearPostCache({ defaultContent, parentEvent })
// Note: draftEvent is not available here, but that's okay since the event is already published
addReplies([lastPublishedEvent])
}
close()
}}
>
{t('Close')}
</Button>
</div>
</div>
)}
</div> </div>
) )
} }

71
src/components/RelayStatusDisplay/index.tsx

@ -0,0 +1,71 @@
import { Check, X, AlertCircle } from 'lucide-react'
import { simplifyUrl } from '@/lib/url'
interface RelayStatus {
url: string
success: boolean
error?: string
authAttempted?: boolean
}
interface RelayStatusDisplayProps {
relayStatuses: RelayStatus[]
successCount: number
totalCount: number
className?: string
}
export default function RelayStatusDisplay({
relayStatuses,
successCount,
totalCount,
className = ''
}: RelayStatusDisplayProps) {
if (relayStatuses.length === 0) {
return null
}
return (
<div className={`space-y-2 ${className}`}>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Published to {successCount} of {totalCount} relays
</div>
<div className="space-y-1">
{relayStatuses.map((status, index) => (
<div
key={index}
className="flex items-center gap-2 text-sm"
>
<div className="flex-shrink-0">
{status.success ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<X className="h-4 w-4 text-red-500" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-xs truncate">
{simplifyUrl(status.url)}
</span>
{status.authAttempted && (
<div title="Authentication attempted">
<AlertCircle className="h-3 w-3 text-amber-500" />
</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>
)
}

12
src/providers/NostrProvider/index.tsx

@ -631,7 +631,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const relays = await client.determineTargetRelays(event, options) const relays = await client.determineTargetRelays(event, options)
await client.publishEvent(relays, event) const publishResult = await client.publishEvent(relays, event)
console.log('Publish result:', publishResult)
// Store relay status for display
if (publishResult.relayStatuses.length > 0) {
// We'll pass this to the UI components that need it
(event as any).relayStatuses = publishResult.relayStatuses
console.log('Attached relay statuses to event:', (event as any).relayStatuses)
}
return event return event
} }

245
src/services/client.service.ts

@ -153,7 +153,17 @@ class ClientService extends EventTarget {
return relays return relays
} }
async publishEvent(relayUrls: string[], event: NEvent) { async publishEvent(relayUrls: string[], event: NEvent): Promise<{
success: boolean
relayStatuses: Array<{
url: string
success: boolean
error?: string
authAttempted?: boolean
}>
successCount: number
totalCount: number
}> {
const uniqueRelayUrls = this.optimizeRelaySelection(Array.from(new Set(relayUrls))) const uniqueRelayUrls = this.optimizeRelaySelection(Array.from(new Set(relayUrls)))
console.log(`Publishing kind ${event.kind} event to ${uniqueRelayUrls.length} relays`) console.log(`Publishing kind ${event.kind} event to ${uniqueRelayUrls.length} relays`)
// if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { // if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
@ -166,70 +176,173 @@ class ClientService extends EventTarget {
// }) // })
// } // }
await new Promise<void>((resolve, reject) => { const relayStatuses: Array<{
url: string
success: boolean
error?: string
authAttempted?: boolean
}> = []
const result = await new Promise<{
success: boolean
relayStatuses: typeof relayStatuses
successCount: number
totalCount: number
}>((resolve, reject) => {
let successCount = 0 let successCount = 0
let finishedCount = 0 let finishedCount = 0
const errors: { url: string; error: any }[] = [] const errors: { url: string; error: any }[] = []
let resolved = false
const checkCompletion = () => {
if (resolved) return
// If one third of the relays have accepted the event, consider it a success
const isSuccess = successCount >= Math.max(1, Math.ceil(uniqueRelayUrls.length / 3))
if (isSuccess && !resolved) {
console.log(`✓ Publishing successful (${successCount}/${uniqueRelayUrls.length} relays)`)
this.emitNewEvent(event)
resolved = true
resolve({
success: true,
relayStatuses,
successCount,
totalCount: uniqueRelayUrls.length
})
return
}
if (finishedCount >= uniqueRelayUrls.length && !resolved) {
if (successCount > 0) {
console.log(`✓ Publishing successful (${successCount}/${uniqueRelayUrls.length} relays)`)
this.emitNewEvent(event)
resolved = true
resolve({
success: true,
relayStatuses,
successCount,
totalCount: uniqueRelayUrls.length
})
} else {
console.log(`✗ Publishing failed (0/${uniqueRelayUrls.length} relays)`)
resolved = true
reject(
new AggregateError(
errors.map(
({ url, error }) =>
new Error(
`${url}: ${error instanceof Error ? error.message : String(error)}`
)
)
)
)
}
}
}
// Add overall timeout to prevent hanging
const overallTimeout = setTimeout(() => {
if (!resolved) {
console.log(`⚠ Publishing timeout after 15s (${successCount}/${uniqueRelayUrls.length} relays succeeded)`)
resolved = true
if (successCount > 0) {
this.emitNewEvent(event)
resolve({
success: true,
relayStatuses,
successCount,
totalCount: uniqueRelayUrls.length
})
} else {
reject(new Error('Publishing timeout - no relays responded in time'))
}
}
}, 15_000) // 15 second overall timeout
Promise.allSettled( Promise.allSettled(
uniqueRelayUrls.map(async (url) => { uniqueRelayUrls.map(async (url) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this const that = this
const relay = await this.pool.ensureRelay(url)
relay.publishTimeout = 8_000 // 8s try {
return relay const relay = await this.pool.ensureRelay(url)
.publish(event) relay.publishTimeout = 8_000 // 8s
.then(() => {
console.log(`✓ Published to ${url}`) await relay.publish(event)
this.trackEventSeenOn(event.id, relay) console.log(`✓ Published to ${url}`)
successCount++ this.trackEventSeenOn(event.id, relay)
successCount++
finishedCount++
relayStatuses.push({
url,
success: true
}) })
.catch((error) => {
console.log(`✗ Failed to publish to ${url}:`, error.message) checkCompletion()
if ( } catch (error) {
error instanceof Error && const errorMessage = error instanceof Error ? error.message : String(error)
error.message.startsWith('auth-required') && console.log(`✗ Failed to publish to ${url}:`, errorMessage)
!!that.signer
) { // Check if this is an auth-required error and we have a signer
if (
error instanceof Error &&
error.message.startsWith('auth-required') &&
!!that.signer
) {
try {
console.log(`Attempting auth for ${url}`) console.log(`Attempting auth for ${url}`)
return relay const relay = await this.pool.ensureRelay(url)
.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt)) await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt))
.then(() => relay.publish(event)) await relay.publish(event)
} else { console.log(`✓ Published to ${url} after auth`)
errors.push({ url, error }) this.trackEventSeenOn(event.id, relay)
successCount++
finishedCount++
relayStatuses.push({
url,
success: true,
authAttempted: true
})
checkCompletion()
} catch (authError) {
const authErrorMessage = authError instanceof Error ? authError.message : String(authError)
console.log(`✗ Auth failed for ${url}:`, authErrorMessage)
errors.push({ url, error: authError })
finishedCount++
relayStatuses.push({
url,
success: false,
error: authErrorMessage,
authAttempted: true
})
checkCompletion()
} }
}) } else {
.finally(() => { // For permanent errors like "blocked" or "writes disabled", don't retry
errors.push({ url, error })
finishedCount++ finishedCount++
// If one third of the relays have accepted the event, consider it a success
const isSuccess = successCount >= uniqueRelayUrls.length / 3 relayStatuses.push({
if (isSuccess) { url,
console.log(`✓ Publishing successful (${successCount}/${uniqueRelayUrls.length} relays)`) success: false,
this.emitNewEvent(event) error: errorMessage
resolve() })
}
if (finishedCount >= uniqueRelayUrls.length) { checkCompletion()
if (successCount > 0) { }
console.log(`✓ Publishing successful (${successCount}/${uniqueRelayUrls.length} relays)`) }
this.emitNewEvent(event)
resolve()
} else {
console.log(`✗ Publishing failed (0/${uniqueRelayUrls.length} relays)`)
reject(
new AggregateError(
errors.map(
({ url, error }) =>
new Error(
`${url}: ${error instanceof Error ? error.message : String(error)}`
)
)
)
)
}
}
})
}) })
) ).finally(() => {
clearTimeout(overallTimeout)
})
}) })
return result
} }
emitNewEvent(event: NEvent) { emitNewEvent(event: NEvent) {
@ -1376,8 +1489,34 @@ class ClientService extends EventTarget {
// ================= Performance Optimization ================= // ================= Performance Optimization =================
private optimizeRelaySelection(relays: string[]): string[] { private optimizeRelaySelection(relays: string[]): string[] {
// Filter out invalid or problematic relay URLs
const validRelays = relays.filter(url => {
try {
// Skip empty or invalid URLs
if (!url || typeof url !== 'string') return false
// Skip localhost URLs that might be misconfigured
if (url.includes('localhost:7777')) {
console.warn(`Skipping potentially misconfigured relay: ${url}`)
return false
}
// Validate websocket URL format
if (!isWebsocketUrl(url)) return false
// Skip URLs that are clearly invalid
const normalizedUrl = normalizeUrl(url)
if (!normalizedUrl) return false
return true
} catch (error) {
console.warn(`Skipping invalid relay URL: ${url}`, error)
return false
}
})
// Limit to 4 relays for better performance // Limit to 4 relays for better performance
return relays.slice(0, 4) return validRelays.slice(0, 4)
} }
// ================= Utils ================= // ================= Utils =================

Loading…
Cancel
Save