diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index 0c2d5c08..3c2761d0 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -1,6 +1,7 @@ import { emit } from '@tauri-apps/api/event'; import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins'; -import {useWatchWorkspace} from "@yaakapp-internal/sync"; +import { useWatchWorkspace } from '@yaakapp-internal/sync'; +import type { ShowToastRequest } from '@yaakapp/api'; import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId, @@ -28,6 +29,7 @@ import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModel import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; +import {useToast} from "../hooks/useToast"; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; export function GlobalHooks() { @@ -55,6 +57,12 @@ export function GlobalHooks() { useActiveWorkspaceChangedToast(); useEnsureActiveCookieJar(); + // Listen for toasts + const toast = useToast(); + useListenToTauriEvent('show_toast', (event) => { + toast.show({ ...event.payload }); + }); + // Trigger workspace sync operation when workspace files change const activeWorkspace = useActiveWorkspace(); const { debouncedSync } = useSyncWorkspace(activeWorkspace, { debounceMillis: 1000 }); diff --git a/src-web/components/ToastContext.tsx b/src-web/components/ToastContext.tsx deleted file mode 100644 index 9829a0e1..00000000 --- a/src-web/components/ToastContext.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react'; -import type { ToastState } from './Toasts'; - -export const ToastContext = createContext({} as ToastState); diff --git a/src-web/components/Toasts.tsx b/src-web/components/Toasts.tsx index 68e9026c..05d70616 100644 --- a/src-web/components/Toasts.tsx +++ b/src-web/components/Toasts.tsx @@ -1,74 +1,24 @@ -import type { ShowToastRequest } from '@yaakapp-internal/plugins'; import { AnimatePresence } from 'framer-motion'; -import React, { type ReactNode, useContext, useMemo, useRef, useState } from 'react'; -import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; -import { generateId } from '../lib/generateId'; +import { useAtomValue } from 'jotai'; +import React, { type ReactNode } from 'react'; +import { toastsAtom, useToast } from '../hooks/useToast'; import { Toast, type ToastProps } from './core/Toast'; import { Portal } from './Portal'; -import { ToastContext } from './ToastContext'; -type ToastEntry = { +export type ToastEntry = { id?: string; message: ReactNode; timeout?: 3000 | 5000 | 8000 | null; onClose?: ToastProps['onClose']; } & Omit; -type PrivateToastEntry = ToastEntry & { +export type PrivateToastEntry = ToastEntry & { id: string; timeout: number | null; }; -export interface ToastState { - toasts: PrivateToastEntry[]; - actions: Actions; -} - -export interface Actions { - show: (d: ToastEntry) => void; - hide: (id: string) => void; -} - -export const ToastProvider = ({ children }: { children: React.ReactNode }) => { - const [toasts, setToasts] = useState([]); - const timeoutRef = useRef(); - const actions = useMemo( - () => ({ - show({ id, timeout = 5000, ...props }: ToastEntry) { - id = id ?? generateId(); - if (timeout != null) { - timeoutRef.current = setTimeout(() => this.hide(id), timeout); - } - setToasts((a) => { - if (a.some((v) => v.id === id)) { - // It's already visible with this id - return a; - } - return [...a, { id, timeout, ...props }]; - }); - return id; - }, - hide: (id: string) => { - setToasts((all) => { - const t = all.find((t) => t.id === id); - t?.onClose?.(); - return all.filter((t) => t.id !== id); - }); - }, - }), - [], - ); - - useListenToTauriEvent('show_toast', (event) => { - actions.show({ ...event.payload }); - }); - - const state: ToastState = { toasts, actions }; - return {children}; -}; - function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) { - const { actions } = useContext(ToastContext); + const toast = useToast(); return ( actions.hide(id)} + onClose={() => toast.hide(id)} > {message} @@ -84,7 +34,7 @@ function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) { } export const Toasts = () => { - const { toasts } = useContext(ToastContext); + const toasts = useAtomValue(toastsAtom); return (
diff --git a/src-web/hooks/useToast.ts b/src-web/hooks/useToast.ts index e469a164..715271bd 100644 --- a/src-web/hooks/useToast.ts +++ b/src-web/hooks/useToast.ts @@ -1,6 +1,36 @@ -import { useContext } from 'react'; -import { ToastContext } from '../components/ToastContext'; +import { atom } from 'jotai/index'; +import { useMemo } from 'react'; +import type { PrivateToastEntry, ToastEntry } from '../components/Toasts'; +import { generateId } from '../lib/generateId'; +import { jotaiStore } from '../lib/jotai'; + +export const toastsAtom = atom([]); export function useToast() { - return useContext(ToastContext).actions; + return useMemo( + () => ({ + show({ id, timeout = 5000, ...props }: ToastEntry) { + id = id ?? generateId(); + if (timeout != null) { + setTimeout(() => this.hide(id), timeout); + } + jotaiStore.set(toastsAtom, (a) => { + if (a.some((v) => v.id === id)) { + // It's already visible with this id + return a; + } + return [...a, { id, timeout, ...props }]; + }); + return id; + }, + hide: (id: string) => { + jotaiStore.set(toastsAtom, (all) => { + const t = all.find((t) => t.id === id); + t?.onClose?.(); + return all.filter((t) => t.id !== id); + }); + }, + }), + [], + ); } diff --git a/src-web/routes/__root.tsx b/src-web/routes/__root.tsx index 6c0f1939..14867334 100644 --- a/src-web/routes/__root.tsx +++ b/src-web/routes/__root.tsx @@ -10,7 +10,7 @@ import { HelmetProvider } from 'react-helmet-async'; import { DialogProvider, Dialogs } from '../components/Dialogs'; import { GlobalHooks } from '../components/GlobalHooks'; import RouteError from '../components/RouteError'; -import { ToastProvider, Toasts } from '../components/Toasts'; +import { Toasts } from '../components/Toasts'; import { useOsInfo } from '../hooks/useOsInfo'; import { jotaiStore } from '../lib/jotai'; @@ -66,19 +66,17 @@ function RouteComponent() { - - - - -
- -
-
+ + + +
+ +