diff --git a/src/components/BookmarkButton/index.tsx b/src/components/BookmarkButton/index.tsx index fb59416..bec0015 100644 --- a/src/components/BookmarkButton/index.tsx +++ b/src/components/BookmarkButton/index.tsx @@ -6,7 +6,6 @@ import { BookmarkIcon, Loader } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' export default function BookmarkButton({ stuff }: { stuff: Event | string }) { const { t } = useTranslation() @@ -33,13 +32,8 @@ export default function BookmarkButton({ stuff }: { stuff: Event | string }) { if (isBookmarked || !event) return setUpdating(true) - try { - await addBookmark(event) - } catch (error) { - toast.error(t('Bookmark failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await addBookmark(event) + setUpdating(false) }) } @@ -49,13 +43,8 @@ export default function BookmarkButton({ stuff }: { stuff: Event | string }) { if (!isBookmarked || !event) return setUpdating(true) - try { - await removeBookmark(event) - } catch (error) { - toast.error(t('Remove bookmark failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await removeBookmark(event) + setUpdating(false) }) } diff --git a/src/components/FollowButton/index.tsx b/src/components/FollowButton/index.tsx index 82c567a..f71fdfe 100644 --- a/src/components/FollowButton/index.tsx +++ b/src/components/FollowButton/index.tsx @@ -15,7 +15,6 @@ import { useNostr } from '@/providers/NostrProvider' import { Loader } from 'lucide-react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' export default function FollowButton({ pubkey }: { pubkey: string }) { const { t } = useTranslation() @@ -33,13 +32,8 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { if (isFollowing) return setUpdating(true) - try { - await follow(pubkey) - } catch (error) { - toast.error(t('Follow failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await follow(pubkey) + setUpdating(false) }) } @@ -49,13 +43,8 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { if (!isFollowing) return setUpdating(true) - try { - await unfollow(pubkey) - } catch (error) { - toast.error(t('Unfollow failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await unfollow(pubkey) + setUpdating(false) }) } diff --git a/src/components/MailboxSetting/SaveButton.tsx b/src/components/MailboxSetting/SaveButton.tsx index 7778c13..9110346 100644 --- a/src/components/MailboxSetting/SaveButton.tsx +++ b/src/components/MailboxSetting/SaveButton.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button' import { createRelayListDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { useNostr } from '@/providers/NostrProvider' import { TMailboxRelay } from '@/types' import { CloudUpload, Loader } from 'lucide-react' @@ -25,11 +26,18 @@ export default function SaveButton({ setPushing(true) const event = createRelayListDraftEvent(mailboxRelays) - const relayListEvent = await publish(event) - await updateRelayListEvent(relayListEvent) - toast.success('Successfully saved mailbox relays') - setHasChange(false) - setPushing(false) + try { + const relayListEvent = await publish(event) + await updateRelayListEvent(relayListEvent) + toast.success('Successfully saved mailbox relays') + 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 ( diff --git a/src/components/Note/EmojiPack.tsx b/src/components/Note/EmojiPack.tsx index 404b805..4f60b92 100644 --- a/src/components/Note/EmojiPack.tsx +++ b/src/components/Note/EmojiPack.tsx @@ -7,7 +7,6 @@ import { CheckIcon, Loader, PlusIcon } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' import Image from '../Image' 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 setUpdating(true) - try { - await addEmojiPack(event) - toast.success(t('Emoji pack added')) - } catch (error) { - toast.error(t('Add emoji pack failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await addEmojiPack(event) + setUpdating(false) }) } @@ -44,14 +37,8 @@ export default function EmojiPack({ event, className }: { event: Event; classNam if (!isCollected) return setUpdating(true) - try { - await removeEmojiPack(event) - toast.success(t('Emoji pack removed')) - } catch (error) { - toast.error(t('Remove emoji pack failed') + ': ' + (error as Error).message) - } finally { - setUpdating(false) - } + await removeEmojiPack(event) + setUpdating(false) }) } diff --git a/src/components/Note/Poll.tsx b/src/components/Note/Poll.tsx index ad834f6..ef66dde 100644 --- a/src/components/Note/Poll.tsx +++ b/src/components/Note/Poll.tsx @@ -3,6 +3,7 @@ import { POLL_TYPE } from '@/constants' import { useTranslatedEvent } from '@/hooks' import { useFetchPollResults } from '@/hooks/useFetchPollResults' import { createPollResponseDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getPollMetadataFromEvent } from '@/lib/event-metadata' import { cn, isPartiallyInViewport } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -126,8 +127,10 @@ export default function Poll({ event, className }: { event: Event; className?: s setSelectedOptionIds([]) pollResultsService.addPollResponse(event.id, pubkey, selectedOptionIds) } catch (error) { - console.error('Failed to vote:', error) - toast.error('Failed to vote: ' + (error as Error).message) + const errors = formatError(error) + errors.forEach((err) => { + toast.error(`${t('Failed to vote')}: ${err}`, { duration: 10_000 }) + }) } finally { setIsVoting(false) } diff --git a/src/components/NoteOptions/ReportDialog.tsx b/src/components/NoteOptions/ReportDialog.tsx index d1fdd92..f436f8f 100644 --- a/src/components/NoteOptions/ReportDialog.tsx +++ b/src/components/NoteOptions/ReportDialog.tsx @@ -16,6 +16,7 @@ import { import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { createReportDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Loader } from 'lucide-react' @@ -94,13 +95,9 @@ function ReportContent({ event, closeDialog }: { event: NostrEvent; closeDialog: toast.success(t('Successfully report')) closeDialog() } catch (error) { - const errors = error instanceof AggregateError ? error.errors : [error] + const errors = formatError(error) errors.forEach((err) => { - toast.error( - `${t('Failed to report')}: ${err instanceof Error ? err.message : String(err)}`, - { duration: 10_000 } - ) - console.error(err) + toast.error(`${t('Failed to report')}: ${err}`, { duration: 10_000 }) }) return } finally { diff --git a/src/components/NoteOptions/useMenuActions.tsx b/src/components/NoteOptions/useMenuActions.tsx index d35dfcb..8ad095f 100644 --- a/src/components/NoteOptions/useMenuActions.tsx +++ b/src/components/NoteOptions/useMenuActions.tsx @@ -1,3 +1,4 @@ +import { formatError } from '@/lib/error' import { getNoteBech32Id, isProtectedEvent } from '@/lib/event' import { toNjump } from '@/lib/link' import { pubkeyToNpub } from '@/lib/pubkey' @@ -117,7 +118,7 @@ export function useMenuActions({ error: (err) => { return t('Failed to republish to relay set: {{name}}. Error: {{error}}', { name: set.name, - error: err.message + error: formatError(err).join('; ') }) } }) @@ -147,7 +148,7 @@ export function useMenuActions({ error: (err) => { return t('Failed to republish to relay: {{url}}. Error: {{error}}', { url: simplifyUrl(relay), - error: err.message + error: formatError(err).join('; ') }) } }) diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index b235274..e4bf863 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -26,6 +26,7 @@ import PostOptions from './PostOptions' import PostRelaySelector from './PostRelaySelector' import PostTextarea, { TPostTextareaHandle } from './PostTextarea' import Uploader from './Uploader' +import { formatError } from '@/lib/error' export default function PostContent({ defaultContent = '', @@ -160,13 +161,9 @@ export default function PostContent({ toast.success(t('Post successful'), { duration: 2000 }) close() } catch (error) { - const errors = error instanceof AggregateError ? error.errors : [error] + const errors = formatError(error) errors.forEach((err) => { - toast.error( - `${t('Failed to post')}: ${err instanceof Error ? err.message : String(err)}`, - { duration: 10_000 } - ) - console.error(err) + toast.error(`${t('Failed to post')}: ${err}`, { duration: 10_000 }) }) return } finally { diff --git a/src/components/RelayInfo/ReviewEditor.tsx b/src/components/RelayInfo/ReviewEditor.tsx index 056427c..1e9a593 100644 --- a/src/components/RelayInfo/ReviewEditor.tsx +++ b/src/components/RelayInfo/ReviewEditor.tsx @@ -1,6 +1,7 @@ import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { createRelayReviewDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { useNostr } from '@/providers/NostrProvider' import { Loader2, Star } from 'lucide-react' import { NostrEvent } from 'nostr-tools' @@ -32,12 +33,10 @@ export default function ReviewEditor({ const evt = await publish(draftEvent) onReviewed(evt) } catch (error) { - if (error instanceof AggregateError) { - error.errors.forEach((e) => toast.error(`${t('Failed to review')}: ${e.message}`)) - } else if (error instanceof Error) { - toast.error(`${t('Failed to review')}: ${error.message}`) - } - console.error(error) + const errors = formatError(error) + errors.forEach((err) => { + toast.error(`${t('Failed to review')}: ${err}`, { duration: 10_000 }) + }) return } finally { setSubmitting(false) diff --git a/src/components/RelayMembershipControl/JoinDialog.tsx b/src/components/RelayMembershipControl/JoinDialog.tsx index d7b276f..73c2b7a 100644 --- a/src/components/RelayMembershipControl/JoinDialog.tsx +++ b/src/components/RelayMembershipControl/JoinDialog.tsx @@ -19,6 +19,7 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { createJoinDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import relayMembershipService from '@/services/relay-membership.service' @@ -57,13 +58,9 @@ export default function JoinDialog({ setInviteCode('') setShowJoinDialog(false) } catch (error) { - const errors = error instanceof AggregateError ? error.errors : [error] + const errors = formatError(error) errors.forEach((err) => { - toast.error( - `${t('Failed to send join request')}: ${err instanceof Error ? err.message : String(err)}`, - { duration: 10_000 } - ) - console.error(err) + toast.error(`${t('Failed to send join request')}: ${err}`, { duration: 10_000 }) }) return } finally { diff --git a/src/components/RelayMembershipControl/index.tsx b/src/components/RelayMembershipControl/index.tsx index ecb233c..2b56998 100644 --- a/src/components/RelayMembershipControl/index.tsx +++ b/src/components/RelayMembershipControl/index.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button' import { createJoinDraftEvent, createLeaveDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { checkNip43Support } from '@/lib/relay' import { useNostr } from '@/providers/NostrProvider' import relayMembershipService from '@/services/relay-membership.service' @@ -72,7 +73,11 @@ export default function RelayMembershipControl({ toast.success(t('Join request sent successfully')) await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey) 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) } finally { setIsLoading(false) diff --git a/src/components/StuffStats/LikeButton.tsx b/src/components/StuffStats/LikeButton.tsx index 9399aaa..2e34498 100644 --- a/src/components/StuffStats/LikeButton.tsx +++ b/src/components/StuffStats/LikeButton.tsx @@ -23,6 +23,8 @@ import Emoji from '../Emoji' import EmojiPicker from '../EmojiPicker' import SuggestedEmojis from '../SuggestedEmojis' import { formatCount } from './utils' +import { formatError } from '@/lib/error' +import { toast } from 'sonner' export default function LikeButton({ stuff }: { stuff: Event | string }) { const { t } = useTranslation() @@ -90,7 +92,10 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) { const evt = await publish(reaction, { additionalRelayUrls: seenOn }) stuffStatsService.updateStuffStatsByEvents([evt]) } 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 { setLiking(false) clearTimeout(timer) diff --git a/src/components/StuffStats/Likes.tsx b/src/components/StuffStats/Likes.tsx index e429120..0acde9a 100644 --- a/src/components/StuffStats/Likes.tsx +++ b/src/components/StuffStats/Likes.tsx @@ -5,6 +5,7 @@ import { createExternalContentReactionDraftEvent, createReactionDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getDefaultRelayUrls } from '@/lib/relay' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -14,6 +15,7 @@ import { TEmoji } from '@/types' import { Loader } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' import Emoji from '../Emoji' 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 }) stuffStatsService.updateStuffStatsByEvents([evt]) } catch (error) { - console.error('like failed', error) + const errors = formatError(error) + errors.forEach((err) => { + toast.error(`Failed to like: ${err}`, { duration: 10_000 }) + }) } finally { setLiking(null) clearTimeout(timer) diff --git a/src/components/StuffStats/RepostButton.tsx b/src/components/StuffStats/RepostButton.tsx index 6d51fa1..88aae57 100644 --- a/src/components/StuffStats/RepostButton.tsx +++ b/src/components/StuffStats/RepostButton.tsx @@ -22,6 +22,8 @@ import { useTranslation } from 'react-i18next' import PostEditor from '../PostEditor' import { formatCount } from './utils' 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 }) { const { t } = useTranslation() @@ -87,7 +89,10 @@ export default function RepostButton({ stuff }: { stuff: Event | string }) { const evt = await publish(repost) stuffStatsService.updateStuffStatsByEvents([evt]) } 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 { setReposting(false) clearTimeout(timer) diff --git a/src/lib/error.ts b/src/lib/error.ts new file mode 100644 index 0000000..0e250bf --- /dev/null +++ b/src/lib/error.ts @@ -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) + }) +} diff --git a/src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx b/src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx index 44f1717..b392c79 100644 --- a/src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx +++ b/src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx @@ -4,6 +4,7 @@ import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { RECOMMENDED_BLOSSOM_SERVERS } from '@/constants' import { createBlossomServerListDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getServersFromServerTags } from '@/lib/tag' import { normalizeHttpUrl } from '@/lib/url' import { cn } from '@/lib/utils' @@ -13,6 +14,7 @@ import { AlertCircle, ArrowUpToLine, Loader, X } from 'lucide-react' import { Event } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' export default function BlossomServerListSetting() { const { t } = useTranslation() @@ -48,7 +50,10 @@ export default function BlossomServerListSetting() { setBlossomServerListEvent(newEvent) setUrl('') } 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 { setAdding(false) } @@ -72,7 +77,10 @@ export default function BlossomServerListSetting() { await client.updateBlossomServerListEventCache(newEvent) setBlossomServerListEvent(newEvent) } 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 { setRemovingIndex(-1) } @@ -88,7 +96,10 @@ export default function BlossomServerListSetting() { await client.updateBlossomServerListEventCache(newEvent) setBlossomServerListEvent(newEvent) } 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 { setMovingIndex(-1) } diff --git a/src/pages/secondary/ProfileEditorPage/index.tsx b/src/pages/secondary/ProfileEditorPage/index.tsx index 9b47c27..6f83806 100644 --- a/src/pages/secondary/ProfileEditorPage/index.tsx +++ b/src/pages/secondary/ProfileEditorPage/index.tsx @@ -7,6 +7,7 @@ import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { createProfileDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { generateImageByPubkey } from '@/lib/pubkey' import { isEmail } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' @@ -14,6 +15,7 @@ import { useNostr } from '@/providers/NostrProvider' import { Loader, Upload } from 'lucide-react' import { forwardRef, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { const { t } = useTranslation() @@ -97,10 +99,17 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { JSON.stringify(newProfileContent), profileEvent?.tags ) - const newProfileEvent = await publish(profileDraftEvent) - await updateProfileEvent(newProfileEvent) - setSaving(false) - pop() + try { + const newProfileEvent = await publish(profileDraftEvent) + await updateProfileEvent(newProfileEvent) + 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 }) => { diff --git a/src/pages/secondary/RizfulPage/index.tsx b/src/pages/secondary/RizfulPage/index.tsx index 03948a5..4693f1f 100644 --- a/src/pages/secondary/RizfulPage/index.tsx +++ b/src/pages/secondary/RizfulPage/index.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { createProfileDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { isEmail } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { useZap } from '@/providers/ZapProvider' @@ -65,8 +66,13 @@ const RizfulPage = forwardRef(({ index }: { index?: number }, ref) => { ) const newProfileEvent = await publish(profileDraftEvent) await updateProfileEvent(newProfileEvent) - } catch (e: unknown) { - toast.error(e instanceof Error ? e.message : String(e)) + } catch (error) { + const errors = formatError(error) + errors.forEach((err) => { + toast.error(`${t('Failed to update profile with Lightning Address')}: ${err}`, { + duration: 10_000 + }) + }) } } diff --git a/src/pages/secondary/WalletPage/LightningAddressInput.tsx b/src/pages/secondary/WalletPage/LightningAddressInput.tsx index 6fb6022..79df0e5 100644 --- a/src/pages/secondary/WalletPage/LightningAddressInput.tsx +++ b/src/pages/secondary/WalletPage/LightningAddressInput.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { createProfileDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { isEmail } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { Loader } from 'lucide-react' @@ -45,9 +46,19 @@ export default function LightningAddressInput() { JSON.stringify(profileContent), profileEvent?.tags ) - const newProfileEvent = await publish(profileDraftEvent) - await updateProfileEvent(newProfileEvent) - setSaving(false) + try { + const newProfileEvent = await publish(profileDraftEvent) + 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 ( diff --git a/src/providers/BookmarksProvider.tsx b/src/providers/BookmarksProvider.tsx index 526cc4a..ca49610 100644 --- a/src/providers/BookmarksProvider.tsx +++ b/src/providers/BookmarksProvider.tsx @@ -1,8 +1,10 @@ import { buildATag, buildETag, createBookmarkDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import client from '@/services/client.service' import { Event } from 'nostr-tools' import { createContext, useContext } from 'react' +import { toast } from 'sonner' import { useNostr } from './NostrProvider' type TBookmarksContext = { @@ -45,8 +47,15 @@ export function BookmarksProvider({ children }: { children: React.ReactNode }) { [...currentTags, isReplaceable ? buildATag(event) : buildETag(event.id, event.pubkey)], bookmarkListEvent?.content ) - const newBookmarkEvent = await publish(newBookmarkDraftEvent) - await updateBookmarkListEvent(newBookmarkEvent) + try { + 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) => { @@ -64,8 +73,15 @@ export function BookmarksProvider({ children }: { children: React.ReactNode }) { if (newTags.length === bookmarkListEvent.tags.length) return const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content) - const newBookmarkEvent = await publish(newBookmarkDraftEvent) - await updateBookmarkListEvent(newBookmarkEvent) + try { + 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 ( diff --git a/src/providers/EmojiPackProvider.tsx b/src/providers/EmojiPackProvider.tsx index 9134b5c..e4ba015 100644 --- a/src/providers/EmojiPackProvider.tsx +++ b/src/providers/EmojiPackProvider.tsx @@ -1,8 +1,10 @@ import { buildATag, createUserEmojiListDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getReplaceableCoordinateFromEvent } from '@/lib/event' import client from '@/services/client.service' import { Event, kinds } from 'nostr-tools' import { createContext, useContext, useMemo } from 'react' +import { toast } from 'sonner' import { useNostr } from './NostrProvider' type TEmojiPackContext = { @@ -54,8 +56,15 @@ export function EmojiPackProvider({ children }: { children: React.ReactNode }) { [...currentTags, buildATag(event)], userEmojiListEvent?.content ) - const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) - await updateUserEmojiListEvent(newUserEmojiListEvent) + try { + 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) => { @@ -72,8 +81,15 @@ export function EmojiPackProvider({ children }: { children: React.ReactNode }) { newTags, userEmojiListEvent.content ) - const newUserEmojiListEvent = await publish(newUserEmojiListDraftEvent) - await updateUserEmojiListEvent(newUserEmojiListEvent) + try { + 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 ( diff --git a/src/providers/FavoriteRelaysProvider.tsx b/src/providers/FavoriteRelaysProvider.tsx index 6c8e438..fcf4d79 100644 --- a/src/providers/FavoriteRelaysProvider.tsx +++ b/src/providers/FavoriteRelaysProvider.tsx @@ -1,4 +1,5 @@ import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getReplaceableEventIdentifier } from '@/lib/event' import { getRelaySetFromEvent } from '@/lib/event-metadata' import { randomString } from '@/lib/random' @@ -10,6 +11,7 @@ import storage from '@/services/local-storage.service' import { TRelaySet } from '@/types' import { Event, kinds } from 'nostr-tools' import { createContext, useContext, useEffect, useState } from 'react' +import { toast } from 'sonner' import { useNostr } from './NostrProvider' type TFavoriteRelaysContext = { @@ -146,8 +148,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode [...favoriteRelays, ...normalizedUrls], relaySetEvents ) - const newFavoriteRelaysEvent = await publish(draftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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[]) => { @@ -160,8 +169,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode favoriteRelays.filter((url) => !normalizedUrls.includes(url)), relaySetEvents ) - const newFavoriteRelaysEvent = await publish(draftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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[] = []) => { @@ -174,15 +190,22 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode name: relaySetName, relayUrls: normalizedUrls }) - const newRelaySetEvent = await publish(relaySetDraftEvent) - await indexedDb.putReplaceableEvent(newRelaySetEvent) + try { + const newRelaySetEvent = await publish(relaySetDraftEvent) + await indexedDb.putReplaceableEvent(newRelaySetEvent) - const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [ - ...relaySetEvents, - newRelaySetEvent - ]) - const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [ + ...relaySetEvents, + newRelaySetEvent + ]) + const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) + 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[]) => { @@ -190,8 +213,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode ...relaySetEvents, ...newRelaySetEvents ]) - const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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) => { @@ -201,30 +231,52 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode if (newRelaySetEvents.length === relaySetEvents.length) return const draftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, newRelaySetEvents) - const newFavoriteRelaysEvent = await publish(draftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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 draftEvent = createRelaySetDraftEvent(newSet) - const newRelaySetEvent = await publish(draftEvent) - await indexedDb.putReplaceableEvent(newRelaySetEvent) - setRelaySetEvents((prev) => { - return prev.map((event) => { - if (getReplaceableEventIdentifier(event) === newSet.id) { - return newRelaySetEvent - } - return event + try { + const newRelaySetEvent = await publish(draftEvent) + await indexedDb.putReplaceableEvent(newRelaySetEvent) + + setRelaySetEvents((prev) => { + 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[]) => { setFavoriteRelays(reorderedRelays) const draftEvent = createFavoriteRelaysDraftEvent(reorderedRelays, relaySetEvents) - const newFavoriteRelaysEvent = await publish(draftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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[]) => { @@ -233,8 +285,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode favoriteRelays, reorderedSets.map((set) => set.aTag) ) - const newFavoriteRelaysEvent = await publish(draftEvent) - updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + try { + 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 ( diff --git a/src/providers/FollowListProvider.tsx b/src/providers/FollowListProvider.tsx index b321098..fc747ec 100644 --- a/src/providers/FollowListProvider.tsx +++ b/src/providers/FollowListProvider.tsx @@ -4,6 +4,8 @@ import client from '@/services/client.service' import { createContext, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useNostr } from './NostrProvider' +import { formatError } from '@/lib/error' +import { toast } from 'sonner' type TFollowListContext = { followingSet: Set @@ -44,8 +46,15 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) (followListEvent?.tags ?? []).concat([['p', pubkey]]), followListEvent?.content ) - const newFollowListEvent = await publish(newFollowListDraftEvent) - await updateFollowListEvent(newFollowListEvent) + try { + 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) => { @@ -58,8 +67,15 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) followListEvent.tags.filter(([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey), followListEvent.content ) - const newFollowListEvent = await publish(newFollowListDraftEvent) - await updateFollowListEvent(newFollowListEvent) + try { + 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 ( diff --git a/src/providers/MuteListProvider.tsx b/src/providers/MuteListProvider.tsx index edf551b..0daab73 100644 --- a/src/providers/MuteListProvider.tsx +++ b/src/providers/MuteListProvider.tsx @@ -1,4 +1,5 @@ import { createMuteListDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getPubkeysFromPTags } from '@/lib/tag' import client from '@/services/client.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 event = await publish(newMuteListDraftEvent) - toast.success(t('Successfully updated mute list')) return event } @@ -147,7 +147,10 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { const privateTags = await getPrivateTags(newMuteListEvent) await updateMuteListEvent(newMuteListEvent, privateTags) } 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 { setChanging(false) } @@ -170,7 +173,10 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText) await updateMuteListEvent(newMuteListEvent, newPrivateTags) } 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 { setChanging(false) } @@ -196,6 +202,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { cipherText ) 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 { setChanging(false) } @@ -223,6 +234,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { cipherText ) 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 { setChanging(false) } @@ -248,6 +264,11 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags)) const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText) 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 { setChanging(false) } diff --git a/src/providers/PinListProvider.tsx b/src/providers/PinListProvider.tsx index 9974dae..dd37215 100644 --- a/src/providers/PinListProvider.tsx +++ b/src/providers/PinListProvider.tsx @@ -1,5 +1,6 @@ import { MAX_PINNED_NOTES } from '@/constants' import { buildETag, createPinListDraftEvent } from '@/lib/draft-event' +import { formatError } from '@/lib/error' import { getPinnedEventHexIdSetFromPinListEvent } from '@/lib/event-metadata' import client from '@/services/client.service' import { Event, kinds } from 'nostr-tools' @@ -67,7 +68,7 @@ export function PinListProvider({ children }: { children: React.ReactNode }) { const { unwrap } = toast.promise(_pin, { loading: t('Pinning...'), 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() } @@ -92,7 +93,7 @@ export function PinListProvider({ children }: { children: React.ReactNode }) { const { unwrap } = toast.promise(_unpin, { loading: t('Unpinning...'), 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() } diff --git a/src/providers/PinnedUsersProvider.tsx b/src/providers/PinnedUsersProvider.tsx index 386fb89..42f6899 100644 --- a/src/providers/PinnedUsersProvider.tsx +++ b/src/providers/PinnedUsersProvider.tsx @@ -1,8 +1,10 @@ import { ExtendedKind } from '@/constants' +import { formatError } from '@/lib/error' import { getPubkeysFromPTags } from '@/lib/tag' import indexedDb from '@/services/indexed-db.service' import { Event } from 'nostr-tools' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' import { z } from 'zod' import { useNostr } from './NostrProvider' @@ -105,7 +107,10 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode }) const newEvent = await publish(draftEvent) await updatePinnedUsersEvent(newEvent, privateTags) } 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] @@ -130,7 +135,10 @@ export function PinnedUsersProvider({ children }: { children: React.ReactNode }) const newEvent = await publish(draftEvent) await updatePinnedUsersEvent(newEvent, newPrivateTags) } 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 }) + }) } }, [ diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 08e205a..5dfb270 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -179,12 +179,20 @@ class ClientService extends EventTarget { const successThreshold = uniqueRelayUrls.length / 3 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) { this.emitNewEvent(event, uniqueRelayUrls) resolve() } - if (++finishedCount >= uniqueRelayUrls.length) { + if (finishedCount >= uniqueRelayUrls.length) { reject( new AggregateError( errors.map( @@ -204,8 +212,7 @@ class ClientService extends EventTarget { return undefined }) if (!relay) { - errors.push({ url, error: new Error('Cannot connect to relay') }) - checkCompletion() + checkCompletion(url, false, new Error('Cannot connect to relay')) return } @@ -216,7 +223,7 @@ class ClientService extends EventTarget { try { await relay.publish(event) that.trackEventSeenOn(event.id, relay) - successCount++ + checkCompletion(url, true) } catch (error) { if ( !hasAuthed && @@ -227,17 +234,20 @@ class ClientService extends EventTarget { try { await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt)) hasAuthed = true - return await publishPromise() + await publishPromise().catch(() => { + // ignore + }) + return } catch (error) { - errors.push({ url, error }) + checkCompletion(url, false, error) } } else { - errors.push({ url, error }) + checkCompletion(url, false, error) } } } - return publishPromise().finally(checkCompletion) + return publishPromise() }) ) })