mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +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 { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import { ToastPortal } from './components/ToastPortal'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
@ -50,7 +49,6 @@ function App(): React.ReactElement {
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
</QueryClientProvider>
|
||||
</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 type { LanguageVarious } from '@renderer/types'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
import { App, ConfigProvider, theme } from 'antd'
|
||||
import deDE from 'antd/locale/de_DE'
|
||||
import elGR from 'antd/locale/el_GR'
|
||||
import enUS from 'antd/locale/en_US'
|
||||
@ -114,7 +115,15 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
motionDurationMid: '100ms'
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
<App
|
||||
message={{
|
||||
top: 20,
|
||||
maxCount: 3,
|
||||
duration: 3
|
||||
}}>
|
||||
<MessageInitializer />
|
||||
{children}
|
||||
</App>
|
||||
</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" />
|
||||
|
||||
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 { HookAPI } from 'antd/es/modal/useModal'
|
||||
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 {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
|
||||
@ -69,8 +69,10 @@ export async function checkDataLimit() {
|
||||
const toastId = window.toast.warning({
|
||||
title: t('settings.data.limit.appDataDiskQuota'),
|
||||
description: t('settings.data.limit.appDataDiskQuotaDescription'),
|
||||
timeout: 0, // Never auto-dismiss
|
||||
hideCloseButton: true // Hide close button so user cannot dismiss
|
||||
timeout: 0 // Never auto-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
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||
import { HeroUIProvider } from '@renderer/context/HeroUIProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -55,7 +54,6 @@ function MiniWindow(): React.ReactElement {
|
||||
</CodeStyleProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@ -4,7 +4,6 @@ import '@ant-design/v5-patch-for-react-19'
|
||||
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { loggerService } from '@logger'
|
||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||
import AntdProvider from '@renderer/context/AntdProvider'
|
||||
import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider'
|
||||
@ -54,7 +53,6 @@ const App: FC = () => {
|
||||
</CodeStyleProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user