fix: expose more detailed error messages

This commit is contained in:
codytseng 2026-01-17 11:50:24 +08:00
parent d37aa61501
commit 71791c9513
27 changed files with 330 additions and 153 deletions

View file

@ -6,7 +6,6 @@ import { BookmarkIcon, Loader } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function BookmarkButton({ stuff }: { stuff: Event | string }) { export default function BookmarkButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -33,13 +32,8 @@ export default function BookmarkButton({ stuff }: { stuff: Event | string }) {
if (isBookmarked || !event) return if (isBookmarked || !event) return
setUpdating(true) setUpdating(true)
try { await addBookmark(event)
await addBookmark(event) setUpdating(false)
} catch (error) {
toast.error(t('Bookmark failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }
@ -49,13 +43,8 @@ export default function BookmarkButton({ stuff }: { stuff: Event | string }) {
if (!isBookmarked || !event) return if (!isBookmarked || !event) return
setUpdating(true) setUpdating(true)
try { await removeBookmark(event)
await removeBookmark(event) setUpdating(false)
} catch (error) {
toast.error(t('Remove bookmark failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }

View file

@ -15,7 +15,6 @@ import { useNostr } from '@/providers/NostrProvider'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function FollowButton({ pubkey }: { pubkey: string }) { export default function FollowButton({ pubkey }: { pubkey: string }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -33,13 +32,8 @@ export default function FollowButton({ pubkey }: { pubkey: string }) {
if (isFollowing) return if (isFollowing) return
setUpdating(true) setUpdating(true)
try { await follow(pubkey)
await follow(pubkey) setUpdating(false)
} catch (error) {
toast.error(t('Follow failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }
@ -49,13 +43,8 @@ export default function FollowButton({ pubkey }: { pubkey: string }) {
if (!isFollowing) return if (!isFollowing) return
setUpdating(true) setUpdating(true)
try { await unfollow(pubkey)
await unfollow(pubkey) setUpdating(false)
} catch (error) {
toast.error(t('Unfollow failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }

View file

@ -1,5 +1,6 @@
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 { formatError } from '@/lib/error'
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'
@ -25,11 +26,18 @@ export default function SaveButton({
setPushing(true) setPushing(true)
const event = createRelayListDraftEvent(mailboxRelays) const event = createRelayListDraftEvent(mailboxRelays)
const relayListEvent = await publish(event) try {
await updateRelayListEvent(relayListEvent) const relayListEvent = await publish(event)
toast.success('Successfully saved mailbox relays') await updateRelayListEvent(relayListEvent)
setHasChange(false) toast.success('Successfully saved mailbox relays')
setPushing(false) setHasChange(false)
setPushing(false)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to save mailbox relays')}: ${err}`, { duration: 10_000 })
})
}
} }
return ( return (

View file

@ -7,7 +7,6 @@ import { CheckIcon, Loader, PlusIcon } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import Image from '../Image' import Image from '../Image'
export default function EmojiPack({ event, className }: { event: Event; className?: string }) { export default function EmojiPack({ event, className }: { event: Event; className?: string }) {
@ -27,14 +26,8 @@ export default function EmojiPack({ event, className }: { event: Event; classNam
if (isCollected) return if (isCollected) return
setUpdating(true) setUpdating(true)
try { await addEmojiPack(event)
await addEmojiPack(event) setUpdating(false)
toast.success(t('Emoji pack added'))
} catch (error) {
toast.error(t('Add emoji pack failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }
@ -44,14 +37,8 @@ export default function EmojiPack({ event, className }: { event: Event; classNam
if (!isCollected) return if (!isCollected) return
setUpdating(true) setUpdating(true)
try { await removeEmojiPack(event)
await removeEmojiPack(event) setUpdating(false)
toast.success(t('Emoji pack removed'))
} catch (error) {
toast.error(t('Remove emoji pack failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
}) })
} }

View file

@ -3,6 +3,7 @@ import { POLL_TYPE } from '@/constants'
import { useTranslatedEvent } from '@/hooks' import { useTranslatedEvent } from '@/hooks'
import { useFetchPollResults } from '@/hooks/useFetchPollResults' import { useFetchPollResults } from '@/hooks/useFetchPollResults'
import { createPollResponseDraftEvent } from '@/lib/draft-event' import { createPollResponseDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getPollMetadataFromEvent } from '@/lib/event-metadata' import { getPollMetadataFromEvent } from '@/lib/event-metadata'
import { cn, isPartiallyInViewport } from '@/lib/utils' import { cn, isPartiallyInViewport } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -126,8 +127,10 @@ export default function Poll({ event, className }: { event: Event; className?: s
setSelectedOptionIds([]) setSelectedOptionIds([])
pollResultsService.addPollResponse(event.id, pubkey, selectedOptionIds) pollResultsService.addPollResponse(event.id, pubkey, selectedOptionIds)
} catch (error) { } catch (error) {
console.error('Failed to vote:', error) const errors = formatError(error)
toast.error('Failed to vote: ' + (error as Error).message) errors.forEach((err) => {
toast.error(`${t('Failed to vote')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setIsVoting(false) setIsVoting(false)
} }

View file

@ -16,6 +16,7 @@ import {
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { createReportDraftEvent } from '@/lib/draft-event' import { createReportDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
@ -94,13 +95,9 @@ function ReportContent({ event, closeDialog }: { event: NostrEvent; closeDialog:
toast.success(t('Successfully report')) toast.success(t('Successfully report'))
closeDialog() closeDialog()
} catch (error) { } catch (error) {
const errors = error instanceof AggregateError ? error.errors : [error] const errors = formatError(error)
errors.forEach((err) => { errors.forEach((err) => {
toast.error( toast.error(`${t('Failed to report')}: ${err}`, { duration: 10_000 })
`${t('Failed to report')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
)
console.error(err)
}) })
return return
} finally { } finally {

View file

@ -1,3 +1,4 @@
import { formatError } from '@/lib/error'
import { getNoteBech32Id, isProtectedEvent } from '@/lib/event' import { getNoteBech32Id, isProtectedEvent } from '@/lib/event'
import { toNjump } from '@/lib/link' import { toNjump } from '@/lib/link'
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
@ -117,7 +118,7 @@ export function useMenuActions({
error: (err) => { error: (err) => {
return t('Failed to republish to relay set: {{name}}. Error: {{error}}', { return t('Failed to republish to relay set: {{name}}. Error: {{error}}', {
name: set.name, name: set.name,
error: err.message error: formatError(err).join('; ')
}) })
} }
}) })
@ -147,7 +148,7 @@ export function useMenuActions({
error: (err) => { error: (err) => {
return t('Failed to republish to relay: {{url}}. Error: {{error}}', { return t('Failed to republish to relay: {{url}}. Error: {{error}}', {
url: simplifyUrl(relay), url: simplifyUrl(relay),
error: err.message error: formatError(err).join('; ')
}) })
} }
}) })

View file

@ -26,6 +26,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 { formatError } from '@/lib/error'
export default function PostContent({ export default function PostContent({
defaultContent = '', defaultContent = '',
@ -160,13 +161,9 @@ export default function PostContent({
toast.success(t('Post successful'), { duration: 2000 }) toast.success(t('Post successful'), { duration: 2000 })
close() close()
} catch (error) { } catch (error) {
const errors = error instanceof AggregateError ? error.errors : [error] const errors = formatError(error)
errors.forEach((err) => { errors.forEach((err) => {
toast.error( toast.error(`${t('Failed to post')}: ${err}`, { duration: 10_000 })
`${t('Failed to post')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
)
console.error(err)
}) })
return return
} finally { } finally {

View file

@ -1,6 +1,7 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { createRelayReviewDraftEvent } from '@/lib/draft-event' import { createRelayReviewDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Loader2, Star } from 'lucide-react' import { Loader2, Star } from 'lucide-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
@ -32,12 +33,10 @@ export default function ReviewEditor({
const evt = await publish(draftEvent) const evt = await publish(draftEvent)
onReviewed(evt) onReviewed(evt)
} catch (error) { } catch (error) {
if (error instanceof AggregateError) { const errors = formatError(error)
error.errors.forEach((e) => toast.error(`${t('Failed to review')}: ${e.message}`)) errors.forEach((err) => {
} else if (error instanceof Error) { toast.error(`${t('Failed to review')}: ${err}`, { duration: 10_000 })
toast.error(`${t('Failed to review')}: ${error.message}`) })
}
console.error(error)
return return
} finally { } finally {
setSubmitting(false) setSubmitting(false)

View file

@ -19,6 +19,7 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { createJoinDraftEvent } from '@/lib/draft-event' import { createJoinDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import relayMembershipService from '@/services/relay-membership.service' import relayMembershipService from '@/services/relay-membership.service'
@ -57,13 +58,9 @@ export default function JoinDialog({
setInviteCode('') setInviteCode('')
setShowJoinDialog(false) setShowJoinDialog(false)
} catch (error) { } catch (error) {
const errors = error instanceof AggregateError ? error.errors : [error] const errors = formatError(error)
errors.forEach((err) => { errors.forEach((err) => {
toast.error( toast.error(`${t('Failed to send join request')}: ${err}`, { duration: 10_000 })
`${t('Failed to send join request')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
)
console.error(err)
}) })
return return
} finally { } finally {

View file

@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { createJoinDraftEvent, createLeaveDraftEvent } from '@/lib/draft-event' import { createJoinDraftEvent, createLeaveDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { checkNip43Support } from '@/lib/relay' import { checkNip43Support } from '@/lib/relay'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import relayMembershipService from '@/services/relay-membership.service' import relayMembershipService from '@/services/relay-membership.service'
@ -72,7 +73,11 @@ export default function RelayMembershipControl({
toast.success(t('Join request sent successfully')) toast.success(t('Join request sent successfully'))
await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey) await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey)
onMembershipStatusChange?.(true) onMembershipStatusChange?.(true)
} catch { } catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to send join request')}: ${err}`, { duration: 10_000 })
})
setShowJoinDialog(true) setShowJoinDialog(true)
} finally { } finally {
setIsLoading(false) setIsLoading(false)

View file

@ -23,6 +23,8 @@ import Emoji from '../Emoji'
import EmojiPicker from '../EmojiPicker' import EmojiPicker from '../EmojiPicker'
import SuggestedEmojis from '../SuggestedEmojis' import SuggestedEmojis from '../SuggestedEmojis'
import { formatCount } from './utils' import { formatCount } from './utils'
import { formatError } from '@/lib/error'
import { toast } from 'sonner'
export default function LikeButton({ stuff }: { stuff: Event | string }) { export default function LikeButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -90,7 +92,10 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
const evt = await publish(reaction, { additionalRelayUrls: seenOn }) const evt = await publish(reaction, { additionalRelayUrls: seenOn })
stuffStatsService.updateStuffStatsByEvents([evt]) stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) { } catch (error) {
console.error('like failed', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to like')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setLiking(false) setLiking(false)
clearTimeout(timer) clearTimeout(timer)

View file

@ -5,6 +5,7 @@ import {
createExternalContentReactionDraftEvent, createExternalContentReactionDraftEvent,
createReactionDraftEvent createReactionDraftEvent
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getDefaultRelayUrls } from '@/lib/relay' import { getDefaultRelayUrls } from '@/lib/relay'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -14,6 +15,7 @@ import { TEmoji } from '@/types'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import Emoji from '../Emoji' import Emoji from '../Emoji'
export default function Likes({ stuff }: { stuff: Event | string }) { export default function Likes({ stuff }: { stuff: Event | string }) {
@ -57,7 +59,10 @@ export default function Likes({ stuff }: { stuff: Event | string }) {
const evt = await publish(reaction, { additionalRelayUrls: seenOn }) const evt = await publish(reaction, { additionalRelayUrls: seenOn })
stuffStatsService.updateStuffStatsByEvents([evt]) stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) { } catch (error) {
console.error('like failed', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to like: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setLiking(null) setLiking(null)
clearTimeout(timer) clearTimeout(timer)

View file

@ -22,6 +22,8 @@ import { useTranslation } from 'react-i18next'
import PostEditor from '../PostEditor' import PostEditor from '../PostEditor'
import { formatCount } from './utils' import { formatCount } from './utils'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants' import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { formatError } from '@/lib/error'
import { toast } from 'sonner'
export default function RepostButton({ stuff }: { stuff: Event | string }) { export default function RepostButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -87,7 +89,10 @@ export default function RepostButton({ stuff }: { stuff: Event | string }) {
const evt = await publish(repost) const evt = await publish(repost)
stuffStatsService.updateStuffStatsByEvents([evt]) stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) { } catch (error) {
console.error('repost failed', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to repost')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setReposting(false) setReposting(false)
clearTimeout(timer) clearTimeout(timer)

6
src/lib/error.ts Normal file
View file

@ -0,0 +1,6 @@
export function formatError(error: unknown): string[] {
const errors = error instanceof AggregateError ? error.errors : [error]
return errors.map((err) => {
return err instanceof Error ? err.message : String(err)
})
}

View file

@ -4,6 +4,7 @@ import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { RECOMMENDED_BLOSSOM_SERVERS } from '@/constants' import { RECOMMENDED_BLOSSOM_SERVERS } from '@/constants'
import { createBlossomServerListDraftEvent } from '@/lib/draft-event' import { createBlossomServerListDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getServersFromServerTags } from '@/lib/tag' import { getServersFromServerTags } from '@/lib/tag'
import { normalizeHttpUrl } from '@/lib/url' import { normalizeHttpUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -13,6 +14,7 @@ import { AlertCircle, ArrowUpToLine, Loader, X } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function BlossomServerListSetting() { export default function BlossomServerListSetting() {
const { t } = useTranslation() const { t } = useTranslation()
@ -48,7 +50,10 @@ export default function BlossomServerListSetting() {
setBlossomServerListEvent(newEvent) setBlossomServerListEvent(newEvent)
setUrl('') setUrl('')
} catch (error) { } catch (error) {
console.error('Failed to add Blossom URL:', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to add Blossom URL')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setAdding(false) setAdding(false)
} }
@ -72,7 +77,10 @@ export default function BlossomServerListSetting() {
await client.updateBlossomServerListEventCache(newEvent) await client.updateBlossomServerListEventCache(newEvent)
setBlossomServerListEvent(newEvent) setBlossomServerListEvent(newEvent)
} catch (error) { } catch (error) {
console.error('Failed to remove Blossom URL:', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to remove Blossom URL')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setRemovingIndex(-1) setRemovingIndex(-1)
} }
@ -88,7 +96,10 @@ export default function BlossomServerListSetting() {
await client.updateBlossomServerListEventCache(newEvent) await client.updateBlossomServerListEventCache(newEvent)
setBlossomServerListEvent(newEvent) setBlossomServerListEvent(newEvent)
} catch (error) { } catch (error) {
console.error('Failed to move Blossom URL to top:', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to move Blossom URL to top')}: ${err}`, { duration: 10_000 })
})
} finally { } finally {
setMovingIndex(-1) setMovingIndex(-1)
} }

View file

@ -7,6 +7,7 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { createProfileDraftEvent } from '@/lib/draft-event' import { createProfileDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { generateImageByPubkey } from '@/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { isEmail } from '@/lib/utils' import { isEmail } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
@ -14,6 +15,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { Loader, Upload } from 'lucide-react' import { Loader, Upload } from 'lucide-react'
import { forwardRef, useEffect, useMemo, useState } from 'react' import { forwardRef, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -97,10 +99,17 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
JSON.stringify(newProfileContent), JSON.stringify(newProfileContent),
profileEvent?.tags profileEvent?.tags
) )
const newProfileEvent = await publish(profileDraftEvent) try {
await updateProfileEvent(newProfileEvent) const newProfileEvent = await publish(profileDraftEvent)
setSaving(false) await updateProfileEvent(newProfileEvent)
pop() setSaving(false)
pop()
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to save profile')}: ${err}`, { duration: 10_000 })
})
}
} }
const onBannerUploadSuccess = ({ url }: { url: string }) => { const onBannerUploadSuccess = ({ url }: { url: string }) => {

View file

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { createProfileDraftEvent } from '@/lib/draft-event' import { createProfileDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { isEmail } from '@/lib/utils' import { isEmail } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useZap } from '@/providers/ZapProvider' import { useZap } from '@/providers/ZapProvider'
@ -65,8 +66,13 @@ const RizfulPage = forwardRef(({ index }: { index?: number }, ref) => {
) )
const newProfileEvent = await publish(profileDraftEvent) const newProfileEvent = await publish(profileDraftEvent)
await updateProfileEvent(newProfileEvent) await updateProfileEvent(newProfileEvent)
} catch (e: unknown) { } catch (error) {
toast.error(e instanceof Error ? e.message : String(e)) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to update profile with Lightning Address')}: ${err}`, {
duration: 10_000
})
})
} }
} }

View file

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { createProfileDraftEvent } from '@/lib/draft-event' import { createProfileDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { isEmail } from '@/lib/utils' import { isEmail } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
@ -45,9 +46,19 @@ export default function LightningAddressInput() {
JSON.stringify(profileContent), JSON.stringify(profileContent),
profileEvent?.tags profileEvent?.tags
) )
const newProfileEvent = await publish(profileDraftEvent) try {
await updateProfileEvent(newProfileEvent) const newProfileEvent = await publish(profileDraftEvent)
setSaving(false) await updateProfileEvent(newProfileEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`${t('Failed to update profile with Lightning Address')}: ${err}`, {
duration: 10_000
})
})
} finally {
setSaving(false)
}
} }
return ( return (

View file

@ -1,8 +1,10 @@
import { buildATag, buildETag, createBookmarkDraftEvent } from '@/lib/draft-event' import { buildATag, buildETag, createBookmarkDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
import { toast } from 'sonner'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
type TBookmarksContext = { type TBookmarksContext = {
@ -45,8 +47,15 @@ export function BookmarksProvider({ children }: { children: React.ReactNode }) {
[...currentTags, isReplaceable ? buildATag(event) : buildETag(event.id, event.pubkey)], [...currentTags, isReplaceable ? buildATag(event) : buildETag(event.id, event.pubkey)],
bookmarkListEvent?.content bookmarkListEvent?.content
) )
const newBookmarkEvent = await publish(newBookmarkDraftEvent) try {
await updateBookmarkListEvent(newBookmarkEvent) const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to add bookmark: ${err}`, { duration: 10_000 })
})
}
} }
const removeBookmark = async (event: Event) => { const removeBookmark = async (event: Event) => {
@ -64,8 +73,15 @@ export function BookmarksProvider({ children }: { children: React.ReactNode }) {
if (newTags.length === bookmarkListEvent.tags.length) return if (newTags.length === bookmarkListEvent.tags.length) return
const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content) const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content)
const newBookmarkEvent = await publish(newBookmarkDraftEvent) try {
await updateBookmarkListEvent(newBookmarkEvent) const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to remove bookmark: ${err}`, { duration: 10_000 })
})
}
} }
return ( return (

View file

@ -1,8 +1,10 @@
import { buildATag, createUserEmojiListDraftEvent } from '@/lib/draft-event' import { buildATag, createUserEmojiListDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getReplaceableCoordinateFromEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent } from '@/lib/event'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useMemo } from 'react' import { createContext, useContext, useMemo } from 'react'
import { toast } from 'sonner'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
type TEmojiPackContext = { type TEmojiPackContext = {
@ -54,8 +56,15 @@ export function EmojiPackProvider({ children }: { children: React.ReactNode }) {
[...currentTags, buildATag(event)], [...currentTags, buildATag(event)],
userEmojiListEvent?.content userEmojiListEvent?.content
) )
const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) try {
await updateUserEmojiListEvent(newUserEmojiListEvent) const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent)
await updateUserEmojiListEvent(newUserEmojiListEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to add emoji pack: ${err}`, { duration: 10_000 })
})
}
} }
const removeEmojiPack = async (event: Event) => { const removeEmojiPack = async (event: Event) => {
@ -72,8 +81,15 @@ export function EmojiPackProvider({ children }: { children: React.ReactNode }) {
newTags, newTags,
userEmojiListEvent.content userEmojiListEvent.content
) )
const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) try {
await updateUserEmojiListEvent(newUserEmojiListEvent) const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent)
await updateUserEmojiListEvent(newUserEmojiListEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to remove emoji pack: ${err}`, { duration: 10_000 })
})
}
} }
return ( return (

View file

@ -1,4 +1,5 @@
import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event' import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getReplaceableEventIdentifier } from '@/lib/event' import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent } from '@/lib/event-metadata'
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
@ -10,6 +11,7 @@ import storage from '@/services/local-storage.service'
import { TRelaySet } from '@/types' import { TRelaySet } from '@/types'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
type TFavoriteRelaysContext = { type TFavoriteRelaysContext = {
@ -146,8 +148,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
[...favoriteRelays, ...normalizedUrls], [...favoriteRelays, ...normalizedUrls],
relaySetEvents relaySetEvents
) )
const newFavoriteRelaysEvent = await publish(draftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to add favorite relays: ${err}`, { duration: 10_000 })
})
}
} }
const deleteFavoriteRelays = async (relayUrls: string[]) => { const deleteFavoriteRelays = async (relayUrls: string[]) => {
@ -160,8 +169,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
favoriteRelays.filter((url) => !normalizedUrls.includes(url)), favoriteRelays.filter((url) => !normalizedUrls.includes(url)),
relaySetEvents relaySetEvents
) )
const newFavoriteRelaysEvent = await publish(draftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to delete favorite relays: ${err}`, { duration: 10_000 })
})
}
} }
const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => { const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
@ -174,15 +190,22 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
name: relaySetName, name: relaySetName,
relayUrls: normalizedUrls relayUrls: normalizedUrls
}) })
const newRelaySetEvent = await publish(relaySetDraftEvent) try {
await indexedDb.putReplaceableEvent(newRelaySetEvent) const newRelaySetEvent = await publish(relaySetDraftEvent)
await indexedDb.putReplaceableEvent(newRelaySetEvent)
const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [ const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [
...relaySetEvents, ...relaySetEvents,
newRelaySetEvent newRelaySetEvent
]) ])
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to create relay set: ${err}`, { duration: 10_000 })
})
}
} }
const addRelaySets = async (newRelaySetEvents: Event[]) => { const addRelaySets = async (newRelaySetEvents: Event[]) => {
@ -190,8 +213,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
...relaySetEvents, ...relaySetEvents,
...newRelaySetEvents ...newRelaySetEvents
]) ])
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to add relay sets: ${err}`, { duration: 10_000 })
})
}
} }
const deleteRelaySet = async (id: string) => { const deleteRelaySet = async (id: string) => {
@ -201,30 +231,52 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
if (newRelaySetEvents.length === relaySetEvents.length) return if (newRelaySetEvents.length === relaySetEvents.length) return
const draftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, newRelaySetEvents) const draftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, newRelaySetEvents)
const newFavoriteRelaysEvent = await publish(draftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to delete relay set: ${err}`, { duration: 10_000 })
})
}
} }
const updateRelaySet = async (newSet: TRelaySet) => { const updateRelaySet = async (newSet: TRelaySet) => {
const draftEvent = createRelaySetDraftEvent(newSet) const draftEvent = createRelaySetDraftEvent(newSet)
const newRelaySetEvent = await publish(draftEvent)
await indexedDb.putReplaceableEvent(newRelaySetEvent)
setRelaySetEvents((prev) => { try {
return prev.map((event) => { const newRelaySetEvent = await publish(draftEvent)
if (getReplaceableEventIdentifier(event) === newSet.id) { await indexedDb.putReplaceableEvent(newRelaySetEvent)
return newRelaySetEvent
} setRelaySetEvents((prev) => {
return event return prev.map((event) => {
if (getReplaceableEventIdentifier(event) === newSet.id) {
return newRelaySetEvent
}
return event
})
}) })
}) } catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to update relay set: ${err}`, { duration: 10_000 })
})
}
} }
const reorderFavoriteRelays = async (reorderedRelays: string[]) => { const reorderFavoriteRelays = async (reorderedRelays: string[]) => {
setFavoriteRelays(reorderedRelays) setFavoriteRelays(reorderedRelays)
const draftEvent = createFavoriteRelaysDraftEvent(reorderedRelays, relaySetEvents) const draftEvent = createFavoriteRelaysDraftEvent(reorderedRelays, relaySetEvents)
const newFavoriteRelaysEvent = await publish(draftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to reorder favorite relays: ${err}`, { duration: 10_000 })
})
}
} }
const reorderRelaySets = async (reorderedSets: TRelaySet[]) => { const reorderRelaySets = async (reorderedSets: TRelaySet[]) => {
@ -233,8 +285,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
favoriteRelays, favoriteRelays,
reorderedSets.map((set) => set.aTag) reorderedSets.map((set) => set.aTag)
) )
const newFavoriteRelaysEvent = await publish(draftEvent) try {
updateFavoriteRelaysEvent(newFavoriteRelaysEvent) const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to reorder relay sets: ${err}`, { duration: 10_000 })
})
}
} }
return ( return (

View file

@ -4,6 +4,8 @@ import client from '@/services/client.service'
import { createContext, useContext, useMemo } from 'react' import { createContext, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
import { formatError } from '@/lib/error'
import { toast } from 'sonner'
type TFollowListContext = { type TFollowListContext = {
followingSet: Set<string> followingSet: Set<string>
@ -44,8 +46,15 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
(followListEvent?.tags ?? []).concat([['p', pubkey]]), (followListEvent?.tags ?? []).concat([['p', pubkey]]),
followListEvent?.content followListEvent?.content
) )
const newFollowListEvent = await publish(newFollowListDraftEvent) try {
await updateFollowListEvent(newFollowListEvent) const newFollowListEvent = await publish(newFollowListDraftEvent)
await updateFollowListEvent(newFollowListEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to follow: ${err}`, { duration: 10_000 })
})
}
} }
const unfollow = async (pubkey: string) => { const unfollow = async (pubkey: string) => {
@ -58,8 +67,15 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
followListEvent.tags.filter(([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey), followListEvent.tags.filter(([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey),
followListEvent.content followListEvent.content
) )
const newFollowListEvent = await publish(newFollowListDraftEvent) try {
await updateFollowListEvent(newFollowListEvent) const newFollowListEvent = await publish(newFollowListDraftEvent)
await updateFollowListEvent(newFollowListEvent)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to unfollow: ${err}`, { duration: 10_000 })
})
}
} }
return ( return (

View file

@ -1,4 +1,5 @@
import { createMuteListDraftEvent } from '@/lib/draft-event' import { createMuteListDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getPubkeysFromPTags } from '@/lib/tag' import { getPubkeysFromPTags } from '@/lib/tag'
import client from '@/services/client.service' import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
@ -115,7 +116,6 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
} }
const newMuteListDraftEvent = createMuteListDraftEvent(tags, content) const newMuteListDraftEvent = createMuteListDraftEvent(tags, content)
const event = await publish(newMuteListDraftEvent) const event = await publish(newMuteListDraftEvent)
toast.success(t('Successfully updated mute list'))
return event return event
} }
@ -147,7 +147,10 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const privateTags = await getPrivateTags(newMuteListEvent) const privateTags = await getPrivateTags(newMuteListEvent)
await updateMuteListEvent(newMuteListEvent, privateTags) await updateMuteListEvent(newMuteListEvent, privateTags)
} catch (error) { } catch (error) {
toast.error(t('Failed to mute user publicly') + ': ' + (error as Error).message) const errors = formatError(error)
errors.forEach((err) => {
toast.error(t('Failed to mute user publicly') + ': ' + err, { duration: 10_000 })
})
} finally { } finally {
setChanging(false) setChanging(false)
} }
@ -170,7 +173,10 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText) const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText)
await updateMuteListEvent(newMuteListEvent, newPrivateTags) await updateMuteListEvent(newMuteListEvent, newPrivateTags)
} catch (error) { } catch (error) {
toast.error(t('Failed to mute user privately') + ': ' + (error as Error).message) const errors = formatError(error)
errors.forEach((err) => {
toast.error(t('Failed to mute user privately') + ': ' + err, { duration: 10_000 })
})
} finally { } finally {
setChanging(false) setChanging(false)
} }
@ -196,6 +202,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
cipherText cipherText
) )
await updateMuteListEvent(newMuteListEvent, newPrivateTags) await updateMuteListEvent(newMuteListEvent, newPrivateTags)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(t('Failed to unmute user') + ': ' + err, { duration: 10_000 })
})
} finally { } finally {
setChanging(false) setChanging(false)
} }
@ -223,6 +234,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
cipherText cipherText
) )
await updateMuteListEvent(newMuteListEvent, newPrivateTags) await updateMuteListEvent(newMuteListEvent, newPrivateTags)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(t('Failed to switch to public mute') + ': ' + err, { duration: 10_000 })
})
} finally { } finally {
setChanging(false) setChanging(false)
} }
@ -248,6 +264,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags)) const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText) const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
await updateMuteListEvent(newMuteListEvent, newPrivateTags) await updateMuteListEvent(newMuteListEvent, newPrivateTags)
} catch (error) {
const errors = formatError(error)
errors.forEach((err) => {
toast.error(t('Failed to switch to private mute') + ': ' + err, { duration: 10_000 })
})
} finally { } finally {
setChanging(false) setChanging(false)
} }

View file

@ -1,5 +1,6 @@
import { MAX_PINNED_NOTES } from '@/constants' import { MAX_PINNED_NOTES } from '@/constants'
import { buildETag, createPinListDraftEvent } from '@/lib/draft-event' import { buildETag, createPinListDraftEvent } from '@/lib/draft-event'
import { formatError } from '@/lib/error'
import { getPinnedEventHexIdSetFromPinListEvent } from '@/lib/event-metadata' import { getPinnedEventHexIdSetFromPinListEvent } from '@/lib/event-metadata'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
@ -67,7 +68,7 @@ export function PinListProvider({ children }: { children: React.ReactNode }) {
const { unwrap } = toast.promise(_pin, { const { unwrap } = toast.promise(_pin, {
loading: t('Pinning...'), loading: t('Pinning...'),
success: t('Pinned!'), success: t('Pinned!'),
error: (err) => t('Failed to pin: {{error}}', { error: err.message }) error: (err) => t('Failed to pin: {{error}}', { error: formatError(err).join('; ') })
}) })
await unwrap() await unwrap()
} }
@ -92,7 +93,7 @@ export function PinListProvider({ children }: { children: React.ReactNode }) {
const { unwrap } = toast.promise(_unpin, { const { unwrap } = toast.promise(_unpin, {
loading: t('Unpinning...'), loading: t('Unpinning...'),
success: t('Unpinned!'), success: t('Unpinned!'),
error: (err) => t('Failed to unpin: {{error}}', { error: err.message }) error: (err) => t('Failed to unpin: {{error}}', { error: formatError(err).join('; ') })
}) })
await unwrap() await unwrap()
} }

View file

@ -1,8 +1,10 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { formatError } from '@/lib/error'
import { getPubkeysFromPTags } from '@/lib/tag' import { getPubkeysFromPTags } from '@/lib/tag'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { z } from 'zod' import { z } from 'zod'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
@ -105,7 +107,10 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
const newEvent = await publish(draftEvent) const newEvent = await publish(draftEvent)
await updatePinnedUsersEvent(newEvent, privateTags) await updatePinnedUsersEvent(newEvent, privateTags)
} catch (error) { } catch (error) {
console.error('Failed to pin user:', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to pin user: ${err}`, { duration: 10_000 })
})
} }
}, },
[accountPubkey, isPinned, pinnedUsersEvent, publish, updatePinnedUsersEvent, privateTags] [accountPubkey, isPinned, pinnedUsersEvent, publish, updatePinnedUsersEvent, privateTags]
@ -130,7 +135,10 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode })
const newEvent = await publish(draftEvent) const newEvent = await publish(draftEvent)
await updatePinnedUsersEvent(newEvent, newPrivateTags) await updatePinnedUsersEvent(newEvent, newPrivateTags)
} catch (error) { } catch (error) {
console.error('Failed to unpin user:', error) const errors = formatError(error)
errors.forEach((err) => {
toast.error(`Failed to unpin user: ${err}`, { duration: 10_000 })
})
} }
}, },
[ [

View file

@ -179,12 +179,20 @@ class ClientService extends EventTarget {
const successThreshold = uniqueRelayUrls.length / 3 const successThreshold = uniqueRelayUrls.length / 3
const errors: { url: string; error: any }[] = [] const errors: { url: string; error: any }[] = []
const checkCompletion = () => { const checkCompletion = (url: string, success: boolean, error?: unknown) => {
if (error) {
errors.push({ url, error })
}
if (success) {
successCount++
}
finishedCount++
if (successCount >= successThreshold) { if (successCount >= successThreshold) {
this.emitNewEvent(event, uniqueRelayUrls) this.emitNewEvent(event, uniqueRelayUrls)
resolve() resolve()
} }
if (++finishedCount >= uniqueRelayUrls.length) { if (finishedCount >= uniqueRelayUrls.length) {
reject( reject(
new AggregateError( new AggregateError(
errors.map( errors.map(
@ -204,8 +212,7 @@ class ClientService extends EventTarget {
return undefined return undefined
}) })
if (!relay) { if (!relay) {
errors.push({ url, error: new Error('Cannot connect to relay') }) checkCompletion(url, false, new Error('Cannot connect to relay'))
checkCompletion()
return return
} }
@ -216,7 +223,7 @@ class ClientService extends EventTarget {
try { try {
await relay.publish(event) await relay.publish(event)
that.trackEventSeenOn(event.id, relay) that.trackEventSeenOn(event.id, relay)
successCount++ checkCompletion(url, true)
} catch (error) { } catch (error) {
if ( if (
!hasAuthed && !hasAuthed &&
@ -227,17 +234,20 @@ class ClientService extends EventTarget {
try { try {
await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt)) await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt))
hasAuthed = true hasAuthed = true
return await publishPromise() await publishPromise().catch(() => {
// ignore
})
return
} catch (error) { } catch (error) {
errors.push({ url, error }) checkCompletion(url, false, error)
} }
} else { } else {
errors.push({ url, error }) checkCompletion(url, false, error)
} }
} }
} }
return publishPromise().finally(checkCompletion) return publishPromise()
}) })
) )
}) })