diff --git a/package-lock.json b/package-lock.json index 225e22b..62bffde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "i18next-browser-languagedetector": "^8.0.4", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", + "next-themes": "^0.4.6", "nostr-tools": "^2.13.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", @@ -54,6 +55,7 @@ "react-dom": "^18.3.1", "react-i18next": "^15.2.0", "react-simple-pull-to-refresh": "^1.3.3", + "sonner": "^2.0.5", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "tippy.js": "^6.3.7", @@ -7584,6 +7586,16 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -8894,6 +8906,16 @@ "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true }, + "node_modules/sonner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.5.tgz", + "integrity": "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", diff --git a/package.json b/package.json index ac29732..297d68f 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "i18next-browser-languagedetector": "^8.0.4", "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", + "next-themes": "^0.4.6", "nostr-tools": "^2.13.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", @@ -64,6 +65,7 @@ "react-dom": "^18.3.1", "react-i18next": "^15.2.0", "react-simple-pull-to-refresh": "^1.3.3", + "sonner": "^2.0.5", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "tippy.js": "^6.3.7", diff --git a/src/App.tsx b/src/App.tsx index 7d0d7e9..77299b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,8 @@ import 'yet-another-react-lightbox/styles.css' import './index.css' -import { Toaster } from '@/components/ui/toaster' import { ThemeProvider } from '@/providers/ThemeProvider' +import { Toaster } from './components/ui/sonner' import { PageManager } from './PageManager' import { AutoplayProvider } from './providers/AutoplayProvider' import { BookmarksProvider } from './providers/BookmarksProvider' diff --git a/src/components/BookmarkButton/index.tsx b/src/components/BookmarkButton/index.tsx index 9353bd1..3991243 100644 --- a/src/components/BookmarkButton/index.tsx +++ b/src/components/BookmarkButton/index.tsx @@ -1,14 +1,13 @@ -import { useToast } from '@/hooks' import { useBookmarks } from '@/providers/BookmarksProvider' import { useNostr } from '@/providers/NostrProvider' import { BookmarkIcon, Loader } from 'lucide-react' +import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Event } from 'nostr-tools' +import { toast } from 'sonner' export default function BookmarkButton({ event }: { event: Event }) { const { t } = useTranslation() - const { toast } = useToast() const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr() const { addBookmark, removeBookmark } = useBookmarks() const [updating, setUpdating] = useState(false) @@ -28,11 +27,7 @@ export default function BookmarkButton({ event }: { event: Event }) { try { await addBookmark(event) } catch (error) { - toast({ - title: t('Bookmark failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(t('Bookmark failed') + ': ' + (error as Error).message) } finally { setUpdating(false) } @@ -48,11 +43,7 @@ export default function BookmarkButton({ event }: { event: Event }) { try { await removeBookmark(event) } catch (error) { - toast({ - title: t('Remove bookmark failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(t('Remove bookmark failed') + ': ' + (error as Error).message) } finally { setUpdating(false) } diff --git a/src/components/Embedded/EmbeddedLNInvoice.tsx b/src/components/Embedded/EmbeddedLNInvoice.tsx index d933e1e..e079a28 100644 --- a/src/components/Embedded/EmbeddedLNInvoice.tsx +++ b/src/components/Embedded/EmbeddedLNInvoice.tsx @@ -1,15 +1,14 @@ import { Button } from '@/components/ui/button' -import { useToast } from '@/hooks' import { formatAmount, getAmountFromInvoice } from '@/lib/lightning' import { useNostr } from '@/providers/NostrProvider' import lightning from '@/services/lightning.service' import { Loader, Zap } from 'lucide-react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' export function EmbeddedLNInvoice({ invoice }: { invoice: string }) { const { t } = useTranslation() - const { toast } = useToast() const { checkLogin, pubkey } = useNostr() const [paying, setPaying] = useState(false) @@ -29,11 +28,7 @@ export function EmbeddedLNInvoice({ invoice }: { invoice: string }) { return } } catch (error) { - toast({ - title: t('Lightning payment failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(t('Lightning payment failed') + ': ' + (error as Error).message) } finally { setPaying(false) } diff --git a/src/components/FollowButton/index.tsx b/src/components/FollowButton/index.tsx index 7896c0c..d48dd32 100644 --- a/src/components/FollowButton/index.tsx +++ b/src/components/FollowButton/index.tsx @@ -10,16 +10,15 @@ import { AlertDialogTrigger } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { useToast } from '@/hooks' import { useFollowList } from '@/providers/FollowListProvider' 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() - const { toast } = useToast() const { pubkey: accountPubkey, checkLogin } = useNostr() const { followings, follow, unfollow } = useFollowList() const [updating, setUpdating] = useState(false) @@ -37,11 +36,7 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { try { await follow(pubkey) } catch (error) { - toast({ - title: t('Follow failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(t('Follow failed') + ': ' + (error as Error).message) } finally { setUpdating(false) } @@ -57,11 +52,7 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { try { await unfollow(pubkey) } catch (error) { - toast({ - title: t('Unfollow failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(t('Unfollow failed') + ': ' + (error as Error).message) } finally { setUpdating(false) } diff --git a/src/components/MailboxSetting/SaveButton.tsx b/src/components/MailboxSetting/SaveButton.tsx index 221b9ad..7de6134 100644 --- a/src/components/MailboxSetting/SaveButton.tsx +++ b/src/components/MailboxSetting/SaveButton.tsx @@ -1,10 +1,10 @@ -import { useToast } from '@/hooks' +import { Button } from '@/components/ui/button' import { createRelayListDraftEvent } from '@/lib/draft-event' import { useNostr } from '@/providers/NostrProvider' import { TMailboxRelay } from '@/types' import { CloudUpload, Loader } from 'lucide-react' import { useState } from 'react' -import { Button } from '../ui/button' +import { toast } from 'sonner' export default function SaveButton({ mailboxRelays, @@ -15,7 +15,6 @@ export default function SaveButton({ hasChange: boolean setHasChange: (hasChange: boolean) => void }) { - const { toast } = useToast() const { pubkey, publish, updateRelayListEvent } = useNostr() const [pushing, setPushing] = useState(false) @@ -26,10 +25,7 @@ export default function SaveButton({ const event = createRelayListDraftEvent(mailboxRelays) const relayListEvent = await publish(event) await updateRelayListEvent(relayListEvent) - toast({ - title: 'Save Successful', - description: 'Successfully saved mailbox relays' - }) + toast.success('Successfully saved mailbox relays') setHasChange(false) setPushing(false) } diff --git a/src/components/MuteButton/index.tsx b/src/components/MuteButton/index.tsx index e32c144..3a8ae4d 100644 --- a/src/components/MuteButton/index.tsx +++ b/src/components/MuteButton/index.tsx @@ -6,18 +6,17 @@ import { DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { useToast } from '@/hooks' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { BellOff, Loader } from 'lucide-react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' export default function MuteButton({ pubkey }: { pubkey: string }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() - const { toast } = useToast() const { pubkey: accountPubkey, checkLogin } = useNostr() const { mutePubkeys, changing, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() @@ -39,11 +38,7 @@ export default function MuteButton({ pubkey }: { pubkey: string }) { await mutePubkeyPublicly(pubkey) } } catch (error) { - toast({ - title: t('Mute failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(`${t('Mute failed')}: ${(error as Error).message}`) } finally { setUpdating(false) } @@ -59,11 +54,7 @@ export default function MuteButton({ pubkey }: { pubkey: string }) { try { await unmutePubkey(pubkey) } catch (error) { - toast({ - title: t('Unmute failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(`${t('Unmute failed')}: ${(error as Error).message}`) } finally { setUpdating(false) } diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx index 4030247..4faed6c 100644 --- a/src/components/NoteStats/ZapButton.tsx +++ b/src/components/NoteStats/ZapButton.tsx @@ -1,4 +1,3 @@ -import { useToast } from '@/hooks' import { getLightningAddressFromProfile } from '@/lib/lightning' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -10,11 +9,11 @@ import { Loader, Zap } from 'lucide-react' import { Event } from 'nostr-tools' import { MouseEvent, TouchEvent, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import ZapDialog from '../ZapDialog' export default function ZapButton({ event }: { event: Event }) { const { t } = useTranslation() - const { toast } = useToast() const { checkLogin, pubkey } = useNostr() const { noteStatsMap, addZap } = useNoteStats() const { defaultZapSats, defaultZapComment, quickZap } = useZap() @@ -60,11 +59,7 @@ export default function ZapButton({ event }: { event: Event }) { } addZap(event.id, zapResult.invoice, defaultZapSats, defaultZapComment) } catch (error) { - toast({ - title: t('Zap failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(`${t('Zap failed')}: ${(error as Error).message}`) } finally { setZapping(false) } diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 50f01b0..a7c4adc 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -1,7 +1,6 @@ import Note from '@/components/Note' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' -import { useToast } from '@/hooks/use-toast' import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event' import { isTouchDevice } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -10,6 +9,7 @@ import { ImageUp, LoaderCircle, Settings, Smile } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import EmojiPickerDialog from '../EmojiPickerDialog' import Mentions from './Mentions' import { usePostEditor } from './PostEditorProvider' @@ -28,7 +28,6 @@ export default function PostContent({ close: () => void }) { const { t } = useTranslation() - const { toast } = useToast() const { publish, checkLogin } = useNostr() const { uploadingFiles, setUploadingFiles } = usePostEditor() const [text, setText] = useState('') @@ -63,29 +62,16 @@ export default function PostContent({ close() } catch (error) { if (error instanceof AggregateError) { - error.errors.forEach((e) => - toast({ - variant: 'destructive', - title: t('Failed to post'), - description: e.message - }) - ) + error.errors.forEach((e) => toast.error(`${t('Failed to post')}: ${e.message}`)) } else if (error instanceof Error) { - toast({ - variant: 'destructive', - title: t('Failed to post'), - description: error.message - }) + toast.error(`${t('Failed to post')}: ${error.message}`) } console.error(error) return } finally { setPosting(false) } - toast({ - title: t('Post successful'), - description: t('Your post has been published') - }) + toast.success(t('Post successful'), { duration: 2000 }) }) } diff --git a/src/components/PostEditor/Uploader.tsx b/src/components/PostEditor/Uploader.tsx index a80b8eb..be42bd0 100644 --- a/src/components/PostEditor/Uploader.tsx +++ b/src/components/PostEditor/Uploader.tsx @@ -1,6 +1,6 @@ -import { useToast } from '@/hooks/use-toast' import mediaUpload from '@/services/media-upload.service' import { useRef } from 'react' +import { toast } from 'sonner' export default function Uploader({ children, @@ -15,7 +15,6 @@ export default function Uploader({ className?: string accept?: string }) { - const { toast } = useToast() const fileInputRef = useRef(null) const handleFileChange = async (event: React.ChangeEvent) => { @@ -30,11 +29,7 @@ export default function Uploader({ } } catch (error) { console.error('Error uploading file', error) - toast({ - variant: 'destructive', - title: 'Failed to upload file', - description: (error as Error).message - }) + toast.error(`Failed to upload file: ${(error as Error).message}`) if (fileInputRef.current) { fileInputRef.current.value = '' } diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx index 5465f57..e7e1d25 100644 --- a/src/components/ZapDialog/index.tsx +++ b/src/components/ZapDialog/index.tsx @@ -15,7 +15,6 @@ import { } from '@/components/ui/drawer' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useToast } from '@/hooks' import { useNostr } from '@/providers/NostrProvider' import { useNoteStats } from '@/providers/NoteStatsProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -24,6 +23,7 @@ import lightning from '@/services/lightning.service' import { Loader } from 'lucide-react' import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import UserAvatar from '../UserAvatar' import Username from '../Username' @@ -134,7 +134,6 @@ function ZapDialogContent({ defaultComment?: string }) { const { t } = useTranslation() - const { toast } = useToast() const { pubkey } = useNostr() const { defaultZapSats, defaultZapComment } = useZap() const { addZap } = useNoteStats() @@ -159,11 +158,7 @@ function ZapDialogContent({ addZap(eventId, zapResult.invoice, sats, comment) } } catch (error) { - toast({ - title: t('Zap failed'), - description: (error as Error).message, - variant: 'destructive' - }) + toast.error(`${t('Zap failed')}: ${(error as Error).message}`) } finally { setZapping(false) } diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..e0739b6 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +// import { useTheme } from "next-themes" +import { useTheme } from '@/providers/ThemeProvider' +import { Toaster as Sonner } from 'sonner' + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + // const { theme = "system" } = useTheme() + const { themeSetting } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx deleted file mode 100644 index b7d309b..0000000 --- a/src/components/ui/toast.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import * as React from 'react' -import * as ToastPrimitives from '@radix-ui/react-toast' -import { cva, type VariantProps } from 'class-variance-authority' -import { X } from 'lucide-react' - -import { cn } from '@/lib/utils' - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', - { - variants: { - variant: { - default: 'border bg-background text-foreground', - destructive: - 'destructive group border-destructive bg-destructive text-destructive-foreground' - } - }, - defaultVariants: { - variant: 'default' - } - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction -} diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx deleted file mode 100644 index c9b1d27..0000000 --- a/src/components/ui/toaster.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useToast } from '@/hooks/use-toast' -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport -} from '@/components/ui/toast' - -export function Toaster() { - const { toasts } = useToast() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ) - })} - -
- ) -} diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index f3be2c3..20948f7 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,4 +1,3 @@ -export * from './use-toast' export * from './useFetchEvent' export * from './useFetchFollowings' export * from './useFetchNip05' diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts deleted file mode 100644 index 104bcf3..0000000 --- a/src/hooks/use-toast.ts +++ /dev/null @@ -1,189 +0,0 @@ -'use client' - -// Inspired by react-hot-toast library -import * as React from 'react' - -import type { ToastActionElement, ToastProps } from '@/components/ui/toast' - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST' -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType['ADD_TOAST'] - toast: ToasterToast - } - | { - type: ActionType['UPDATE_TOAST'] - toast: Partial - } - | { - type: ActionType['DISMISS_TOAST'] - toastId?: ToasterToast['id'] - } - | { - type: ActionType['REMOVE_TOAST'] - toastId?: ToasterToast['id'] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: 'REMOVE_TOAST', - toastId: toastId - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'ADD_TOAST': - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) - } - - case 'UPDATE_TOAST': - return { - ...state, - toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)) - } - - case 'DISMISS_TOAST': { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false - } - : t - ) - } - } - case 'REMOVE_TOAST': - if (action.toastId === undefined) { - return { - ...state, - toasts: [] - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId) - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: 'UPDATE_TOAST', - toast: { ...props, id } - }) - const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }) - - dispatch({ - type: 'ADD_TOAST', - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - } - } - }) - - return { - id: id, - dismiss, - update - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }) - } -} - -export { useToast, toast } diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index b53808b..98ab2d8 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -240,6 +240,8 @@ export default { 'Mute user privately': 'كتم المستخدم بشكل خاص', 'Mute user publicly': 'كتم المستخدم علنياً', Quotes: 'الاقتباسات', - 'Lightning Invoice': 'فاتورة Lightning' + 'Lightning Invoice': 'فاتورة Lightning', + 'Bookmark failed': 'فشل في الإشارة المرجعية', + 'Remove bookmark failed': 'فشل في إزالة الإشارة المرجعية' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 9967d82..83f0833 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -247,6 +247,8 @@ export default { 'Mute user privately': 'Benutzer privat stummschalten', 'Mute user publicly': 'Benutzer öffentlich stummschalten', Quotes: 'Zitate', - 'Lightning Invoice': 'Lightning-Rechnung' + 'Lightning Invoice': 'Lightning-Rechnung', + 'Bookmark failed': 'Bookmark fehlgeschlagen', + 'Remove bookmark failed': 'Bookmark entfernen fehlgeschlagen' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index c9a9377..ceebf17 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -240,6 +240,8 @@ export default { 'Mute user privately': 'Mute user privately', 'Mute user publicly': 'Mute user publicly', Quotes: 'Quotes', - 'Lightning Invoice': 'Lightning Invoice' + 'Lightning Invoice': 'Lightning Invoice', + 'Bookmark failed': 'Bookmark failed', + 'Remove bookmark failed': 'Remove bookmark failed' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index d30d43b..7fa3ca9 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -245,6 +245,8 @@ export default { 'Mute user privately': 'Silenciar usuario en privado', 'Mute user publicly': 'Silenciar usuario públicamente', Quotes: 'Citas', - 'Lightning Invoice': 'Factura Lightning' + 'Lightning Invoice': 'Factura Lightning', + 'Bookmark failed': 'Error al marcar', + 'Remove bookmark failed': 'Error al quitar marcador' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 0ebdb63..8da7a2d 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -245,6 +245,8 @@ export default { 'Mute user privately': 'Mettre l’utilisateur en sourdine en privé', 'Mute user publicly': 'Mettre l’utilisateur en sourdine publiquement', Quotes: 'Citations', - 'Lightning Invoice': 'Facture Lightning' + 'Lightning Invoice': 'Facture Lightning', + 'Bookmark failed': 'Échec de la mise en favori', + 'Remove bookmark failed': 'Échec de la suppression du favori' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index e743790..cd9e800 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -244,6 +244,8 @@ export default { 'Mute user privately': 'Zittisci utente privatamente', 'Mute user publicly': 'Zittisci utente pubblicamente', Quotes: 'Citazioni', - 'Lightning Invoice': 'Fattura Lightning' + 'Lightning Invoice': 'Fattura Lightning', + 'Bookmark failed': 'Impossibile aggiungere segnalibro', + 'Remove bookmark failed': 'Impossibile rimuovere segnalibro' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 19247db..82ba70f 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -241,6 +241,8 @@ export default { 'Mute user privately': 'ユーザーを非公開でミュート', 'Mute user publicly': 'ユーザーを公開でミュート', Quotes: '引用', - 'Lightning Invoice': 'ライトニングインボイス' + 'Lightning Invoice': 'ライトニングインボイス', + 'Bookmark failed': 'ブックマークに失敗しました', + 'Remove bookmark failed': 'ブックマークの削除に失敗しました' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 3e6b423..e521c8f 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -243,6 +243,8 @@ export default { 'Mute user privately': 'Zablokuj użytkownika prywatnie', 'Mute user publicly': 'Zablokuj użytkownika publicznie', Quotes: 'Cytaty', - 'Lightning Invoice': 'Faktura Lightning' + 'Lightning Invoice': 'Faktura Lightning', + 'Bookmark failed': 'Nie udało się dodać zakładki', + 'Remove bookmark failed': 'Nie udało się usunąć zakładki' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 9043325..4a0ea26 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -243,6 +243,8 @@ export default { 'Mute user privately': 'Silenciar usuário privadamente', 'Mute user publicly': 'Silenciar usuário publicamente', Quotes: 'Citações', - 'Lightning Invoice': 'Fatura Lightning' + 'Lightning Invoice': 'Fatura Lightning', + 'Bookmark failed': 'Falha ao favoritar', + 'Remove bookmark failed': 'Falha ao remover favorito' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 5d41a38..8d9b1ea 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -244,6 +244,8 @@ export default { 'Mute user privately': 'Silenciar usuário privadamente', 'Mute user publicly': 'Silenciar usuário publicamente', Quotes: 'Citações', - 'Lightning Invoice': 'Fatura Lightning' + 'Lightning Invoice': 'Fatura Lightning', + 'Bookmark failed': 'Falha ao favoritar', + 'Remove bookmark failed': 'Falha ao remover favorito' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 002f091..c060863 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -245,6 +245,8 @@ export default { 'Mute user privately': 'Заглушить пользователя приватно', 'Mute user publicly': 'Заглушить пользователя публично', Quotes: 'Цитаты', - 'Lightning Invoice': 'Lightning-счет' + 'Lightning Invoice': 'Lightning-счет', + 'Bookmark failed': 'Не удалось добавить закладку', + 'Remove bookmark failed': 'Не удалось удалить закладку' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index e94820c..d6ea4e9 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -241,6 +241,8 @@ export default { 'Mute user privately': '悄悄屏蔽', 'Mute user publicly': '公开屏蔽', Quotes: '引用', - 'Lightning Invoice': '闪电发票' + 'Lightning Invoice': '闪电发票', + 'Bookmark failed': '收藏失败', + 'Remove bookmark failed': '取消收藏失败' } } diff --git a/src/pages/secondary/RelayPage/index.tsx b/src/pages/secondary/RelayPage/index.tsx index 6f4a17c..c620b4c 100644 --- a/src/pages/secondary/RelayPage/index.tsx +++ b/src/pages/secondary/RelayPage/index.tsx @@ -3,12 +3,13 @@ import RelayInfo from '@/components/RelayInfo' import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' import SearchInput from '@/components/SearchInput' import { Button } from '@/components/ui/button' -import { useFetchRelayInfo, useToast } from '@/hooks' +import { useFetchRelayInfo } from '@/hooks' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { normalizeUrl, simplifyUrl } from '@/lib/url' import { Check, Copy, Link } from 'lucide-react' import { forwardRef, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import NotFoundPage from '../NotFoundPage' const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number }, ref) => { @@ -63,7 +64,6 @@ RelayPage.displayName = 'RelayPage' export default RelayPage function RelayPageControls({ url }: { url: string }) { - const { toast } = useToast() const [copiedUrl, setCopiedUrl] = useState(false) const [copiedShareableUrl, setCopiedShareableUrl] = useState(false) @@ -76,10 +76,7 @@ function RelayPageControls({ url }: { url: string }) { const handleCopyShareableUrl = () => { navigator.clipboard.writeText(`https://jumble.social/?r=${url}`) setCopiedShareableUrl(true) - toast({ - title: 'Shareable URL copied to clipboard', - description: 'You can share this URL with others.' - }) + toast.success('Shareable URL copied to clipboard') setTimeout(() => setCopiedShareableUrl(false), 2000) } diff --git a/src/pages/secondary/WalletPage/LightningAddressInput.tsx b/src/pages/secondary/WalletPage/LightningAddressInput.tsx index 2f6d14a..e1a5ce5 100644 --- a/src/pages/secondary/WalletPage/LightningAddressInput.tsx +++ b/src/pages/secondary/WalletPage/LightningAddressInput.tsx @@ -1,17 +1,16 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useToast } from '@/hooks' import { isEmail } from '@/lib/common' import { createProfileDraftEvent } from '@/lib/draft-event' import { useNostr } from '@/providers/NostrProvider' import { Loader } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' export default function LightningAddressInput() { const { t } = useTranslation() - const { toast } = useToast() const { profile, profileEvent, publish, updateProfileEvent } = useNostr() const [lightningAddress, setLightningAddress] = useState('') const [hasChanged, setHasChanged] = useState(false) @@ -36,11 +35,7 @@ export default function LightningAddressInput() { } else if (isEmail(lightningAddress)) { lud16 = lightningAddress } else { - toast({ - title: 'Invalid Lightning Address', - description: 'Please enter a valid Lightning Address or LNURL', - variant: 'destructive' - }) + toast.error(t('Invalid Lightning Address. Please enter a valid Lightning Address or LNURL.')) setSaving(false) return } diff --git a/src/providers/MuteListProvider.tsx b/src/providers/MuteListProvider.tsx index a4ec407..528eabe 100644 --- a/src/providers/MuteListProvider.tsx +++ b/src/providers/MuteListProvider.tsx @@ -5,9 +5,9 @@ import indexedDb from '@/services/indexed-db.service' import dayjs from 'dayjs' 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' -import { useToast } from '@/hooks' type TMuteListContext = { mutePubkeys: string[] @@ -32,7 +32,6 @@ export const useMuteList = () => { } export function MuteListProvider({ children }: { children: React.ReactNode }) { - const { toast } = useToast() const { pubkey: accountPubkey, muteListEvent, @@ -111,10 +110,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { } const newMuteListDraftEvent = createMuteListDraftEvent(tags, content) const event = await publish(newMuteListDraftEvent) - toast({ - title: 'Mute list updated', - description: 'Your mute list has been updated successfully.' - }) + toast.success('Successfully updated mute list') return event } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index e01d4c9..a4c4232 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -1,6 +1,5 @@ import LoginDialog from '@/components/LoginDialog' import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants' -import { useToast } from '@/hooks' import { createSeenNotificationsAtDraftEvent } from '@/lib/draft-event' import { getLatestEvent, @@ -20,11 +19,12 @@ import * as nip19 from 'nostr-tools/nip19' import * as nip49 from 'nostr-tools/nip49' import { createContext, useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { BunkerSigner } from './bunker.signer' import { Nip07Signer } from './nip-07.signer' +import { NostrConnectionSigner } from './nostrConnection.signer' import { NpubSigner } from './npub.signer' import { NsecSigner } from './nsec.signer' -import { NostrConnectionSigner } from './nostrConnection.signer' type TNostrContext = { isInitialized: boolean @@ -80,7 +80,6 @@ export const useNostr = () => { export function NostrProvider({ children }: { children: React.ReactNode }) { const { t } = useTranslation() - const { toast } = useToast() const [account, setAccount] = useState(null) const [nsec, setNsec] = useState(null) const [ncryptsec, setNcryptsec] = useState(null) @@ -379,11 +378,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } return login(nip07Signer, { pubkey, signerType: 'nip-07' }) } catch (err) { - toast({ - title: 'Login failed', - description: (err as Error).message, - variant: 'destructive' - }) + toast.error(t('Login failed') + ': ' + (err as Error).message) throw err } }