diff --git a/packages/common/infra/src/framework/react/index.tsx b/packages/common/infra/src/framework/react/index.tsx
index 8577bca001..4805e37ac4 100644
--- a/packages/common/infra/src/framework/react/index.tsx
+++ b/packages/common/infra/src/framework/react/index.tsx
@@ -71,7 +71,8 @@ export const FrameworkScope = ({
const nextStack = useMemo(() => {
if (!scope) return provider;
- return new FrameworkStackProvider([provider, scope.framework]);
+ // make sure the stack order is inside to outside
+ return new FrameworkStackProvider([scope.framework, provider]);
}, [scope, provider]);
return (
diff --git a/packages/frontend/component/src/ui/notification/desktop/notification-card.tsx b/packages/frontend/component/src/ui/notification/desktop/notification-card.tsx
index 812aae7372..d8b57141e3 100644
--- a/packages/frontend/component/src/ui/notification/desktop/notification-card.tsx
+++ b/packages/frontend/component/src/ui/notification/desktop/notification-card.tsx
@@ -1,3 +1,4 @@
+import { useI18n } from '@affine/i18n';
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { useCallback } from 'react';
@@ -10,6 +11,7 @@ import * as styles from './styles.css';
export const DesktopNotificationCard = ({
notification,
}: NotificationCardProps) => {
+ const t = useI18n();
const {
theme = 'info',
style = 'normal',
@@ -17,6 +19,7 @@ export const DesktopNotificationCard = ({
iconColor,
thumb,
action,
+ error,
title,
footer,
alignMessage = 'title',
@@ -24,6 +27,12 @@ export const DesktopNotificationCard = ({
rootAttrs,
} = notification;
+ const errorI18nKey = error ? (`error.${error.name}` as const) : undefined;
+ const errorTitle =
+ errorI18nKey && errorI18nKey in t
+ ? t[errorI18nKey](error?.data)
+ : undefined;
+
const onActionClicked = useCallback(() => {
action?.onClick()?.catch(console.error);
if (action?.autoClose !== false) {
@@ -46,7 +55,7 @@ export const DesktopNotificationCard = ({
{icon}
) : null}
-
diff --git a/packages/frontend/component/src/ui/notification/notify.tsx b/packages/frontend/component/src/ui/notification/notify.tsx
index c53e6baa2a..b851926b19 100644
--- a/packages/frontend/component/src/ui/notification/notify.tsx
+++ b/packages/frontend/component/src/ui/notification/notify.tsx
@@ -1,3 +1,4 @@
+import { UserFriendlyError } from '@affine/error';
import {
InformationFillDuotoneIcon,
SingleSelectCheckSolidIcon,
@@ -35,7 +36,16 @@ export function notify(notification: Notification, options?: ExternalToast) {
}, options);
}
-notify.error = (notification: Notification, options?: ExternalToast) => {
+notify.error = (
+ notification: Notification | UserFriendlyError,
+ options?: ExternalToast
+) => {
+ if (notification instanceof UserFriendlyError) {
+ notification = {
+ error: notification,
+ };
+ }
+
return notify(
{
icon:
,
diff --git a/packages/frontend/component/src/ui/notification/types.ts b/packages/frontend/component/src/ui/notification/types.ts
index 9344d0e8cb..eced811437 100644
--- a/packages/frontend/component/src/ui/notification/types.ts
+++ b/packages/frontend/component/src/ui/notification/types.ts
@@ -1,3 +1,4 @@
+import type { UserFriendlyError } from '@affine/error';
import type { HTMLAttributes, ReactNode } from 'react';
import type { ButtonProps } from '../button';
@@ -29,6 +30,7 @@ export interface Notification {
thumb?: ReactNode;
title?: ReactNode;
message?: ReactNode;
+ error?: UserFriendlyError;
icon?: ReactNode;
iconColor?: string;
footer?: ReactNode;
diff --git a/packages/frontend/core/src/components/hooks/use-navigate-helper.ts b/packages/frontend/core/src/components/hooks/use-navigate-helper.ts
index 94e1a0908e..dd0e2990b4 100644
--- a/packages/frontend/core/src/components/hooks/use-navigate-helper.ts
+++ b/packages/frontend/core/src/components/hooks/use-navigate-helper.ts
@@ -1,3 +1,4 @@
+import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { toURLSearchParams } from '@affine/core/modules/navigation';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
import type { DocMode } from '@blocksuite/affine/model';
@@ -183,6 +184,25 @@ export function useNavigateHelper() {
[navigate]
);
+ const jumpToWorkspaceSettings = useCallback(
+ (
+ workspaceId: string,
+ tab?: SettingTab,
+ logic: RouteLogic = RouteLogic.PUSH
+ ) => {
+ const searchParams = new URLSearchParams();
+ if (tab) {
+ searchParams.set('tab', tab);
+ }
+ return navigate(
+ `/workspace/${workspaceId}/settings?${searchParams.toString()}`,
+ {
+ replace: logic === RouteLogic.REPLACE,
+ }
+ );
+ },
+ [navigate]
+ );
return useMemo(
() => ({
jumpToPage,
@@ -198,6 +218,7 @@ export function useNavigateHelper() {
jumpToTag,
jumpToOpenInApp,
jumpToImportTemplate,
+ jumpToWorkspaceSettings,
}),
[
jumpToPage,
@@ -213,6 +234,7 @@ export function useNavigateHelper() {
jumpToTag,
jumpToOpenInApp,
jumpToImportTemplate,
+ jumpToWorkspaceSettings,
]
);
}
diff --git a/packages/frontend/core/src/components/notification/list.style.css.ts b/packages/frontend/core/src/components/notification/list.style.css.ts
index 8f9280d581..72cce92945 100644
--- a/packages/frontend/core/src/components/notification/list.style.css.ts
+++ b/packages/frontend/core/src/components/notification/list.style.css.ts
@@ -34,6 +34,7 @@ export const itemContainer = style({
position: 'relative',
padding: '8px',
gap: '8px',
+ cursor: 'pointer',
selectors: {
[`&:hover:not([data-disabled="true"])`]: {
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
@@ -81,9 +82,17 @@ export const itemDeleteButton = style({
export const itemNameLabel = style({
fontWeight: 'bold',
color: cssVarV2('text/primary'),
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: '4px',
+ lineHeight: '22px',
selectors: {
[`&[data-inactived="true"]`]: {
color: cssVarV2('text/placeholder'),
},
},
});
+
+export const itemActionButton = style({
+ width: 'fit-content',
+});
diff --git a/packages/frontend/core/src/components/notification/list.tsx b/packages/frontend/core/src/components/notification/list.tsx
index 31657fbbb0..5b2157d780 100644
--- a/packages/frontend/core/src/components/notification/list.tsx
+++ b/packages/frontend/core/src/components/notification/list.tsx
@@ -1,16 +1,31 @@
-import { Avatar, IconButton, Scrollable, Skeleton } from '@affine/component';
+import {
+ Avatar,
+ Button,
+ IconButton,
+ notify,
+ Scrollable,
+ Skeleton,
+} from '@affine/component';
+import { AcceptInviteService } from '@affine/core/modules/cloud';
import {
type Notification,
NotificationListService,
NotificationType,
} from '@affine/core/modules/notification';
+import { WorkspacesService } from '@affine/core/modules/workspace';
import { UserFriendlyError } from '@affine/error';
-import type { MentionNotificationBodyType } from '@affine/graphql';
+import type {
+ InvitationAcceptedNotificationBodyType,
+ InvitationBlockedNotificationBodyType,
+ InvitationNotificationBodyType,
+ MentionNotificationBodyType,
+} from '@affine/graphql';
import { i18nTime, Trans, useI18n } from '@affine/i18n';
-import { DeleteIcon } from '@blocksuite/icons/rc';
+import { CollaborationIcon, DeleteIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useNavigateHelper } from '../hooks/use-navigate-helper';
import * as styles from './list.style.css';
export const NotificationList = () => {
@@ -92,34 +107,24 @@ const NotificationItemSkeleton = () => {
};
const NotificationItem = ({ notification }: { notification: Notification }) => {
- const notificationListService = useService(NotificationListService);
const t = useI18n();
const type = notification.type;
- const handleDelete = useCallback(() => {
- notificationListService.readNotification(notification.id).catch(err => {
- console.error(err);
- });
- }, [notificationListService, notification.id]);
-
- return (
+ return type === NotificationType.Mention ? (
+
+ ) : type === NotificationType.InvitationAccepted ? (
+
+ ) : type === NotificationType.Invitation ? (
+
+ ) : type === NotificationType.InvitationBlocked ? (
+
+ ) : (
- {type === NotificationType.Mention ? (
-
- ) : (
- <>
-
-
- {t['com.affine.notification.unsupported']()} ({type})
-
- >
- )}
-
}
- onClick={handleDelete}
- />
+
+
+ {t['com.affine.notification.unsupported']()} ({type})
+
+
);
};
@@ -129,13 +134,30 @@ const MentionNotificationItem = ({
}: {
notification: Notification;
}) => {
+ const notificationListService = useService(NotificationListService);
+ const { jumpToPageBlock } = useNavigateHelper();
const t = useI18n();
const body = notification.body as MentionNotificationBodyType;
const memberInactived = !body.createdByUser;
- const username =
- body.createdByUser?.name ?? t['com.affine.inactive-member']();
+
+ const handleClick = useCallback(() => {
+ if (!body.workspace?.id) {
+ return;
+ }
+ notificationListService.readNotification(notification.id).catch(err => {
+ console.error(err);
+ });
+ jumpToPageBlock(
+ body.workspace.id,
+ body.doc.id,
+ body.doc.mode,
+ body.doc.blockId ? [body.doc.blockId] : undefined,
+ body.doc.elementId ? [body.doc.elementId] : undefined
+ );
+ }, [body, jumpToPageBlock, notificationListService, notification.id]);
+
return (
- <>
+
,
}}
values={{
- username: username,
- docTitle: body.doc.title ?? t['Untitled'](),
+ username:
+ body.createdByUser?.name ?? t['com.affine.inactive-member'](),
+ docTitle: body.doc.title || t['Untitled'](),
}}
/>
@@ -166,6 +189,286 @@ const MentionNotificationItem = ({
})}
- >
+