Browse Source

universal publishing feedback

imwald
Silberengel 5 months ago
parent
commit
aa72b07557
  1. 27
      src/components/MailboxSetting/SaveButton.tsx
  2. 131
      src/components/PostEditor/PostContent.tsx
  3. 78
      src/lib/publishing-feedback.tsx
  4. 14
      src/providers/NostrProvider/index.tsx

27
src/components/MailboxSetting/SaveButton.tsx

@ -1,10 +1,11 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { createRelayListDraftEvent } from '@/lib/draft-event' import { createRelayListDraftEvent } from '@/lib/draft-event'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay } from '@/types' import { TMailboxRelay } from '@/types'
import { CloudUpload, Loader } from 'lucide-react' import { CloudUpload, Loader } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { toast } from 'sonner' import { useTranslation } from 'react-i18next'
export default function SaveButton({ export default function SaveButton({
mailboxRelays, mailboxRelays,
@ -15,6 +16,7 @@ export default function SaveButton({
hasChange: boolean hasChange: boolean
setHasChange: (hasChange: boolean) => void setHasChange: (hasChange: boolean) => void
}) { }) {
const { t } = useTranslation()
const { pubkey, publish, updateRelayListEvent } = useNostr() const { pubkey, publish, updateRelayListEvent } = useNostr()
const [pushing, setPushing] = useState(false) const [pushing, setPushing] = useState(false)
@ -22,13 +24,30 @@ export default function SaveButton({
if (!pubkey) return if (!pubkey) return
setPushing(true) setPushing(true)
try {
const event = createRelayListDraftEvent(mailboxRelays) const event = createRelayListDraftEvent(mailboxRelays)
const relayListEvent = await publish(event) const result = await publish(event)
await updateRelayListEvent(relayListEvent) await updateRelayListEvent(result)
toast.success('Successfully saved mailbox relays')
setHasChange(false) setHasChange(false)
// Show publishing feedback
if ((result as any).relayStatuses) {
showPublishingFeedback({
success: true,
relayStatuses: (result as any).relayStatuses,
successCount: (result as any).relayStatuses.filter((s: any) => s.success).length,
totalCount: (result as any).relayStatuses.length
}, {
message: t('Mailbox relays saved'),
duration: 6000
})
} else {
showSimplePublishSuccess(t('Mailbox relays saved'))
}
} finally {
setPushing(false) setPushing(false)
} }
}
return ( return (
<Button className="w-full" disabled={!pubkey || pushing || !hasChange} onClick={save}> <Button className="w-full" disabled={!pubkey || pushing || !hasChange} onClick={save}>

131
src/components/PostEditor/PostContent.tsx

@ -19,7 +19,7 @@ import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X } fr
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import EmojiPickerDialog from '../EmojiPickerDialog' import EmojiPickerDialog from '../EmojiPickerDialog'
import Mentions, { extractMentions } from './Mentions' import Mentions, { extractMentions } from './Mentions'
import PollEditor from './PollEditor' import PollEditor from './PollEditor'
@ -27,7 +27,6 @@ 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 = '',
@ -65,14 +64,6 @@ 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 = (
@ -249,82 +240,28 @@ export default function PostContent({
}) })
// console.log('Published event:', newEvent) // console.log('Published event:', newEvent)
// Check if we have relay status information // Show publishing feedback
console.log('Published event:', newEvent)
console.log('Relay statuses:', (newEvent as any).relayStatuses)
if ((newEvent as any).relayStatuses) { if ((newEvent as any).relayStatuses) {
setRelayStatuses((newEvent as any).relayStatuses) showPublishingFeedback({
setLastPublishedEvent(newEvent) success: true,
setShowRelayStatus(true) relayStatuses: (newEvent as any).relayStatuses,
successCount: (newEvent as any).relayStatuses.filter((s: any) => s.success).length,
// Show success message with relay count totalCount: (newEvent as any).relayStatuses.length
const successCount = (newEvent as any).relayStatuses.filter((s: any) => s.success).length }, {
const totalCount = (newEvent as any).relayStatuses.length message: parentEvent ? t('Reply published') : t('Post published'),
toast.success(t('Post successful - published to {{count}} of {{total}} relays', { duration: 6000
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 { } else {
toast.success(t('Post successful'), { duration: 2000 }) showSimplePublishSuccess(parentEvent ? t('Reply published') : t('Post published'))
}
postEditorCache.clearPostCache({ defaultContent, parentEvent }) postEditorCache.clearPostCache({ defaultContent, parentEvent })
deleteDraftEventCache(draftEvent) deleteDraftEventCache(draftEvent)
addReplies([newEvent]) addReplies([newEvent])
close() close()
}
} catch (error) { } catch (error) {
console.error('Publishing error:', error) console.error('Publishing error:', error)
// Publishing errors are handled via relay status feedback
// 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')
)
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 { } finally {
setPosting(false) setPosting(false)
} }
@ -578,44 +515,6 @@ 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>
) )
} }

78
src/lib/publishing-feedback.tsx

@ -0,0 +1,78 @@
import RelayStatusDisplay from '@/components/RelayStatusDisplay'
import { CheckCircle2 } from 'lucide-react'
import { toast } from 'sonner'
export type RelayStatus = {
url: string
success: boolean
error?: string
authAttempted?: boolean
}
export type PublishResult = {
success: boolean
relayStatuses: RelayStatus[]
successCount: number
totalCount: number
}
/**
* Show publishing feedback with relay status details
* @param result Publishing result with relay statuses
* @param options Optional configuration
*/
export function showPublishingFeedback(
result: PublishResult,
options: {
message?: string
duration?: number
} = {}
) {
const { message = 'Published successfully', duration = 6000 } = options
const { relayStatuses, successCount, totalCount } = result
if (relayStatuses.length === 0) {
// Fallback for events without relay status tracking
toast.success(message, { duration: 2000 })
return
}
// Show toast with custom relay status display
toast.success(
<div className="w-full">
<div className="flex items-center gap-2 mb-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div className="font-semibold">{message}</div>
</div>
<div className="text-xs text-muted-foreground mb-2">
Published to {successCount} of {totalCount} relays
</div>
<RelayStatusDisplay
relayStatuses={relayStatuses}
successCount={successCount}
totalCount={totalCount}
/>
</div>,
{
duration,
className: 'max-w-md'
}
)
}
/**
* Simple success toast without relay details
*/
export function showSimplePublishSuccess(message = 'Published successfully') {
toast.success(message, { duration: 2000 })
}
/**
* Show publishing error
*/
export function showPublishingError(error: Error | string) {
const message = error instanceof Error ? error.message : error
toast.error(message, { duration: 4000 })
}

14
src/providers/NostrProvider/index.tsx

@ -14,6 +14,7 @@ import {
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import client from '@/services/client.service' import client from '@/services/client.service'
import customEmojiService from '@/services/custom-emoji.service' import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
@ -657,10 +658,19 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// Privacy: Only use user's own relays, never connect to "seen on" relays // Privacy: Only use user's own relays, never connect to "seen on" relays
const relays = await client.determineTargetRelays(targetEvent) const relays = await client.determineTargetRelays(targetEvent)
await client.publishEvent(relays, deletionRequest) const result = await client.publishEvent(relays, deletionRequest)
addDeletedEvent(targetEvent) addDeletedEvent(targetEvent)
toast.success(t('Deletion request sent to {{count}} relays', { count: relays.length }))
// Show publishing feedback
if (result.relayStatuses) {
showPublishingFeedback(result, {
message: t('Deletion request sent'),
duration: 6000
})
} else {
showSimplePublishSuccess(t('Deletion request sent'))
}
} }
const signHttpAuth = async (url: string, method: string, content = '') => { const signHttpAuth = async (url: string, method: string, content = '') => {

Loading…
Cancel
Save