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' @@ -27,6 +27,7 @@ import PostOptions from './PostOptions'
import PostRelaySelector from './PostRelaySelector'
import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
import Uploader from './Uploader'
import RelayStatusDisplay from '@/components/RelayStatusDisplay'
export default function PostContent({
defaultContent = '',
@ -64,6 +65,14 @@ export default function PostContent({ @@ -64,6 +65,14 @@ export default function PostContent({
relays: []
})
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 canPost = useMemo(() => {
const result = (
@ -239,24 +248,86 @@ export default function PostContent({ @@ -239,24 +248,86 @@ export default function PostContent({
minPow
})
// console.log('Published event:', newEvent)
postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent)
addReplies([newEvent])
close()
// Check if we have relay status information
console.log('Published event:', newEvent)
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) {
const errors = error instanceof AggregateError ? error.errors : [error]
errors.forEach((err) => {
toast.error(
`${t('Failed to post')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
console.error('Publishing error:', error)
// Handle different types of errors with user-friendly messages
let errorMessage = t('Failed to post')
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
} finally {
setPosting(false)
}
toast.success(t('Post successful'), { duration: 2000 })
})
}
@ -507,6 +578,44 @@ export default function PostContent({ @@ -507,6 +578,44 @@ export default function PostContent({
{parentEvent ? t('Reply') : t('Post')}
</Button>
</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>
)
}

71
src/components/RelayStatusDisplay/index.tsx

@ -0,0 +1,71 @@ @@ -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 }) { @@ -631,7 +631,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
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
}

245
src/services/client.service.ts

@ -153,7 +153,17 @@ class ClientService extends EventTarget { @@ -153,7 +153,17 @@ class ClientService extends EventTarget {
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)))
console.log(`Publishing kind ${event.kind} event to ${uniqueRelayUrls.length} relays`)
// if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
@ -166,70 +176,173 @@ class ClientService extends EventTarget { @@ -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 finishedCount = 0
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(
uniqueRelayUrls.map(async (url) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const relay = await this.pool.ensureRelay(url)
relay.publishTimeout = 8_000 // 8s
return relay
.publish(event)
.then(() => {
console.log(`✓ Published to ${url}`)
this.trackEventSeenOn(event.id, relay)
successCount++
try {
const relay = await this.pool.ensureRelay(url)
relay.publishTimeout = 8_000 // 8s
await relay.publish(event)
console.log(`✓ Published to ${url}`)
this.trackEventSeenOn(event.id, relay)
successCount++
finishedCount++
relayStatuses.push({
url,
success: true
})
.catch((error) => {
console.log(`✗ Failed to publish to ${url}:`, error.message)
if (
error instanceof Error &&
error.message.startsWith('auth-required') &&
!!that.signer
) {
checkCompletion()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.log(`✗ Failed to publish to ${url}:`, errorMessage)
// 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}`)
return relay
.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt))
.then(() => relay.publish(event))
} else {
errors.push({ url, error })
const relay = await this.pool.ensureRelay(url)
await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt))
await relay.publish(event)
console.log(`✓ Published to ${url} after auth`)
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()
}
})
.finally(() => {
} else {
// For permanent errors like "blocked" or "writes disabled", don't retry
errors.push({ url, error })
finishedCount++
// If one third of the relays have accepted the event, consider it a success
const isSuccess = successCount >= uniqueRelayUrls.length / 3
if (isSuccess) {
console.log(`✓ Publishing successful (${successCount}/${uniqueRelayUrls.length} relays)`)
this.emitNewEvent(event)
resolve()
}
if (finishedCount >= uniqueRelayUrls.length) {
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)}`
)
)
)
)
}
}
})
relayStatuses.push({
url,
success: false,
error: errorMessage
})
checkCompletion()
}
}
})
)
).finally(() => {
clearTimeout(overallTimeout)
})
})
return result
}
emitNewEvent(event: NEvent) {
@ -1376,8 +1489,34 @@ class ClientService extends EventTarget { @@ -1376,8 +1489,34 @@ class ClientService extends EventTarget {
// ================= Performance Optimization =================
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
return relays.slice(0, 4)
return validRelays.slice(0, 4)
}
// ================= Utils =================

Loading…
Cancel
Save