aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2026-01-01 14:35:04 +0000
committerMohamed Bassem <me@mbassem.com>2026-01-01 14:35:04 +0000
commit016433d48292d6bdd64565a2a03e716abca9f408 (patch)
tree96585d9784cc0c79e71e0ef7d2d9ddf3d5998cc1
parent2b89f1777e3c89a1477105b84b9e8fec52929768 (diff)
downloadkarakeep-016433d48292d6bdd64565a2a03e716abca9f408.tar.zst
feat(mobile): use react native sonner
-rw-r--r--apps/mobile/components/ui/Toast.tsx215
-rw-r--r--apps/mobile/lib/providers.tsx5
-rw-r--r--apps/mobile/package.json1
-rw-r--r--pnpm-lock.yaml109
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