diff options
| -rw-r--r-- | apps/mobile/components/ui/Toast.tsx | 215 | ||||
| -rw-r--r-- | apps/mobile/lib/providers.tsx | 5 | ||||
| -rw-r--r-- | apps/mobile/package.json | 1 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 109 |
4 files changed, 70 insertions, 260 deletions
diff --git a/apps/mobile/components/ui/Toast.tsx b/apps/mobile/components/ui/Toast.tsx index 96323263..722c93ab 100644 --- a/apps/mobile/components/ui/Toast.tsx +++ b/apps/mobile/components/ui/Toast.tsx @@ -1,8 +1,4 @@ -import { createContext, useContext, useEffect, useRef, useState } from "react"; -import { Animated, Platform, View } from "react-native"; -import { FullWindowOverlay } from "react-native-screens"; -import { Text } from "@/components/ui/Text"; -import { cn } from "@/lib/utils"; +import { toast as sonnerToast } from "sonner-native"; const toastVariants = { default: "bg-foreground", @@ -11,184 +7,41 @@ const toastVariants = { info: "bg-blue-500", }; -interface ToastProps { - id: number; - message: string; - onHide: (id: number) => void; - variant?: keyof typeof toastVariants; - duration?: number; - showProgress?: boolean; -} -function Toast({ - id, - message, - onHide, - variant = "default", - duration = 3000, - showProgress = true, -}: ToastProps) { - const opacity = useRef(new Animated.Value(0)).current; - const progress = useRef(new Animated.Value(0)).current; - - useEffect(() => { - Animated.sequence([ - Animated.timing(opacity, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(progress, { - toValue: 1, - duration: duration - 1000, - useNativeDriver: false, - }), - Animated.timing(opacity, { - toValue: 0, - duration: 500, - useNativeDriver: true, - }), - ]).start(() => onHide(id)); - }, [duration]); - - return ( - <Animated.View - className={` - ${toastVariants[variant]} - m-2 mb-1 transform rounded-lg p-4 transition-all - `} - style={{ - opacity, - transform: [ - { - translateY: opacity.interpolate({ - inputRange: [0, 1], - outputRange: [-20, 0], - }), - }, - ], - }} - > - <Text className="text-left font-semibold text-background">{message}</Text> - {showProgress && ( - <View className="mt-2 rounded"> - <Animated.View - className="h-2 rounded bg-white opacity-30 dark:bg-black" - style={{ - width: progress.interpolate({ - inputRange: [0, 1], - outputRange: ["0%", "100%"], - }), - }} - /> - </View> - )} - </Animated.View> - ); -} - type ToastVariant = keyof typeof toastVariants; -interface ToastMessage { - id: number; - text: string; - variant: ToastVariant; - duration?: number; - position?: string; - showProgress?: boolean; -} -interface ToastContextProps { - toast: (t: { - message: string; - variant?: keyof typeof toastVariants; - duration?: number; - position?: "top" | "bottom"; - showProgress?: boolean; - }) => void; - removeToast: (id: number) => void; -} -const ToastContext = createContext<ToastContextProps | undefined>(undefined); - -// TODO: refactor to pass position to Toast instead of ToastProvider -function ToastProvider({ - children, - position = "top", -}: { - children: React.ReactNode; - position?: "top" | "bottom"; -}) { - const [messages, setMessages] = useState<ToastMessage[]>([]); - - const toast: ToastContextProps["toast"] = ({ - message, - variant = "default", - duration = 3000, - position = "top", - showProgress = true, - }: { - message: string; - variant?: ToastVariant; - duration?: number; - position?: "top" | "bottom"; - showProgress?: boolean; - }) => { - setMessages((prev) => [ - ...prev, - { - id: Date.now(), - text: message, - variant, - duration, - position, - showProgress, - }, - ]); - }; - - const removeToast = (id: number) => { - setMessages((prev) => prev.filter((message) => message.id !== id)); - }; - - const content = ( - <View - className={cn("absolute left-0 right-0", { - "top-[45px]": position === "top", - "bottom-0": position === "bottom", - })} - > - {messages.map((message) => ( - <Toast - key={message.id} - id={message.id} - message={message.text} - variant={message.variant} - duration={message.duration} - showProgress={message.showProgress} - onHide={removeToast} - /> - ))} - </View> - ); - - return ( - <ToastContext.Provider value={{ toast, removeToast }}> - {children} - {/* Use FullWindowOverlay on iOS to ensure toasts appear above all content */} - {/* Platform specific implementation due to FullWindowOverlay being iOS-only */} - {Platform.OS === "ios" ? ( - <FullWindowOverlay>{content}</FullWindowOverlay> - ) : ( - content - )} - </ToastContext.Provider> - ); -} - +// Compatibility wrapper for sonner-native function useToast() { - const context = useContext(ToastContext); - if (!context) { - throw new Error("useToast must be used within ToastProvider"); - } - return context; + return { + toast: ({ + message, + variant = "default", + duration = 3000, + }: { + message: string; + variant?: ToastVariant; + duration?: number; + position?: "top" | "bottom"; + showProgress?: boolean; + }) => { + // Map variants to sonner-native methods + switch (variant) { + case "success": + sonnerToast.success(message, { duration }); + break; + case "destructive": + sonnerToast.error(message, { duration }); + break; + case "info": + sonnerToast.info(message, { duration }); + break; + default: + sonnerToast(message, { duration }); + } + }, + removeToast: () => { + // sonner-native handles dismissal automatically + }, + }; } -export { ToastProvider, ToastVariant, Toast, toastVariants, useToast }; +export { ToastVariant, toastVariants, useToast }; diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 36ed7e71..01d2d5b5 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { ToastProvider } from "@/components/ui/Toast"; +import { Toaster } from "sonner-native"; import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; @@ -22,7 +22,8 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( <TRPCProvider settings={settings}> <ReaderSettingsProvider> - <ToastProvider>{children}</ToastProvider> + {children} + <Toaster /> </ReaderSettingsProvider> </TRPCProvider> ); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index f826300d..d1525a48 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -61,6 +61,7 @@ "react-native-screens": "~4.11.1", "react-native-svg": "^15.11.2", "react-native-webview": "^13.13.5", + "sonner-native": "^0.22.2", "tailwind-merge": "^2.2.1", "zod": "^3.24.2", "zustand": "^5.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c9e3c77..7b4be6d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -452,6 +452,9 @@ importers: react-native-webview: specifier: ^13.13.5 version: 13.14.1(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + sonner-native: + specifier: ^0.22.2 + version: 0.22.2(react-native-gesture-handler@2.24.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-screens@4.11.1(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) tailwind-merge: specifier: ^2.2.1 version: 2.2.1 @@ -1369,7 +1372,7 @@ importers: version: 19.1.0 react-native: specifier: 0.79.5 - version: 0.79.5(@babel/core@7.26.0)(@types/react@19.2.5)(react@19.1.0) + version: 0.79.5(@babel/core@7.28.0)(@types/react@19.2.5)(react@19.1.0) superjson: specifier: ^2.2.1 version: 2.2.1 @@ -13763,6 +13766,17 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner-native@0.22.2: + resolution: {integrity: sha512-yboVB3EWPyEs8w3nC+mKio3Tr5uMEHVCGi5ko4ibemNEVxNKpZ4nIM0zMTTrh4R0Ihm3n11unDJN8K44eXZLlw==} + peerDependencies: + react: '*' + react-native: '*' + react-native-gesture-handler: '>=2.16.1' + react-native-reanimated: '>=3.10.1' + react-native-safe-area-context: '>=4.10.5' + react-native-screens: '>=3.31.1' + react-native-svg: '>=15.6.0' + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -15993,7 +16007,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.6 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17087,7 +17101,7 @@ snapshots: '@babel/parser': 7.27.5 '@babel/template': 7.27.2 '@babel/types': 7.27.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -17100,7 +17114,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -20642,15 +20656,6 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - '@react-native/virtualized-lists@0.79.5(@types/react@19.2.5)(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.2.5)(react@19.1.0))(react@19.1.0)': - dependencies: - invariant: 2.2.4 - nullthrows: 1.1.1 - react: 19.1.0 - react-native: 0.79.5(@babel/core@7.26.0)(@types/react@19.2.5)(react@19.1.0) - optionalDependencies: - '@types/react': 19.2.5 - '@react-native/virtualized-lists@0.79.5(@types/react@19.2.5)(react-native@0.79.5(@babel/core@7.28.0)(@types/react@19.2.5)(react@19.1.0))(react@19.1.0)': dependencies: invariant: 2.2.4 @@ -23694,10 +23699,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: - dependencies: - ms: 2.1.3 - debug@4.4.1(supports-color@10.0.0): dependencies: ms: 2.1.3 @@ -25612,14 +25613,6 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.3 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - https-proxy-agent@7.0.6(supports-color@10.0.0): dependencies: agent-base: 7.1.3 @@ -26215,7 +26208,7 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.0.0) is-potential-custom-element-name: 1.0.1 parse5: 8.0.0 saxes: 6.0.0 @@ -29898,54 +29891,6 @@ snapshots: - supports-color - utf-8-validate - react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.2.5)(react@19.1.0): - dependencies: - '@jest/create-cache-key-function': 29.7.0 - '@react-native/assets-registry': 0.79.5 - '@react-native/codegen': 0.79.5(@babel/core@7.26.0) - '@react-native/community-cli-plugin': 0.79.5 - '@react-native/gradle-plugin': 0.79.5 - '@react-native/js-polyfills': 0.79.5 - '@react-native/normalize-colors': 0.79.5 - '@react-native/virtualized-lists': 0.79.5(@types/react@19.2.5)(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.2.5)(react@19.1.0))(react@19.1.0) - abort-controller: 3.0.0 - anser: 1.4.10 - ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.26.0) - babel-plugin-syntax-hermes-parser: 0.25.1 - base64-js: 1.5.1 - chalk: 4.1.2 - commander: 12.1.0 - event-target-shim: 5.0.1 - flow-enums-runtime: 0.0.6 - glob: 7.2.3 - invariant: 2.2.4 - jest-environment-node: 29.7.0 - memoize-one: 5.2.1 - metro-runtime: 0.82.5 - metro-source-map: 0.82.5 - nullthrows: 1.1.1 - pretty-format: 29.7.0 - promise: 8.3.0 - react: 19.1.0 - react-devtools-core: 6.1.5 - react-refresh: 0.14.2 - regenerator-runtime: 0.13.11 - scheduler: 0.25.0 - semver: 7.7.3 - stacktrace-parser: 0.1.11 - whatwg-fetch: 3.6.20 - ws: 6.2.3 - yargs: 17.7.2 - optionalDependencies: - '@types/react': 19.2.5 - transitivePeerDependencies: - - '@babel/core' - - '@react-native-community/cli' - - bufferutil - - supports-color - - utf-8-validate - react-native@0.79.5(@babel/core@7.28.0)(@types/react@19.2.5)(react@19.1.0): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -31106,6 +31051,16 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 + sonner-native@0.22.2(react-native-gesture-handler@2.24.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-screens@4.11.1(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + react-native-gesture-handler: 2.24.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 3.18.0(@babel/core@7.26.0)(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.4.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.11.1(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-svg: 15.12.0(react-native@0.79.5(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -32127,7 +32082,7 @@ snapshots: vite-node@3.2.4(@types/node@24.10.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.0.6(@types/node@24.10.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0) @@ -32177,7 +32132,7 @@ snapshots: vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.0.6(@types/node@24.10.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.41.0)(tsx@4.20.3)(yaml@2.8.0)): dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: @@ -32215,7 +32170,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 |
