feat(core): add more notification types (#11156)
This commit is contained in:
@@ -71,7 +71,8 @@ export const FrameworkScope = ({
|
|||||||
|
|
||||||
const nextStack = useMemo(() => {
|
const nextStack = useMemo(() => {
|
||||||
if (!scope) return provider;
|
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]);
|
}, [scope, provider]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -10,6 +11,7 @@ import * as styles from './styles.css';
|
|||||||
export const DesktopNotificationCard = ({
|
export const DesktopNotificationCard = ({
|
||||||
notification,
|
notification,
|
||||||
}: NotificationCardProps) => {
|
}: NotificationCardProps) => {
|
||||||
|
const t = useI18n();
|
||||||
const {
|
const {
|
||||||
theme = 'info',
|
theme = 'info',
|
||||||
style = 'normal',
|
style = 'normal',
|
||||||
@@ -17,6 +19,7 @@ export const DesktopNotificationCard = ({
|
|||||||
iconColor,
|
iconColor,
|
||||||
thumb,
|
thumb,
|
||||||
action,
|
action,
|
||||||
|
error,
|
||||||
title,
|
title,
|
||||||
footer,
|
footer,
|
||||||
alignMessage = 'title',
|
alignMessage = 'title',
|
||||||
@@ -24,6 +27,12 @@ export const DesktopNotificationCard = ({
|
|||||||
rootAttrs,
|
rootAttrs,
|
||||||
} = notification;
|
} = notification;
|
||||||
|
|
||||||
|
const errorI18nKey = error ? (`error.${error.name}` as const) : undefined;
|
||||||
|
const errorTitle =
|
||||||
|
errorI18nKey && errorI18nKey in t
|
||||||
|
? t[errorI18nKey](error?.data)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const onActionClicked = useCallback(() => {
|
const onActionClicked = useCallback(() => {
|
||||||
action?.onClick()?.catch(console.error);
|
action?.onClick()?.catch(console.error);
|
||||||
if (action?.autoClose !== false) {
|
if (action?.autoClose !== false) {
|
||||||
@@ -46,7 +55,7 @@ export const DesktopNotificationCard = ({
|
|||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title ?? errorTitle}</div>
|
||||||
|
|
||||||
{action ? (
|
{action ? (
|
||||||
<div className={clsx(styles.headAlignWrapper, styles.action)}>
|
<div className={clsx(styles.headAlignWrapper, styles.action)}>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import {
|
import {
|
||||||
InformationFillDuotoneIcon,
|
InformationFillDuotoneIcon,
|
||||||
SingleSelectCheckSolidIcon,
|
SingleSelectCheckSolidIcon,
|
||||||
@@ -35,7 +36,16 @@ export function notify(notification: Notification, options?: ExternalToast) {
|
|||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify.error = (notification: Notification, options?: ExternalToast) => {
|
notify.error = (
|
||||||
|
notification: Notification | UserFriendlyError,
|
||||||
|
options?: ExternalToast
|
||||||
|
) => {
|
||||||
|
if (notification instanceof UserFriendlyError) {
|
||||||
|
notification = {
|
||||||
|
error: notification,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return notify(
|
return notify(
|
||||||
{
|
{
|
||||||
icon: <InformationFillDuotoneIcon />,
|
icon: <InformationFillDuotoneIcon />,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { UserFriendlyError } from '@affine/error';
|
||||||
import type { HTMLAttributes, ReactNode } from 'react';
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import type { ButtonProps } from '../button';
|
import type { ButtonProps } from '../button';
|
||||||
@@ -29,6 +30,7 @@ export interface Notification {
|
|||||||
thumb?: ReactNode;
|
thumb?: ReactNode;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
message?: ReactNode;
|
message?: ReactNode;
|
||||||
|
error?: UserFriendlyError;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||||
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
|
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
|
||||||
import type { DocMode } from '@blocksuite/affine/model';
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
@@ -183,6 +184,25 @@ export function useNavigateHelper() {
|
|||||||
[navigate]
|
[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(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
jumpToPage,
|
jumpToPage,
|
||||||
@@ -198,6 +218,7 @@ export function useNavigateHelper() {
|
|||||||
jumpToTag,
|
jumpToTag,
|
||||||
jumpToOpenInApp,
|
jumpToOpenInApp,
|
||||||
jumpToImportTemplate,
|
jumpToImportTemplate,
|
||||||
|
jumpToWorkspaceSettings,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
jumpToPage,
|
jumpToPage,
|
||||||
@@ -213,6 +234,7 @@ export function useNavigateHelper() {
|
|||||||
jumpToTag,
|
jumpToTag,
|
||||||
jumpToOpenInApp,
|
jumpToOpenInApp,
|
||||||
jumpToImportTemplate,
|
jumpToImportTemplate,
|
||||||
|
jumpToWorkspaceSettings,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const itemContainer = style({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`&:hover:not([data-disabled="true"])`]: {
|
[`&:hover:not([data-disabled="true"])`]: {
|
||||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
@@ -81,9 +82,17 @@ export const itemDeleteButton = style({
|
|||||||
export const itemNameLabel = style({
|
export const itemNameLabel = style({
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: cssVarV2('text/primary'),
|
color: cssVarV2('text/primary'),
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
lineHeight: '22px',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`&[data-inactived="true"]`]: {
|
[`&[data-inactived="true"]`]: {
|
||||||
color: cssVarV2('text/placeholder'),
|
color: cssVarV2('text/placeholder'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const itemActionButton = style({
|
||||||
|
width: 'fit-content',
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
type Notification,
|
type Notification,
|
||||||
NotificationListService,
|
NotificationListService,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '@affine/core/modules/notification';
|
} from '@affine/core/modules/notification';
|
||||||
|
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||||
import { UserFriendlyError } from '@affine/error';
|
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 { 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 { 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';
|
import * as styles from './list.style.css';
|
||||||
|
|
||||||
export const NotificationList = () => {
|
export const NotificationList = () => {
|
||||||
@@ -92,34 +107,24 @@ const NotificationItemSkeleton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NotificationItem = ({ notification }: { notification: Notification }) => {
|
const NotificationItem = ({ notification }: { notification: Notification }) => {
|
||||||
const notificationListService = useService(NotificationListService);
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const type = notification.type;
|
const type = notification.type;
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
return type === NotificationType.Mention ? (
|
||||||
notificationListService.readNotification(notification.id).catch(err => {
|
<MentionNotificationItem notification={notification} />
|
||||||
console.error(err);
|
) : type === NotificationType.InvitationAccepted ? (
|
||||||
});
|
<InvitationAcceptedNotificationItem notification={notification} />
|
||||||
}, [notificationListService, notification.id]);
|
) : type === NotificationType.Invitation ? (
|
||||||
|
<InvitationNotificationItem notification={notification} />
|
||||||
return (
|
) : type === NotificationType.InvitationBlocked ? (
|
||||||
|
<InvitationBlockedNotificationItem notification={notification} />
|
||||||
|
) : (
|
||||||
<div className={styles.itemContainer}>
|
<div className={styles.itemContainer}>
|
||||||
{type === NotificationType.Mention ? (
|
<Avatar size={22} />
|
||||||
<MentionNotificationItem notification={notification} />
|
<div className={styles.itemNotSupported}>
|
||||||
) : (
|
{t['com.affine.notification.unsupported']()} ({type})
|
||||||
<>
|
</div>
|
||||||
<Avatar size={22} />
|
<DeleteButton notification={notification} />
|
||||||
<div className={styles.itemNotSupported}>
|
|
||||||
{t['com.affine.notification.unsupported']()} ({type})
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
size={16}
|
|
||||||
className={styles.itemDeleteButton}
|
|
||||||
icon={<DeleteIcon />}
|
|
||||||
onClick={handleDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -129,13 +134,30 @@ const MentionNotificationItem = ({
|
|||||||
}: {
|
}: {
|
||||||
notification: Notification;
|
notification: Notification;
|
||||||
}) => {
|
}) => {
|
||||||
|
const notificationListService = useService(NotificationListService);
|
||||||
|
const { jumpToPageBlock } = useNavigateHelper();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const body = notification.body as MentionNotificationBodyType;
|
const body = notification.body as MentionNotificationBodyType;
|
||||||
const memberInactived = !body.createdByUser;
|
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 (
|
return (
|
||||||
<>
|
<div className={styles.itemContainer} onClick={handleClick}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={22}
|
size={22}
|
||||||
name={body.createdByUser?.name}
|
name={body.createdByUser?.name}
|
||||||
@@ -155,8 +177,9 @@ const MentionNotificationItem = ({
|
|||||||
2: <b className={styles.itemNameLabel} />,
|
2: <b className={styles.itemNameLabel} />,
|
||||||
}}
|
}}
|
||||||
values={{
|
values={{
|
||||||
username: username,
|
username:
|
||||||
docTitle: body.doc.title ?? t['Untitled'](),
|
body.createdByUser?.name ?? t['com.affine.inactive-member'](),
|
||||||
|
docTitle: body.doc.title || t['Untitled'](),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -166,6 +189,286 @@ const MentionNotificationItem = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<DeleteButton notification={notification} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InvitationAcceptedNotificationItem = ({
|
||||||
|
notification,
|
||||||
|
}: {
|
||||||
|
notification: Notification;
|
||||||
|
}) => {
|
||||||
|
const notificationListService = useService(NotificationListService);
|
||||||
|
const { jumpToWorkspaceSettings } = useNavigateHelper();
|
||||||
|
const t = useI18n();
|
||||||
|
const body = notification.body as InvitationAcceptedNotificationBodyType;
|
||||||
|
const memberInactived = !body.createdByUser;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
notificationListService.readNotification(notification.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
if (!body.workspace?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jumpToWorkspaceSettings(body.workspace.id, 'workspace:members');
|
||||||
|
}, [body, jumpToWorkspaceSettings, notification, notificationListService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer} onClick={handleClick}>
|
||||||
|
<Avatar
|
||||||
|
size={22}
|
||||||
|
name={body.createdByUser?.name}
|
||||||
|
url={body.createdByUser?.avatarUrl}
|
||||||
|
/>
|
||||||
|
<div className={styles.itemMain}>
|
||||||
|
<span>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'com.affine.notification.invitation-accepted'}
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<b
|
||||||
|
className={styles.itemNameLabel}
|
||||||
|
data-inactived={memberInactived}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
username:
|
||||||
|
body.createdByUser?.name ?? t['com.affine.inactive-member'](),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className={styles.itemDate}>
|
||||||
|
{i18nTime(notification.createdAt, {
|
||||||
|
relative: true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DeleteButton notification={notification} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InvitationBlockedNotificationItem = ({
|
||||||
|
notification,
|
||||||
|
}: {
|
||||||
|
notification: Notification;
|
||||||
|
}) => {
|
||||||
|
const notificationListService = useService(NotificationListService);
|
||||||
|
const { jumpToWorkspaceSettings } = useNavigateHelper();
|
||||||
|
const t = useI18n();
|
||||||
|
const body = notification.body as InvitationBlockedNotificationBodyType;
|
||||||
|
const workspaceInactived = !body.workspace;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
notificationListService.readNotification(notification.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
if (!body.workspace?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jumpToWorkspaceSettings(body.workspace.id, 'workspace:members');
|
||||||
|
}, [body, jumpToWorkspaceSettings, notification, notificationListService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer} onClick={handleClick}>
|
||||||
|
<CollaborationIcon width={22} height={22} />
|
||||||
|
<div className={styles.itemMain}>
|
||||||
|
<span>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'com.affine.notification.invitation-blocked'}
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<b
|
||||||
|
className={styles.itemNameLabel}
|
||||||
|
data-inactived={workspaceInactived}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
workspaceName:
|
||||||
|
body.workspace?.name ?? t['com.affine.inactive-workspace'](),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className={styles.itemDate}>
|
||||||
|
{i18nTime(notification.createdAt, {
|
||||||
|
relative: true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DeleteButton notification={notification} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InvitationNotificationItem = ({
|
||||||
|
notification,
|
||||||
|
}: {
|
||||||
|
notification: Notification;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const body = notification.body as InvitationNotificationBodyType;
|
||||||
|
const memberInactived = !body.createdByUser;
|
||||||
|
const workspaceInactived = !body.workspace;
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const acceptInviteService = useService(AcceptInviteService);
|
||||||
|
const notificationListService = useService(NotificationListService);
|
||||||
|
const inviteId = body.inviteId;
|
||||||
|
const [isAccepting, setIsAccepting] = useState(false);
|
||||||
|
const { jumpToPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const WorkspaceNameWithIcon = useCallback(
|
||||||
|
({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>) => {
|
||||||
|
return (
|
||||||
|
<b className={styles.itemNameLabel} {...props}>
|
||||||
|
<CollaborationIcon width={20} height={20} />
|
||||||
|
{children}
|
||||||
|
</b>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleReadAndOpenWorkspace = useCallback(() => {
|
||||||
|
notificationListService.readNotification(notification.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
if (!body.workspace?.id) {
|
||||||
|
return; // should never happen
|
||||||
|
}
|
||||||
|
jumpToPage(body.workspace.id, 'all');
|
||||||
|
}, [body, jumpToPage, notification.id, notificationListService]);
|
||||||
|
|
||||||
|
const handleAcceptInvite = useCallback(() => {
|
||||||
|
setIsAccepting(true);
|
||||||
|
acceptInviteService
|
||||||
|
.waitForAcceptInvite(inviteId)
|
||||||
|
.catch(err => {
|
||||||
|
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||||
|
if (userFriendlyError.is('ALREADY_IN_SPACE')) {
|
||||||
|
// ignore if the user is already in the workspace
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
.then(async value => {
|
||||||
|
if (value === false) {
|
||||||
|
// invite is expired
|
||||||
|
notify.error({
|
||||||
|
title: t['com.affine.expired.page.title'](),
|
||||||
|
message: t['com.affine.expired.page.new-subtitle'](),
|
||||||
|
});
|
||||||
|
notificationListService
|
||||||
|
.readNotification(notification.id)
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// invite is accepted
|
||||||
|
await workspacesService.list.waitForRevalidation();
|
||||||
|
handleReadAndOpenWorkspace();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||||
|
notify.error(userFriendlyError);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsAccepting(false);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
acceptInviteService,
|
||||||
|
handleReadAndOpenWorkspace,
|
||||||
|
inviteId,
|
||||||
|
notification,
|
||||||
|
notificationListService,
|
||||||
|
t,
|
||||||
|
workspacesService,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<Avatar
|
||||||
|
size={22}
|
||||||
|
name={body.createdByUser?.name}
|
||||||
|
url={body.createdByUser?.avatarUrl}
|
||||||
|
/>
|
||||||
|
<div className={styles.itemMain}>
|
||||||
|
<span>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'com.affine.notification.invitation'}
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<b
|
||||||
|
className={styles.itemNameLabel}
|
||||||
|
data-inactived={memberInactived}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
2: <WorkspaceNameWithIcon data-inactived={workspaceInactived} />,
|
||||||
|
}}
|
||||||
|
values={{
|
||||||
|
username:
|
||||||
|
body.createdByUser?.name ?? t['com.affine.inactive-member'](),
|
||||||
|
workspaceName:
|
||||||
|
body.workspace?.name ?? t['com.affine.inactive-workspace'](),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{!workspaceInactived && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className={styles.itemActionButton}
|
||||||
|
onClick={handleAcceptInvite}
|
||||||
|
loading={isAccepting}
|
||||||
|
>
|
||||||
|
{t['com.affine.notification.invitation.accept']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className={styles.itemDate}>
|
||||||
|
{i18nTime(notification.createdAt, {
|
||||||
|
relative: true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DeleteButton notification={notification} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteButton = ({
|
||||||
|
notification,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
notification: Notification;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
const notificationListService = useService(NotificationListService);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation(); // prevent trigger the click event of the parent element
|
||||||
|
|
||||||
|
notificationListService.readNotification(notification.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
onClick?.();
|
||||||
|
},
|
||||||
|
[notificationListService, notification.id, onClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size={16}
|
||||||
|
className={styles.itemDeleteButton}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const ChangePasswordDialog = ({
|
export const ChangePasswordDialog = ({
|
||||||
close,
|
close,
|
||||||
@@ -52,10 +52,12 @@ export const ChangePasswordDialog = ({
|
|||||||
);
|
);
|
||||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||||
|
|
||||||
if (!email) {
|
useEffect(() => {
|
||||||
// should not happen
|
if (!account) {
|
||||||
throw new Unreachable();
|
// we are logged out, close the dialog
|
||||||
}
|
close();
|
||||||
|
}
|
||||||
|
}, [account, close]);
|
||||||
|
|
||||||
const onSendEmail = useAsyncCallback(async () => {
|
const onSendEmail = useAsyncCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
ExpiredPage,
|
ExpiredPage,
|
||||||
JoinFailedPage,
|
JoinFailedPage,
|
||||||
} from '@affine/component/member-components';
|
} from '@affine/component/member-components';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||||
import { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
@@ -17,6 +19,7 @@ import { AcceptInviteService, AuthService } from '../../../modules/cloud';
|
|||||||
const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
||||||
const { jumpToPage } = useNavigateHelper();
|
const { jumpToPage } = useNavigateHelper();
|
||||||
const acceptInviteService = useService(AcceptInviteService);
|
const acceptInviteService = useService(AcceptInviteService);
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
const error = useLiveData(acceptInviteService.error$);
|
const error = useLiveData(acceptInviteService.error$);
|
||||||
const inviteId = useLiveData(acceptInviteService.inviteId$);
|
const inviteId = useLiveData(acceptInviteService.inviteId$);
|
||||||
const inviteInfo = useLiveData(acceptInviteService.inviteInfo$);
|
const inviteInfo = useLiveData(acceptInviteService.inviteInfo$);
|
||||||
@@ -24,19 +27,20 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
|||||||
const loading = useLiveData(acceptInviteService.loading$);
|
const loading = useLiveData(acceptInviteService.loading$);
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
|
|
||||||
const openWorkspace = useCallback(() => {
|
const openWorkspace = useAsyncCallback(async () => {
|
||||||
if (!inviteInfo?.workspace.id) {
|
if (!inviteInfo?.workspace.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await workspacesService.list.waitForRevalidation();
|
||||||
jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE);
|
jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE);
|
||||||
}, [jumpToPage, inviteInfo]);
|
}, [inviteInfo, workspacesService, jumpToPage]);
|
||||||
|
|
||||||
const onOpenAffine = useCallback(() => {
|
const onOpenAffine = useCallback(() => {
|
||||||
navigateHelper.jumpToIndex();
|
navigateHelper.jumpToIndex();
|
||||||
}, [navigateHelper]);
|
}, [navigateHelper]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
acceptInviteService.revalidate({
|
acceptInviteService.acceptInvite({
|
||||||
inviteId: targetInviteId,
|
inviteId: targetInviteId,
|
||||||
});
|
});
|
||||||
}, [acceptInviteService, targetInviteId]);
|
}, [acceptInviteService, targetInviteId]);
|
||||||
@@ -45,10 +49,10 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => {
|
|||||||
if (error && inviteId === targetInviteId) {
|
if (error && inviteId === targetInviteId) {
|
||||||
const err = UserFriendlyError.fromAny(error);
|
const err = UserFriendlyError.fromAny(error);
|
||||||
if (err.is('ALREADY_IN_SPACE')) {
|
if (err.is('ALREADY_IN_SPACE')) {
|
||||||
return navigateHelper.jumpToIndex();
|
return openWorkspace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [error, inviteId, navigateHelper, targetInviteId]);
|
}, [error, inviteId, navigateHelper, openWorkspace, targetInviteId]);
|
||||||
|
|
||||||
if (loading || inviteId !== targetInviteId) {
|
if (loading || inviteId !== targetInviteId) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -123,17 +123,20 @@ export const Component = (): ReactElement => {
|
|||||||
// if workspace is not found, we should retry
|
// if workspace is not found, we should retry
|
||||||
const retryTimesRef = useRef(3);
|
const retryTimesRef = useRef(3);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
retryTimesRef.current = 3; // reset retry times
|
if (params.workspaceId) {
|
||||||
}, [params.workspaceId]);
|
retryTimesRef.current = 3; // reset retry times
|
||||||
|
workspacesService.list.revalidate();
|
||||||
|
}
|
||||||
|
}, [params.workspaceId, workspacesService]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (listLoading === false && meta === undefined) {
|
if (listLoading === false && meta === undefined) {
|
||||||
const timer = setInterval(() => {
|
const timer = setTimeout(() => {
|
||||||
if (retryTimesRef.current > 0) {
|
if (retryTimesRef.current > 0) {
|
||||||
workspacesService.list.revalidate();
|
workspacesService.list.revalidate();
|
||||||
retryTimesRef.current--;
|
retryTimesRef.current--;
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
return () => clearInterval(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [listLoading, meta, workspaceNotFound, workspacesService]);
|
}, [listLoading, meta, workspaceNotFound, workspacesService]);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
const workbenchService = useService(WorkbenchService);
|
||||||
|
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||||
|
const workbench = workbenchService.workbench;
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const tab = searchParams.get('tab') ?? undefined;
|
||||||
|
|
||||||
|
const isOpened = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpened.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOpened.current = true; // prevent open multiple times
|
||||||
|
workbench.openAll();
|
||||||
|
workspaceDialogService.open('setting', {
|
||||||
|
activeTab: tab as SettingTab,
|
||||||
|
});
|
||||||
|
}, [tab, workbench, workspaceDialogService]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -37,6 +37,10 @@ export const workbenchRoutes = [
|
|||||||
path: '/journals',
|
path: '/journals',
|
||||||
lazy: () => import('./pages/journals'),
|
lazy: () => import('./pages/journals'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
lazy: () => import('./pages/workspace/settings'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
lazy: () => import('./pages/404'),
|
lazy: () => import('./pages/404'),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Service,
|
Service,
|
||||||
smartRetry,
|
smartRetry,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||||
|
|
||||||
import type { AcceptInviteStore } from '../stores/accept-invite';
|
import type { AcceptInviteStore } from '../stores/accept-invite';
|
||||||
import type { InviteInfoStore } from '../stores/invite-info';
|
import type { InviteInfoStore } from '../stores/invite-info';
|
||||||
@@ -29,8 +29,8 @@ export class AcceptInviteService extends Service {
|
|||||||
loading$ = new LiveData(false);
|
loading$ = new LiveData(false);
|
||||||
error$ = new LiveData<any>(null);
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
readonly revalidate = effect(
|
readonly acceptInvite = effect(
|
||||||
exhaustMap(({ inviteId }: { inviteId: string }) => {
|
switchMap(({ inviteId }: { inviteId: string }) => {
|
||||||
if (!inviteId) {
|
if (!inviteId) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,9 @@ export class AcceptInviteService extends Service {
|
|||||||
this.accepted$.next(res);
|
this.accepted$.next(res);
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
smartRetry(),
|
smartRetry({
|
||||||
|
count: 1,
|
||||||
|
}),
|
||||||
catchErrorInto(this.error$),
|
catchErrorInto(this.error$),
|
||||||
onStart(() => {
|
onStart(() => {
|
||||||
this.inviteId$.setValue(inviteId);
|
this.inviteId$.setValue(inviteId);
|
||||||
@@ -66,7 +68,21 @@ export class AcceptInviteService extends Service {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async waitForAcceptInvite(inviteId: string) {
|
||||||
|
this.acceptInvite({ inviteId });
|
||||||
|
await this.loading$.waitFor(f => !f);
|
||||||
|
if (this.accepted$.value) {
|
||||||
|
return true; // invite is accepted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.error$.value) {
|
||||||
|
throw this.error$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // invite is expired
|
||||||
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.revalidate.unsubscribe();
|
this.acceptInvite.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"it-IT": 94,
|
"it-IT": 94,
|
||||||
"it": 1,
|
"it": 1,
|
||||||
"ja": 94,
|
"ja": 94,
|
||||||
"ko": 60,
|
"ko": 59,
|
||||||
"pl": 94,
|
"pl": 94,
|
||||||
"pt-BR": 94,
|
"pt-BR": 94,
|
||||||
"ru": 94,
|
"ru": 94,
|
||||||
|
|||||||
@@ -1114,6 +1114,10 @@ export function useAFFiNEI18N(): {
|
|||||||
["com.affine.auth.sign.auth.code.resend.hint"](options: {
|
["com.affine.auth.sign.auth.code.resend.hint"](options: {
|
||||||
readonly second: string;
|
readonly second: string;
|
||||||
}): string;
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Sent`
|
||||||
|
*/
|
||||||
|
["com.affine.auth.sent"](): string;
|
||||||
/**
|
/**
|
||||||
* `The verification link failed to be sent, please try again later.`
|
* `The verification link failed to be sent, please try again later.`
|
||||||
*/
|
*/
|
||||||
@@ -7417,6 +7421,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Transcribing`
|
* `Transcribing`
|
||||||
*/
|
*/
|
||||||
["com.affine.attachmentViewer.audio.transcribing"](): string;
|
["com.affine.attachmentViewer.audio.transcribing"](): string;
|
||||||
|
/**
|
||||||
|
* `Accept & Join`
|
||||||
|
*/
|
||||||
|
["com.affine.notification.invitation.accept"](): string;
|
||||||
/**
|
/**
|
||||||
* `An internal error occurred.`
|
* `An internal error occurred.`
|
||||||
*/
|
*/
|
||||||
@@ -8451,4 +8459,30 @@ export const TypedTrans: {
|
|||||||
}, {
|
}, {
|
||||||
a: JSX.Element;
|
a: JSX.Element;
|
||||||
}>>;
|
}>>;
|
||||||
|
/**
|
||||||
|
* `<1>{{username}}</1> has accept your invitation`
|
||||||
|
*/
|
||||||
|
["com.affine.notification.invitation-accepted"]: ComponentType<TypedTransProps<{
|
||||||
|
readonly username: string;
|
||||||
|
}, {
|
||||||
|
["1"]: JSX.Element;
|
||||||
|
}>>;
|
||||||
|
/**
|
||||||
|
* `There is an issue regarding your invitation to <1>{{workspaceName}}</1> `
|
||||||
|
*/
|
||||||
|
["com.affine.notification.invitation-blocked"]: ComponentType<TypedTransProps<{
|
||||||
|
readonly workspaceName: string;
|
||||||
|
}, {
|
||||||
|
["1"]: JSX.Element;
|
||||||
|
}>>;
|
||||||
|
/**
|
||||||
|
* `<1>{{username}}</1> invited you to join <2>{{workspaceName}}</2>`
|
||||||
|
*/
|
||||||
|
["com.affine.notification.invitation"]: ComponentType<TypedTransProps<Readonly<{
|
||||||
|
username: string;
|
||||||
|
workspaceName: string;
|
||||||
|
}>, {
|
||||||
|
["1"]: JSX.Element;
|
||||||
|
["2"]: JSX.Element;
|
||||||
|
}>>;
|
||||||
} = /*#__PURE__*/ createProxy(createComponent);
|
} = /*#__PURE__*/ createProxy(createComponent);
|
||||||
|
|||||||
@@ -270,6 +270,7 @@
|
|||||||
"com.affine.auth.sign.auth.code.continue": "Continue with code",
|
"com.affine.auth.sign.auth.code.continue": "Continue with code",
|
||||||
"com.affine.auth.sign.auth.code.resend": "Resend code",
|
"com.affine.auth.sign.auth.code.resend": "Resend code",
|
||||||
"com.affine.auth.sign.auth.code.resend.hint": "Resend in {{second}}s",
|
"com.affine.auth.sign.auth.code.resend.hint": "Resend in {{second}}s",
|
||||||
|
"com.affine.auth.sent": "Sent",
|
||||||
"com.affine.auth.sent.change.email.fail": "The verification link failed to be sent, please try again later.",
|
"com.affine.auth.sent.change.email.fail": "The verification link failed to be sent, please try again later.",
|
||||||
"com.affine.auth.sent.change.email.hint": "Verification link has been sent.",
|
"com.affine.auth.sent.change.email.hint": "Verification link has been sent.",
|
||||||
"com.affine.auth.sent.change.password.hint": "Reset password link has been sent.",
|
"com.affine.auth.sent.change.password.hint": "Reset password link has been sent.",
|
||||||
@@ -1847,6 +1848,10 @@
|
|||||||
"com.affine.integration.properties": "Integration properties",
|
"com.affine.integration.properties": "Integration properties",
|
||||||
"com.affine.attachmentViewer.audio.notes": "Notes",
|
"com.affine.attachmentViewer.audio.notes": "Notes",
|
||||||
"com.affine.attachmentViewer.audio.transcribing": "Transcribing",
|
"com.affine.attachmentViewer.audio.transcribing": "Transcribing",
|
||||||
|
"com.affine.notification.invitation-accepted": "<1>{{username}}</1> has accept your invitation",
|
||||||
|
"com.affine.notification.invitation-blocked": "There is an issue regarding your invitation to <1>{{workspaceName}}</1> ",
|
||||||
|
"com.affine.notification.invitation": "<1>{{username}}</1> invited you to join <2>{{workspaceName}}</2>",
|
||||||
|
"com.affine.notification.invitation.accept": "Accept & Join",
|
||||||
"error.INTERNAL_SERVER_ERROR": "An internal error occurred.",
|
"error.INTERNAL_SERVER_ERROR": "An internal error occurred.",
|
||||||
"error.NETWORK_ERROR": "Network error.",
|
"error.NETWORK_ERROR": "Network error.",
|
||||||
"error.TOO_MANY_REQUEST": "Too many requests.",
|
"error.TOO_MANY_REQUEST": "Too many requests.",
|
||||||
|
|||||||
Reference in New Issue
Block a user