mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 07:00:09 +08:00
refactor: migrate heroui/toast to antd message
This commit is contained in:
parent
f4b14dfc10
commit
d01609fc36
@ -6,7 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
|
||||||
import { ToastPortal } from './components/ToastPortal'
|
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
import AntdProvider from './context/AntdProvider'
|
import AntdProvider from './context/AntdProvider'
|
||||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||||
@ -50,7 +49,6 @@ function App(): React.ReactElement {
|
|||||||
</AntdProvider>
|
</AntdProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyleSheetManager>
|
</StyleSheetManager>
|
||||||
<ToastPortal />
|
|
||||||
</HeroUIProvider>
|
</HeroUIProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
21
src/renderer/src/components/MessageInitializer.tsx
Normal file
21
src/renderer/src/components/MessageInitializer.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { App } from 'antd'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { initMessageApi } from './TopView/toast'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageInitializer component
|
||||||
|
* This component initializes the message API from App.useApp() hook
|
||||||
|
* It should be rendered inside the AntdProvider (which wraps App component)
|
||||||
|
* so that the message API can inherit the correct theme (dark/light mode)
|
||||||
|
*/
|
||||||
|
export const MessageInitializer = () => {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize the message API for use in toast.ts
|
||||||
|
initMessageApi(message)
|
||||||
|
}, [message])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { ToastProvider } from '@heroui/toast'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { createPortal } from 'react-dom'
|
|
||||||
|
|
||||||
export const ToastPortal = () => {
|
|
||||||
const [mounted, setMounted] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true)
|
|
||||||
return () => setMounted(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!mounted) return null
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<ToastProvider
|
|
||||||
placement="top-center"
|
|
||||||
regionProps={{
|
|
||||||
className: 'z-[1001]'
|
|
||||||
}}
|
|
||||||
toastOffset={20}
|
|
||||||
toastProps={{
|
|
||||||
timeout: 3000,
|
|
||||||
classNames: {
|
|
||||||
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
|
||||||
base: 'hero-toast'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
document.body
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
|
||||||
import type { RequireSome } from '@renderer/types'
|
|
||||||
|
|
||||||
type AddToastProps = Parameters<typeof addToast>[0]
|
|
||||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
|
||||||
|
|
||||||
const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
|
|
||||||
return (arg: ToastPropsColored | string): string | null => {
|
|
||||||
if (typeof arg === 'string') {
|
|
||||||
return addToast({ color, title: arg })
|
|
||||||
} else {
|
|
||||||
return addToast({ color, ...arg })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syntatic sugar, oh yeah
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display an error toast notification with red color
|
|
||||||
* @param arg - Toast content (string) or toast options object
|
|
||||||
* @returns Toast ID or null
|
|
||||||
*/
|
|
||||||
export const error = createToast('danger')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a success toast notification with green color
|
|
||||||
* @param arg - Toast content (string) or toast options object
|
|
||||||
* @returns Toast ID or null
|
|
||||||
*/
|
|
||||||
export const success = createToast('success')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a warning toast notification with yellow color
|
|
||||||
* @param arg - Toast content (string) or toast options object
|
|
||||||
* @returns Toast ID or null
|
|
||||||
*/
|
|
||||||
export const warning = createToast('warning')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display an info toast notification with default color
|
|
||||||
* @param arg - Toast content (string) or toast options object
|
|
||||||
* @returns Toast ID or null
|
|
||||||
*/
|
|
||||||
export const info = createToast('default')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a loading toast notification that resolves with a promise
|
|
||||||
* @param args - Toast options object containing a promise to resolve
|
|
||||||
* @returns Toast ID or null
|
|
||||||
*/
|
|
||||||
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
|
||||||
// Disappear immediately by default
|
|
||||||
if (args.timeout === undefined) {
|
|
||||||
args.timeout = 1
|
|
||||||
}
|
|
||||||
return addToast(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getToastUtilities = () =>
|
|
||||||
({
|
|
||||||
getToastQueue,
|
|
||||||
addToast,
|
|
||||||
closeToast,
|
|
||||||
closeAll,
|
|
||||||
isToastClosing,
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
warning,
|
|
||||||
info,
|
|
||||||
loading
|
|
||||||
}) as const
|
|
||||||
231
src/renderer/src/components/TopView/toast.tsx
Normal file
231
src/renderer/src/components/TopView/toast.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import type { RequireSome } from '@renderer/types'
|
||||||
|
import { message as antdMessage } from 'antd'
|
||||||
|
import type { MessageInstance } from 'antd/es/message/interface'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
|
// Global message instance for static usage
|
||||||
|
let messageApi: MessageInstance | null = null
|
||||||
|
|
||||||
|
// Initialize message API - should be called once the App component is mounted
|
||||||
|
export const initMessageApi = (api: MessageInstance) => {
|
||||||
|
messageApi = api
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get message API instance
|
||||||
|
const getMessageApi = (): MessageInstance => {
|
||||||
|
if (!messageApi) {
|
||||||
|
// Fallback to static method if hook API is not available
|
||||||
|
return antdMessage
|
||||||
|
}
|
||||||
|
return messageApi
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToastColor = 'danger' | 'success' | 'warning' | 'default'
|
||||||
|
type MessageType = 'error' | 'success' | 'warning' | 'info'
|
||||||
|
|
||||||
|
interface ToastConfig {
|
||||||
|
title?: React.ReactNode
|
||||||
|
icon?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
timeout?: number
|
||||||
|
key?: string | number
|
||||||
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
onClick?: () => void
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadingToastConfig extends ToastConfig {
|
||||||
|
promise: Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorToType = (color: ToastColor): MessageType => {
|
||||||
|
switch (color) {
|
||||||
|
case 'danger':
|
||||||
|
return 'error'
|
||||||
|
case 'success':
|
||||||
|
return 'success'
|
||||||
|
case 'warning':
|
||||||
|
return 'warning'
|
||||||
|
case 'default':
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast content component
|
||||||
|
const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{(icon || title) && (
|
||||||
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{description && <div className="text-sm">{description}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createToast = (color: ToastColor) => {
|
||||||
|
return (arg: ToastConfig | string): string | null => {
|
||||||
|
const api = getMessageApi()
|
||||||
|
const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info'
|
||||||
|
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
// antd message methods return a function to close the message
|
||||||
|
api[type](arg)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, icon, timeout, ...restConfig } = arg
|
||||||
|
|
||||||
|
// Convert timeout from milliseconds to seconds (antd uses seconds)
|
||||||
|
const duration = timeout !== undefined ? timeout / 1000 : 3
|
||||||
|
|
||||||
|
return (
|
||||||
|
(api.open({
|
||||||
|
type: type as 'error' | 'success' | 'warning' | 'info',
|
||||||
|
content: <ToastContent title={title} description={description} icon={icon} />,
|
||||||
|
duration,
|
||||||
|
...restConfig
|
||||||
|
}) as any) || null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an error toast notification with red color
|
||||||
|
* @param arg - Toast content (string) or toast options object
|
||||||
|
* @returns Toast ID or null
|
||||||
|
*/
|
||||||
|
export const error = createToast('danger')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a success toast notification with green color
|
||||||
|
* @param arg - Toast content (string) or toast options object
|
||||||
|
* @returns Toast ID or null
|
||||||
|
*/
|
||||||
|
export const success = createToast('success')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a warning toast notification with yellow color
|
||||||
|
* @param arg - Toast content (string) or toast options object
|
||||||
|
* @returns Toast ID or null
|
||||||
|
*/
|
||||||
|
export const warning = createToast('warning')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an info toast notification with default color
|
||||||
|
* @param arg - Toast content (string) or toast options object
|
||||||
|
* @returns Toast ID or null
|
||||||
|
*/
|
||||||
|
export const info = createToast('default')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a loading toast notification that resolves with a promise
|
||||||
|
* @param args - Toast options object containing a promise to resolve
|
||||||
|
*/
|
||||||
|
export const loading = (args: RequireSome<LoadingToastConfig, 'promise'>): string | null => {
|
||||||
|
const api = getMessageApi()
|
||||||
|
const { title, description, icon, promise, timeout, ...restConfig } = args
|
||||||
|
|
||||||
|
// Generate unique key for this loading message
|
||||||
|
const key = args.key || `loading-${Date.now()}-${Math.random()}`
|
||||||
|
|
||||||
|
// Show loading message
|
||||||
|
api.loading({
|
||||||
|
content: <ToastContent title={title || 'Loading...'} description={description} icon={icon} />,
|
||||||
|
duration: 0, // Don't auto-close
|
||||||
|
key,
|
||||||
|
...restConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle promise resolution
|
||||||
|
promise
|
||||||
|
.then((result) => {
|
||||||
|
api.success({
|
||||||
|
content: <ToastContent title={title || 'Success'} description={description} />,
|
||||||
|
duration: timeout !== undefined ? timeout / 1000 : 2,
|
||||||
|
key,
|
||||||
|
...restConfig
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
api.error({
|
||||||
|
content: (
|
||||||
|
<ToastContent title={title || 'Error'} description={err?.message || description || 'An error occurred'} />
|
||||||
|
),
|
||||||
|
duration: timeout !== undefined ? timeout / 1000 : 3,
|
||||||
|
key,
|
||||||
|
...restConfig
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
return key as string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a toast notification
|
||||||
|
* @param config - Toast configuration object
|
||||||
|
* @returns Toast ID or null
|
||||||
|
*/
|
||||||
|
export const addToast = (config: ToastConfig) => info(config)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a specific toast notification by its key
|
||||||
|
* @param key - Toast key (string)
|
||||||
|
*/
|
||||||
|
export const closeToast = (key: string) => {
|
||||||
|
getMessageApi().destroy(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all toast notifications
|
||||||
|
*/
|
||||||
|
export const closeAll = () => {
|
||||||
|
getMessageApi().destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub functions for compatibility with previous toast API
|
||||||
|
* These are no-ops since antd message doesn't expose a queue
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||||
|
* Antd message doesn't expose a queue. Do not rely on this function.
|
||||||
|
* @returns Empty toast queue stub
|
||||||
|
*/
|
||||||
|
export const getToastQueue = (): any => ({ toasts: [] })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||||
|
* Antd message doesn't track closing state. Do not rely on this function.
|
||||||
|
* @param key - Toast key (unused)
|
||||||
|
* @returns Always returns false
|
||||||
|
*/
|
||||||
|
export const isToastClosing = (key?: string): boolean => {
|
||||||
|
key // unused
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToastUtilities = () =>
|
||||||
|
({
|
||||||
|
getToastQueue,
|
||||||
|
addToast,
|
||||||
|
closeToast,
|
||||||
|
closeAll,
|
||||||
|
isToastClosing,
|
||||||
|
error,
|
||||||
|
success,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
loading
|
||||||
|
}) as const
|
||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { MessageInitializer } from '@renderer/components/MessageInitializer'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import type { LanguageVarious } from '@renderer/types'
|
import type { LanguageVarious } from '@renderer/types'
|
||||||
import { ConfigProvider, theme } from 'antd'
|
import { App, ConfigProvider, theme } from 'antd'
|
||||||
import deDE from 'antd/locale/de_DE'
|
import deDE from 'antd/locale/de_DE'
|
||||||
import elGR from 'antd/locale/el_GR'
|
import elGR from 'antd/locale/el_GR'
|
||||||
import enUS from 'antd/locale/en_US'
|
import enUS from 'antd/locale/en_US'
|
||||||
@ -114,7 +115,15 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
motionDurationMid: '100ms'
|
motionDurationMid: '100ms'
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
<App
|
||||||
|
message={{
|
||||||
|
top: 20,
|
||||||
|
maxCount: 3,
|
||||||
|
duration: 3
|
||||||
|
}}>
|
||||||
|
<MessageInitializer />
|
||||||
{children}
|
{children}
|
||||||
|
</App>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/renderer/src/env.d.ts
vendored
14
src/renderer/src/env.d.ts
vendored
@ -1,12 +1,22 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||||
import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
|
||||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||||
import type { NavigateFunction } from 'react-router-dom'
|
import type { NavigateFunction } from 'react-router-dom'
|
||||||
|
|
||||||
import type { error, info, loading, success, warning } from './components/TopView/toast'
|
import type {
|
||||||
|
addToast,
|
||||||
|
closeAll,
|
||||||
|
closeToast,
|
||||||
|
error,
|
||||||
|
getToastQueue,
|
||||||
|
info,
|
||||||
|
isToastClosing,
|
||||||
|
loading,
|
||||||
|
success,
|
||||||
|
warning
|
||||||
|
} from './components/TopView/toast'
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||||
|
|||||||
@ -69,8 +69,10 @@ export async function checkDataLimit() {
|
|||||||
const toastId = window.toast.warning({
|
const toastId = window.toast.warning({
|
||||||
title: t('settings.data.limit.appDataDiskQuota'),
|
title: t('settings.data.limit.appDataDiskQuota'),
|
||||||
description: t('settings.data.limit.appDataDiskQuotaDescription'),
|
description: t('settings.data.limit.appDataDiskQuotaDescription'),
|
||||||
timeout: 0, // Never auto-dismiss
|
timeout: 0 // Never auto-dismiss
|
||||||
hideCloseButton: true // Hide close button so user cannot dismiss
|
// hideCloseButton: true // Hide close button so user cannot dismiss
|
||||||
|
// commented out because antd message doesn't support hiding close button
|
||||||
|
// so we just rely on the timeout: 0 to keep it persistent
|
||||||
})
|
})
|
||||||
currentToastId = toastId
|
currentToastId = toastId
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import '@renderer/databases'
|
import '@renderer/databases'
|
||||||
|
|
||||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
|
||||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||||
import { HeroUIProvider } from '@renderer/context/HeroUIProvider'
|
import { HeroUIProvider } from '@renderer/context/HeroUIProvider'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -55,7 +54,6 @@ function MiniWindow(): React.ReactElement {
|
|||||||
</CodeStyleProvider>
|
</CodeStyleProvider>
|
||||||
</AntdProvider>
|
</AntdProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<ToastPortal />
|
|
||||||
</HeroUIProvider>
|
</HeroUIProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import '@ant-design/v5-patch-for-react-19'
|
|||||||
|
|
||||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
|
||||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||||
import AntdProvider from '@renderer/context/AntdProvider'
|
import AntdProvider from '@renderer/context/AntdProvider'
|
||||||
import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider'
|
import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider'
|
||||||
@ -54,7 +53,6 @@ const App: FC = () => {
|
|||||||
</CodeStyleProvider>
|
</CodeStyleProvider>
|
||||||
</AntdProvider>
|
</AntdProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<ToastPortal />
|
|
||||||
</HeroUIProvider>
|
</HeroUIProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user