diff --git a/packages/frontend/component/src/components/auth-components/change-password-page.tsx b/packages/frontend/component/src/components/auth-components/change-password-page.tsx index f4b1f66749..ca2a9381f3 100644 --- a/packages/frontend/component/src/components/auth-components/change-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/change-password-page.tsx @@ -1,11 +1,10 @@ import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { Button } from '../../ui/button'; -import { pushNotificationAtom } from '../notification-center'; +import { notify } from '../../ui/notification'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; import type { User } from './type'; @@ -23,22 +22,19 @@ export const ChangePasswordPage: FC<{ }) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); - const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { propsOnSetPassword(passWord) .then(() => setHasSetUp(true)) .catch(e => - pushNotification({ + notify.error({ title: t['com.affine.auth.password.set-failed'](), message: String(e), - key: Date.now().toString(), - type: 'error', }) ); }, - [propsOnSetPassword, t, pushNotification] + [propsOnSetPassword, t] ); return ( diff --git a/packages/frontend/component/src/components/auth-components/set-password-page.tsx b/packages/frontend/component/src/components/auth-components/set-password-page.tsx index 6859707ba9..df14d7a182 100644 --- a/packages/frontend/component/src/components/auth-components/set-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password-page.tsx @@ -1,11 +1,10 @@ import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { Button } from '../../ui/button'; -import { pushNotificationAtom } from '../notification-center'; +import { notify } from '../../ui/notification'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; import type { User } from './type'; @@ -23,22 +22,19 @@ export const SetPasswordPage: FC<{ }) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); - const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { propsOnSetPassword(passWord) .then(() => setHasSetUp(true)) .catch(e => - pushNotification({ + notify.error({ title: t['com.affine.auth.password.set-failed'](), message: String(e), - key: Date.now().toString(), - type: 'error', }) ); }, - [propsOnSetPassword, pushNotification, t] + [propsOnSetPassword, t] ); return ( diff --git a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx index 11a4804fc1..a6a9cf2c5d 100644 --- a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx +++ b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx @@ -1,11 +1,10 @@ import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { Button } from '../../ui/button'; -import { pushNotificationAtom } from '../notification-center'; +import { notify } from '../../ui/notification'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; import type { User } from './type'; @@ -25,22 +24,19 @@ export const SignUpPage: FC<{ }) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); - const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { propsOnSetPassword(passWord) .then(() => setHasSetUp(true)) .catch(e => - pushNotification({ + notify.error({ title: t['com.affine.auth.password.set-failed'](), message: String(e), - key: Date.now().toString(), - type: 'error', }) ); }, - [propsOnSetPassword, pushNotification, t] + [propsOnSetPassword, t] ); const onLater = useCallback(() => { setHasSetUp(true); diff --git a/packages/frontend/component/src/components/notification-center/index.jotai.ts b/packages/frontend/component/src/components/notification-center/index.jotai.ts index 26bc44de42..30b3645ffa 100644 --- a/packages/frontend/component/src/components/notification-center/index.jotai.ts +++ b/packages/frontend/component/src/components/notification-center/index.jotai.ts @@ -1,6 +1,9 @@ import { atom } from 'jotai'; import { nanoid } from 'nanoid'; +/** + * @deprecated use `import type { Notification } from '@affine/component'` instead + */ export type Notification = { key?: string; title: string; @@ -19,6 +22,9 @@ const notificationsBaseAtom = atom([]); const expandNotificationCenterBaseAtom = atom(false); const cleanupQueueAtom = atom<(() => unknown)[]>([]); +/** + * @deprecated use `import { notify } from '@affine/component'` instead + */ export const expandNotificationCenterAtom = atom( get => get(expandNotificationCenterBaseAtom), (get, set, value) => { @@ -29,17 +35,24 @@ export const expandNotificationCenterAtom = atom( set(expandNotificationCenterBaseAtom, value); } ); - +/** + * @deprecated use `import { notify } from '@affine/component'` instead + */ export const notificationsAtom = atom(get => get(notificationsBaseAtom) ); - +/** + * @deprecated use `import { notify } from '@affine/component'` instead + */ export const removeNotificationAtom = atom(null, (_, set, key: string) => { set(notificationsBaseAtom, notifications => notifications.filter(notification => notification.key !== key) ); }); +/** + * @deprecated use `import { notify } from '@affine/component'` instead + */ export const pushNotificationAtom = atom( null, (_, set, newNotification) => { diff --git a/packages/frontend/component/src/components/notification-center/index.tsx b/packages/frontend/component/src/components/notification-center/index.tsx index 712301f437..1534f11bef 100644 --- a/packages/frontend/component/src/components/notification-center/index.tsx +++ b/packages/frontend/component/src/components/notification-center/index.tsx @@ -375,6 +375,9 @@ function NotificationCard(props: NotificationCardProps): ReactNode { ); } +/** + * @deprecated use `import { NotificationCenter } from '@affine/component'` instead + */ export function NotificationCenter(): ReactNode { const notifications = useAtomValue(notificationsAtom); const [expand, setExpand] = useAtom(expandNotificationCenterAtom); diff --git a/packages/frontend/component/src/ui/notification/notification-center.tsx b/packages/frontend/component/src/ui/notification/notification-center.tsx index 2a49867c08..bfcfd50a6d 100644 --- a/packages/frontend/component/src/ui/notification/notification-center.tsx +++ b/packages/frontend/component/src/ui/notification/notification-center.tsx @@ -1,3 +1,4 @@ +import { SingleSelectSelectSolidIcon } from '@blocksuite/icons'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import { type CSSProperties, type FC, useMemo } from 'react'; import { type ExternalToast, toast, Toaster } from 'sonner'; @@ -54,6 +55,29 @@ export function notify(notification: Notification, options?: ExternalToast) { }, options); } +notify.error = (notification: Notification, options?: ExternalToast) => { + return notify({ style: 'alert', theme: 'error', ...notification }, options); +}; + +notify.success = (notification: Notification, options?: ExternalToast) => { + return notify( + { + icon: , + style: 'alert', + theme: 'success', + ...notification, + }, + options + ); +}; + +notify.warning = (notification: Notification, options?: ExternalToast) => { + return notify( + { style: 'information', theme: 'warning', ...notification }, + options + ); +}; + notify.custom = ( Component: FC, options?: ExternalToast diff --git a/packages/frontend/core/src/components/affine/auth/send-email.tsx b/packages/frontend/core/src/components/affine/auth/send-email.tsx index 165d37003a..a97b8cf90b 100644 --- a/packages/frontend/core/src/components/affine/auth/send-email.tsx +++ b/packages/frontend/core/src/components/affine/auth/send-email.tsx @@ -1,11 +1,10 @@ -import { Wrapper } from '@affine/component'; +import { notify, Wrapper } from '@affine/component'; import { AuthContent, AuthInput, BackButton, ModalHeader, } from '@affine/component/auth-components'; -import { pushNotificationAtom } from '@affine/component/notification-center'; import { Button } from '@affine/component/ui/button'; import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; @@ -17,7 +16,6 @@ import { sendVerifyEmailMutation, } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useSetAtom } from 'jotai/react'; import { useCallback, useState } from 'react'; import { useMutation } from '../../../hooks/use-mutation'; @@ -165,7 +163,6 @@ export const SendEmail = ({ const t = useAFFiNEI18N(); const { password: passwordLimits } = useCredentialsRequirement(); const [hasSentEmail, setHasSentEmail] = useState(false); - const pushNotification = useSetAtom(pushNotificationAtom); const title = useEmailTitle(emailType); const hint = useNotificationHint(emailType); @@ -177,14 +174,9 @@ export const SendEmail = ({ // TODO: add error handler await sendEmail(email); - pushNotification({ - title: hint, - message: '', - key: Date.now().toString(), - type: 'success', - }); + notify.success({ title: hint }); setHasSentEmail(true); - }, [email, hint, pushNotification, sendEmail]); + }, [email, hint, sendEmail]); return ( <> diff --git a/packages/frontend/core/src/components/affine/auth/use-auth.ts b/packages/frontend/core/src/components/affine/auth/use-auth.ts index f819e2ece9..50af0bc6bd 100644 --- a/packages/frontend/core/src/components/affine/auth/use-auth.ts +++ b/packages/frontend/core/src/components/affine/auth/use-auth.ts @@ -1,5 +1,4 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; -import type { Notification } from '@affine/component/notification-center/index.jotai'; +import { notify } from '@affine/component'; import type { OAuthProviderType } from '@affine/graphql'; import { atom, useAtom, useSetAtom } from 'jotai'; import { useCallback } from 'react'; @@ -10,19 +9,6 @@ import { useSubscriptionSearch } from './use-subscription'; const COUNT_DOWN_TIME = 60; export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`; -function handleSendEmailError( - res: Response | undefined | void, - pushNotification: (notification: Notification) => void -) { - if (!res?.ok) { - pushNotification({ - title: 'Send email error', - message: 'Please back to home and try again', - type: 'error', - }); - } -} - type AuthStoreAtom = { allowSendEmail: boolean; resendCountDown: number; @@ -60,7 +46,6 @@ const countDownAtom = atom( export const useAuth = () => { const subscriptionData = useSubscriptionSearch(); - const pushNotification = useSetAtom(pushNotificationAtom); const [authStore, setAuthStore] = useAtom(authStoreAtom); const startResendCountDown = useSetAtom(countDownAtom); @@ -96,7 +81,13 @@ export const useAuth = () => { } ).catch(console.error); - handleSendEmailError(res, pushNotification); + if (!res?.ok) { + // TODO: i18n + notify.error({ + title: 'Send email error', + message: 'Please back to home and try again', + }); + } setAuthStore({ isMutating: false, @@ -104,11 +95,12 @@ export const useAuth = () => { resendCountDown: COUNT_DOWN_TIME, }); + // TODO: when errored, should reset the count down startResendCountDown(); return res; }, - [pushNotification, setAuthStore, startResendCountDown, subscriptionData] + [setAuthStore, startResendCountDown, subscriptionData] ); const signUp = useCallback( diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx index 5276e5b0af..1bb07fd510 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -1,5 +1,4 @@ -import { FlexWrapper, Input } from '@affine/component'; -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { FlexWrapper, Input, notify } from '@affine/component'; import { SettingHeader, SettingRow, @@ -35,7 +34,6 @@ import * as styles from './style.css'; export const UserAvatar = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); - const pushNotification = useSetAtom(pushNotificationAtom); const { trigger: avatarTrigger } = useMutation({ mutation: uploadAvatarMutation, @@ -55,19 +53,17 @@ export const UserAvatar = () => { avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger }); user.update({ avatarUrl: data.uploadAvatar.avatarUrl }); - pushNotification({ - title: 'Update user avatar success', - type: 'success', - }); + // TODO: i18n + notify.success({ title: 'Update user avatar success' }); } catch (e) { - pushNotification({ + // TODO: i18n + notify.error({ title: 'Update user avatar failed', message: String(e), - type: 'error', }); } }, - [avatarTrigger, pushNotification, user] + [avatarTrigger, user] ); const handleRemoveUserAvatar = useCallback( @@ -109,7 +105,6 @@ export const AvatarAndName = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); const [input, setInput] = useState(user.name); - const pushNotification = useSetAtom(pushNotificationAtom); const { trigger: updateProfile } = useMutation({ mutation: updateUserProfileMutation, @@ -129,13 +124,12 @@ export const AvatarAndName = () => { }); user.update({ name: data.updateProfile.name }); } catch (e) { - pushNotification({ + notify.error({ title: 'Failed to update user name.', message: String(e), - type: 'error', }); } - }, [allowUpdate, input, user, updateProfile, pushNotification]); + }, [allowUpdate, input, user, updateProfile]); return ( { const t = useAFFiNEI18N(); const [subscription, mutateSubscription] = useUserSubscription(); - const pushNotification = useSetAtom(pushNotificationAtom); const loggedIn = useCurrentLoginStatus() === 'authenticated'; const planDetail = getPlanDetail(t); @@ -165,9 +164,11 @@ const Settings = () => { key={detail.plan} onSubscriptionUpdate={mutateSubscription} onNotify={({ detail, recurring }) => { - pushNotification({ - type: 'success', - theme: 'default', + notify({ + style: 'normal', + icon: ( + + ), title: t['com.affine.payment.updated-notify-title'](), message: detail.plan === SubscriptionPlan.Free diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/index.tsx index 2bd5e8b5d2..272ddf1705 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/index.tsx @@ -1,4 +1,4 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; import { ConfirmModal } from '@affine/component/ui/modal'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; @@ -39,7 +39,6 @@ export const DeleteLeaveWorkspace = ({ const workspaceManager = useService(WorkspaceManager); const workspaceList = useLiveData(workspaceManager.list.workspaceList$); const currentWorkspace = useService(Workspace); - const pushNotification = useSetAtom(pushNotificationAtom); const onLeaveOrDelete = useCallback(() => { if (isOwner) { @@ -69,15 +68,11 @@ export const DeleteLeaveWorkspace = ({ } await workspaceManager.deleteWorkspace(workspaceMetadata); - pushNotification({ - title: t['Successfully deleted'](), - type: 'success', - }); + notify.success({ title: t['Successfully deleted']() }); }, [ currentWorkspace?.id, jumpToIndex, jumpToSubPath, - pushNotification, setSettingModal, t, workspaceList, diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx index 86df117517..c4acbed1b8 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx @@ -1,4 +1,4 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; import { Button } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; @@ -6,7 +6,6 @@ import { useSystemOnline } from '@affine/core/hooks/use-system-online'; import { apis } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Workspace, WorkspaceMetadata } from '@toeverything/infra'; -import { useSetAtom } from 'jotai'; import { useState } from 'react'; interface ExportPanelProps { @@ -23,7 +22,6 @@ export const ExportPanel = ({ const [saving, setSaving] = useState(false); const isOnline = useSystemOnline(); - const pushNotification = useSetAtom(pushNotificationAtom); const onExport = useAsyncCallback(async () => { if (saving || !workspace) { return; @@ -39,21 +37,14 @@ export const ExportPanel = ({ if (result?.error) { throw new Error(result.error); } else if (!result?.canceled) { - pushNotification({ - type: 'success', - title: t['Export success'](), - }); + notify.success({ title: t['Export success']() }); } } catch (e: any) { - pushNotification({ - type: 'error', - title: t['Export failed'](), - message: e.message, - }); + notify.error({ title: t['Export failed'](), message: e.message }); } finally { setSaving(false); } - }, [isOnline, pushNotification, saving, t, workspace, workspaceId]); + }, [isOnline, saving, t, workspace, workspaceId]); return ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx index d2b98623d6..6c2d22bc5d 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx @@ -1,3 +1,4 @@ +import { notify } from '@affine/component'; import type { InviteModalProps, PaginationProps, @@ -7,7 +8,6 @@ import { MemberLimitModal, Pagination, } from '@affine/component/member-components'; -import { pushNotificationAtom } from '@affine/component/notification-center'; import { SettingRow } from '@affine/component/setting-components'; import { Avatar } from '@affine/component/ui/avatar'; import { Button, IconButton } from '@affine/component/ui/button'; @@ -90,8 +90,6 @@ export const CloudWorkspaceMembersPanel = ({ const [open, setOpen] = useState(false); const [memberSkip, setMemberSkip] = useState(0); - const pushNotification = useSetAtom(pushNotificationAtom); - const openModal = useCallback(() => { setOpen(true); }, []); @@ -109,15 +107,14 @@ export const CloudWorkspaceMembersPanel = ({ true ); if (success) { - pushNotification({ + notify.success({ title: t['Invitation sent'](), message: t['Invitation sent hint'](), - type: 'success', }); setOpen(false); } }, - [invite, pushNotification, t] + [invite, t] ); const setSettingModalAtom = useSetAtom(openSettingModalAtom); @@ -146,13 +143,10 @@ export const CloudWorkspaceMembersPanel = ({ async memberId => { const res = await revokeMemberPermission(memberId); if (res?.revoke) { - pushNotification({ - title: t['Removed successfully'](), - type: 'success', - }); + notify.success({ title: t['Removed successfully']() }); } }, - [pushNotification, revokeMemberPermission, t] + [revokeMemberPermission, t] ); const desc = useMemo(() => { diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/profile.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/profile.tsx index 05ca4a171c..a861f42753 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/profile.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/profile.tsx @@ -1,5 +1,4 @@ -import { FlexWrapper, Input, Wrapper } from '@affine/component'; -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { FlexWrapper, Input, notify, Wrapper } from '@affine/component'; import { Avatar } from '@affine/component/ui/avatar'; import { Button } from '@affine/component/ui/button'; import { Upload } from '@affine/core/components/pure/file-upload'; @@ -11,7 +10,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CameraIcon } from '@blocksuite/icons'; import type { Workspace } from '@toeverything/infra'; import { useLiveData } from '@toeverything/infra'; -import { useSetAtom } from 'jotai'; import type { KeyboardEvent, MouseEvent } from 'react'; import { useCallback, useEffect, useState } from 'react'; @@ -24,7 +22,6 @@ export interface ProfilePanelProps extends WorkspaceSettingDetailProps { export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => { const t = useAFFiNEI18N(); - const pushNotification = useSetAtom(pushNotificationAtom); const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready; @@ -93,12 +90,9 @@ export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => { const handleUpdateWorkspaceName = useCallback( (name: string) => { setWorkspaceName(name); - pushNotification({ - title: t['Update workspace name success'](), - type: 'success', - }); + notify.success({ title: t['Update workspace name success']() }); }, - [pushNotification, setWorkspaceName, t] + [setWorkspaceName, t] ); const handleSetInput = useCallback((value: string) => { @@ -130,20 +124,16 @@ export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => { (file: File) => { setWorkspaceAvatar(file) .then(() => { - pushNotification({ - title: 'Update workspace avatar success', - type: 'success', - }); + notify.success({ title: 'Update workspace avatar success' }); }) .catch(error => { - pushNotification({ + notify.error({ title: 'Update workspace avatar failed', message: error, - type: 'error', }); }); }, - [pushNotification, setWorkspaceAvatar] + [setWorkspaceAvatar] ); const canAdjustAvatar = workspaceIsReady && avatarUrl && isOwner; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index c356a32c7e..5b231b03e5 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -1,5 +1,4 @@ -import { Tooltip } from '@affine/component'; -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify, Tooltip } from '@affine/component'; import { Avatar, type AvatarProps } from '@affine/component/ui/avatar'; import { Loading } from '@affine/component/ui/loading'; import { openSettingModalAtom } from '@affine/core/atoms'; @@ -81,7 +80,6 @@ const OfflineStatus = () => { const useSyncEngineSyncProgress = () => { const t = useAFFiNEI18N(); const isOnline = useSystemOnline(); - const pushNotification = useSetAtom(pushNotificationAtom); const { syncing, progress, retrying, errorMessage } = useDocEngineStatus(); const [isOverCapacity, setIsOverCapacity] = useState(false); @@ -89,7 +87,7 @@ const useSyncEngineSyncProgress = () => { const isOwner = useIsWorkspaceOwner(currentWorkspace.meta); const setSettingModalAtom = useSetAtom(openSettingModalAtom); - const jumpToPricePlan = useCallback(async () => { + const jumpToPricePlan = useCallback(() => { setSettingModalAtom({ open: true, activeTab: 'plans', @@ -108,17 +106,17 @@ const useSyncEngineSyncProgress = () => { } setIsOverCapacity(true); if (isOwner) { - pushNotification({ - type: 'warning', + notify.warning({ title: t['com.affine.payment.storage-limit.title'](), message: t['com.affine.payment.storage-limit.description.owner'](), - actionLabel: t['com.affine.payment.storage-limit.view'](), - action: jumpToPricePlan, + action: { + label: t['com.affine.payment.storage-limit.view'](), + onClick: jumpToPricePlan, + }, }); } else { - pushNotification({ - type: 'warning', + notify.warning({ title: t['com.affine.payment.storage-limit.title'](), message: t['com.affine.payment.storage-limit.description.member'](), @@ -129,7 +127,7 @@ const useSyncEngineSyncProgress = () => { return () => { disposableOverCapacity?.dispose(); }; - }, [currentWorkspace, isOwner, jumpToPricePlan, pushNotification, t]); + }, [currentWorkspace, isOwner, jumpToPricePlan, t]); const content = useMemo(() => { // TODO: add i18n diff --git a/packages/frontend/core/src/hooks/affine/use-export-page.ts b/packages/frontend/core/src/hooks/affine/use-export-page.ts index 64532c2036..45e9419550 100644 --- a/packages/frontend/core/src/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-export-page.ts @@ -1,8 +1,8 @@ +import { notify } from '@affine/component'; import { pushGlobalLoadingEventAtom, resolveGlobalLoadingEventAtom, } from '@affine/component/global-loading'; -import { pushNotificationAtom } from '@affine/component/notification-center'; import { apis } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageRootService, RootBlockModel } from '@blocksuite/blocks'; @@ -51,7 +51,6 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { } export const useExportPage = (page: Doc) => { - const pushNotification = useSetAtom(pushNotificationAtom); const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom); const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom); const t = useAFFiNEI18N(); @@ -67,29 +66,21 @@ export const useExportPage = (page: Doc) => { page, type, }); - pushNotification({ + notify.success({ title: t['com.affine.export.success.title'](), message: t['com.affine.export.success.message'](), - type: 'success', }); } catch (err) { console.error(err); - pushNotification({ + notify.error({ title: t['com.affine.export.error.title'](), message: t['com.affine.export.error.message'](), - type: 'error', }); } finally { resolveGlobalLoadingEvent(globalLoadingID); } }, - [ - page, - pushGlobalLoadingEvent, - pushNotification, - resolveGlobalLoadingEvent, - t, - ] + [page, pushGlobalLoadingEvent, resolveGlobalLoadingEvent, t] ); return onClickHandler; diff --git a/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts b/packages/frontend/core/src/hooks/affine/use-is-shared-page.tsx similarity index 88% rename from packages/frontend/core/src/hooks/affine/use-is-shared-page.ts rename to packages/frontend/core/src/hooks/affine/use-is-shared-page.tsx index 61e891f29a..06bc8891e0 100644 --- a/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-is-shared-page.tsx @@ -1,4 +1,4 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { getWorkspacePublicPagesQuery, @@ -7,8 +7,9 @@ import { revokePublicPageMutation, } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { SingleSelectSelectSolidIcon } from '@blocksuite/icons'; import type { PageMode, Workspace } from '@toeverything/infra'; -import { useSetAtom } from 'jotai'; +import { cssVar } from '@toeverything/theme'; import { useCallback, useMemo } from 'react'; import { useMutation } from '../use-mutation'; @@ -69,7 +70,6 @@ export function useIsSharedPage( enableShare: (mode: PageMode) => void; } { const t = useAFFiNEI18N(); - const pushNotification = useSetAtom(pushNotificationAtom); const { data, mutate } = useQuery({ query: getWorkspacePublicPagesQuery, variables: { @@ -103,24 +103,25 @@ export function useIsSharedPage( enableSharePage({ workspaceId, pageId, mode: publishMode }) .then(() => { - pushNotification({ + notify.success({ title: t[notificationToI18nKey['enableSuccessTitle']](), message: t[notificationToI18nKey['enableSuccessMessage']](), - type: 'success', - theme: 'default', + style: 'normal', + icon: ( + + ), }); return mutate(); }) .catch(e => { - pushNotification({ + notify.error({ title: t[notificationToI18nKey['enableErrorTitle']](), message: t[notificationToI18nKey['enableErrorMessage']](), - type: 'error', }); console.error(e); }); }, - [enableSharePage, mutate, pageId, pushNotification, t, workspaceId] + [enableSharePage, mutate, pageId, t, workspaceId] ); const changeShare = useCallback( @@ -130,7 +131,7 @@ export function useIsSharedPage( enableSharePage({ workspaceId, pageId, mode: publishMode }) .then(() => { - pushNotification({ + notify.success({ title: t[notificationToI18nKey['changeSuccessTitle']](), message: t[ 'com.affine.share-menu.confirm-modify-mode.notification.success.message' @@ -144,43 +145,43 @@ export function useIsSharedPage( ? t['Edgeless']() : t['Page'](), }), - type: 'success', - theme: 'default', + style: 'normal', + icon: ( + + ), }); return mutate(); }) .catch(e => { - pushNotification({ + notify.error({ title: t[notificationToI18nKey['changeErrorTitle']](), message: t[notificationToI18nKey['changeErrorMessage']](), - type: 'error', }); console.error(e); }); }, - [enableSharePage, mutate, pageId, pushNotification, t, workspaceId] + [enableSharePage, mutate, pageId, t, workspaceId] ); const disableShare = useCallback(() => { disableSharePage({ workspaceId, pageId }) .then(() => { - pushNotification({ + notify.success({ title: t[notificationToI18nKey['disableSuccessTitle']](), message: t[notificationToI18nKey['disableSuccessMessage']](), - type: 'success', - theme: 'default', + style: 'normal', + icon: , }); return mutate(); }) .catch(e => { - pushNotification({ + notify.error({ title: t[notificationToI18nKey['disableErrorTitle']](), message: t[notificationToI18nKey['disableErrorMessage']](), - type: 'error', }); console.error(e); }); - }, [disableSharePage, mutate, pageId, pushNotification, t, workspaceId]); + }, [disableSharePage, mutate, pageId, t, workspaceId]); return useMemo( () => ({ diff --git a/packages/frontend/core/src/pages/auth.tsx b/packages/frontend/core/src/pages/auth.tsx index d70850dd28..0a3a400e9d 100644 --- a/packages/frontend/core/src/pages/auth.tsx +++ b/packages/frontend/core/src/pages/auth.tsx @@ -1,3 +1,4 @@ +import { notify } from '@affine/component'; import { ChangeEmailPage, ChangePasswordPage, @@ -7,7 +8,6 @@ import { SignInSuccessPage, SignUpPage, } from '@affine/component/auth-components'; -import { pushNotificationAtom } from '@affine/component/notification-center'; import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { changeEmailMutation, @@ -17,7 +17,6 @@ import { verifyEmailMutation, } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useSetAtom } from 'jotai/react'; import type { ReactElement } from 'react'; import { useCallback } from 'react'; import type { LoaderFunction } from 'react-router-dom'; @@ -50,7 +49,6 @@ export const AuthPage = (): ReactElement | null => { const { authType } = useParams(); const [searchParams] = useSearchParams(); - const pushNotification = useSetAtom(pushNotificationAtom); const { trigger: changePassword } = useMutation({ mutation: changePasswordMutation, @@ -72,20 +70,18 @@ export const AuthPage = (): ReactElement | null => { // FIXME: There is not notification if (res?.sendVerifyChangeEmail) { - pushNotification({ + notify.success({ title: t['com.affine.auth.sent.verify.email.hint'](), - type: 'success', }); } else { - pushNotification({ + notify.error({ title: t['com.affine.auth.sent.change.email.fail'](), - type: 'error', }); } return !!res?.sendVerifyChangeEmail; }, - [pushNotification, searchParams, sendVerifyChangeEmail, t] + [searchParams, sendVerifyChangeEmail, t] ); const onSetPassword = useCallback( diff --git a/packages/frontend/core/src/pages/workspace/collection/index.tsx b/packages/frontend/core/src/pages/workspace/collection/index.tsx index 46a64f0f74..12e161cd00 100644 --- a/packages/frontend/core/src/pages/workspace/collection/index.tsx +++ b/packages/frontend/core/src/pages/workspace/collection/index.tsx @@ -1,4 +1,4 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { AffineShapeIcon, useEditCollection, @@ -17,7 +17,6 @@ import { ViewLayersIcon, } from '@blocksuite/icons'; import { useLiveData, useService, Workspace } from '@toeverything/infra'; -import { useSetAtom } from 'jotai'; import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -70,7 +69,6 @@ export const Component = function CollectionPage() { const params = useParams(); const workspace = useService(Workspace); const collection = collections.find(v => v.id === params.collectionId); - const pushNotification = useSetAtom(pushNotificationAtom); useEffect(() => { if (!collection) { navigate.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); @@ -85,17 +83,13 @@ export const Component = function CollectionPage() { text = `${collection.collection.name} has been deleted`; } } - pushNotification({ - type: 'error', - title: text, - }); + notify.error({ title: text }); } }, [ collection, collectionService.collectionsTrash$.value, navigate, params.collectionId, - pushNotification, workspace.docCollection, workspace.id, ]); diff --git a/packages/frontend/core/src/providers/session-provider.tsx b/packages/frontend/core/src/providers/session-provider.tsx index 57a437b564..7f5b332034 100644 --- a/packages/frontend/core/src/providers/session-provider.tsx +++ b/packages/frontend/core/src/providers/session-provider.tsx @@ -1,10 +1,9 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { useSession } from '@affine/core/hooks/affine/use-current-user'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { affine } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from '@affine/workspace-impl'; -import { useSetAtom } from 'jotai'; import type { PropsWithChildren } from 'react'; import { startTransition, useEffect, useRef } from 'react'; @@ -14,7 +13,6 @@ import { mixpanel } from '../utils'; export const CloudSessionProvider = (props: PropsWithChildren) => { const session = useSession(); const prevSession = useRef>(); - const pushNotification = useSetAtom(pushNotificationAtom); const onceSignedInEvents = useOnceSignedInEvents(); const t = useAFFiNEI18N(); @@ -39,10 +37,9 @@ export const CloudSessionProvider = (props: PropsWithChildren) => { session.status === 'authenticated' ) { startTransition(() => refreshAfterSignedInEvents()); - pushNotification({ + notify.success({ title: t['com.affine.auth.has.signed'](), message: t['com.affine.auth.has.signed.message'](), - type: 'success', }); if (environment.isDesktop) { @@ -51,7 +48,7 @@ export const CloudSessionProvider = (props: PropsWithChildren) => { } prevSession.current = session; } - }, [session, prevSession, pushNotification, refreshAfterSignedInEvents, t]); + }, [session, prevSession, refreshAfterSignedInEvents, t]); return props.children; }; diff --git a/packages/frontend/core/src/providers/swr-config-provider.tsx b/packages/frontend/core/src/providers/swr-config-provider.tsx index fcdbc2b3bc..3bdae875f8 100644 --- a/packages/frontend/core/src/providers/swr-config-provider.tsx +++ b/packages/frontend/core/src/providers/swr-config-provider.tsx @@ -1,7 +1,6 @@ -import { pushNotificationAtom } from '@affine/component/notification-center'; +import { notify } from '@affine/component'; import { assertExists } from '@blocksuite/global/utils'; import { GraphQLError } from 'graphql'; -import { useSetAtom } from 'jotai'; import type { PropsWithChildren, ReactNode } from 'react'; import { useCallback } from 'react'; import type { SWRConfiguration } from 'swr'; @@ -11,7 +10,6 @@ const swrConfig: SWRConfiguration = { suspense: true, use: [ useSWRNext => (key, fetcher, config) => { - const pushNotification = useSetAtom(pushNotificationAtom); const fetcherWrapper = useCallback( async (...args: any[]) => { assertExists(fetcher); @@ -23,18 +21,14 @@ const swrConfig: SWRConfiguration = { (Array.isArray(e) && e[0] instanceof GraphQLError) ) { const graphQLError = e instanceof GraphQLError ? e : e[0]; - pushNotification({ + notify.error({ title: 'GraphQL Error', message: graphQLError.toString(), - key: Date.now().toString(), - type: 'error', }); } else { - pushNotification({ + notify.error({ title: 'Error', message: e.toString(), - key: Date.now().toString(), - type: 'error', }); } throw e; @@ -42,7 +36,7 @@ const swrConfig: SWRConfiguration = { } return d; }, - [fetcher, pushNotification] + [fetcher] ); return useSWRNext(key, fetcher ? fetcherWrapper : fetcher, config); }, diff --git a/packages/frontend/electron/renderer/app.tsx b/packages/frontend/electron/renderer/app.tsx index a4076efbc4..dcbff671e5 100644 --- a/packages/frontend/electron/renderer/app.tsx +++ b/packages/frontend/electron/renderer/app.tsx @@ -1,9 +1,9 @@ import '@affine/component/theme/global.css'; import '@affine/component/theme/theme.css'; +import { NotificationCenter } from '@affine/component'; import { AffineContext } from '@affine/component/context'; import { GlobalLoading } from '@affine/component/global-loading'; -import { NotificationCenter } from '@affine/component/notification-center'; import { WorkspaceFallback } from '@affine/core/components/workspace'; import { GlobalScopeProvider } from '@affine/core/modules/infra-web/global-scope'; import { CloudSessionProvider } from '@affine/core/providers/session-provider'; diff --git a/packages/frontend/web/src/app.tsx b/packages/frontend/web/src/app.tsx index a4076efbc4..dcbff671e5 100644 --- a/packages/frontend/web/src/app.tsx +++ b/packages/frontend/web/src/app.tsx @@ -1,9 +1,9 @@ import '@affine/component/theme/global.css'; import '@affine/component/theme/theme.css'; +import { NotificationCenter } from '@affine/component'; import { AffineContext } from '@affine/component/context'; import { GlobalLoading } from '@affine/component/global-loading'; -import { NotificationCenter } from '@affine/component/notification-center'; import { WorkspaceFallback } from '@affine/core/components/workspace'; import { GlobalScopeProvider } from '@affine/core/modules/infra-web/global-scope'; import { CloudSessionProvider } from '@affine/core/providers/session-provider'; diff --git a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts index bab57cd2d4..7e585c7123 100644 --- a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts +++ b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts @@ -27,7 +27,7 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => { await openWorkspaceSettingPanel(page, 'Test Workspace'); await page.getByTestId('delete-workspace-button').click(); await expect( - page.getByTestId('affine-notification').first() + page.locator('.affine-notification-center').first() ).not.toBeVisible(); const workspaceNameDom = page.getByTestId('workspace-name'); const currentWorkspaceName = (await workspaceNameDom.evaluate( @@ -38,7 +38,8 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => { .getByTestId('delete-workspace-input') .pressSequentially(currentWorkspaceName); const promise = page - .getByTestId('affine-notification') + .locator('.affine-notification-center') + .first() .waitFor({ state: 'attached' }); await page.getByTestId('delete-workspace-confirm-button').click(); await promise;