mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 05:05:52 +08:00
feat(core): adjust notification style (#11296)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const containerScrollViewport = style({
|
export const containerScrollViewport = style({
|
||||||
maxHeight: '272px',
|
maxHeight: '272px',
|
||||||
@@ -14,10 +14,39 @@ export const itemList = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const listEmpty = style({
|
export const listEmpty = style({
|
||||||
color: cssVarV2('text/placeholder'),
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '2px',
|
||||||
|
height: '184px',
|
||||||
|
padding: '16px 45px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listEmptyIconContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
marginBottom: '14px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listEmptyTitle = style({
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
padding: '4px 2px',
|
textAlign: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listEmptyDescription = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const error = style({
|
export const error = style({
|
||||||
@@ -36,12 +65,37 @@ export const itemContainer = style({
|
|||||||
gap: '8px',
|
gap: '8px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`&:hover:not([data-disabled="true"])`]: {
|
[`&:hover:not([data-disabled="true"],:has(button:hover))`]: {
|
||||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const itemSkeletonContainer = style({
|
||||||
|
opacity: 0,
|
||||||
|
animation: `${keyframes({
|
||||||
|
'0%': { opacity: 0 },
|
||||||
|
'100%': { opacity: 1 },
|
||||||
|
})} 500ms ease forwards 1s`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const itemDeleteButton = style({
|
||||||
|
position: 'absolute',
|
||||||
|
right: '10px',
|
||||||
|
bottom: '8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: cssVarV2('button/iconButtonSolid'),
|
||||||
|
border: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
boxShadow: cssVar('buttonShadow'),
|
||||||
|
opacity: 0,
|
||||||
|
selectors: {
|
||||||
|
[`${itemContainer}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const itemMain = style({
|
export const itemMain = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -62,29 +116,10 @@ export const itemNotSupported = style({
|
|||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const itemDeleteButton = style({
|
|
||||||
position: 'absolute',
|
|
||||||
right: '10px',
|
|
||||||
bottom: '8px',
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
backgroundColor: cssVarV2('button/iconButtonSolid'),
|
|
||||||
border: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
|
||||||
boxShadow: cssVar('buttonShadow'),
|
|
||||||
opacity: 0,
|
|
||||||
selectors: {
|
|
||||||
[`${itemContainer}:hover &`]: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const itemNameLabel = style({
|
export const itemNameLabel = style({
|
||||||
fontWeight: 'bold',
|
fontWeight: '500',
|
||||||
color: cssVarV2('text/primary'),
|
color: cssVarV2('text/primary'),
|
||||||
display: 'inline-flex',
|
display: 'inline',
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
verticalAlign: 'top',
|
verticalAlign: 'top',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`&[data-inactived="true"]`]: {
|
[`&[data-inactived="true"]`]: {
|
||||||
@@ -95,4 +130,11 @@ export const itemNameLabel = style({
|
|||||||
|
|
||||||
export const itemActionButton = style({
|
export const itemActionButton = style({
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
|
borderRadius: '4px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const itemNameLabelIcon = style({
|
||||||
|
verticalAlign: 'top',
|
||||||
|
marginRight: '4px',
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
} from '@affine/core/modules/notification';
|
} from '@affine/core/modules/notification';
|
||||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||||
|
import { extractEmojiIcon } from '@affine/core/utils';
|
||||||
import { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import type {
|
import type {
|
||||||
InvitationAcceptedNotificationBodyType,
|
InvitationAcceptedNotificationBodyType,
|
||||||
@@ -24,15 +25,21 @@ import type {
|
|||||||
MentionNotificationBodyType,
|
MentionNotificationBodyType,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { i18nTime, Trans, useI18n } from '@affine/i18n';
|
import { i18nTime, Trans, useI18n } from '@affine/i18n';
|
||||||
import { CollaborationIcon, DeleteIcon } from '@blocksuite/icons/rc';
|
import {
|
||||||
|
CollaborationIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
EdgelessIcon,
|
||||||
|
NotificationIcon,
|
||||||
|
PageIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
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 = () => {
|
||||||
const t = useI18n();
|
|
||||||
const notificationListService = useService(NotificationListService);
|
const notificationListService = useService(NotificationListService);
|
||||||
const notifications = useLiveData(notificationListService.notifications$);
|
const notifications = useLiveData(notificationListService.notifications$);
|
||||||
const isLoading = useLiveData(notificationListService.isLoading$);
|
const isLoading = useLiveData(notificationListService.isLoading$);
|
||||||
@@ -84,9 +91,7 @@ export const NotificationList = () => {
|
|||||||
) : userFriendlyError ? (
|
) : userFriendlyError ? (
|
||||||
<div className={styles.error}>{userFriendlyError.message}</div>
|
<div className={styles.error}>{userFriendlyError.message}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.listEmpty}>
|
<NotificationListEmpty />
|
||||||
{t['com.affine.notification.empty']()}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
<Scrollable.Scrollbar />
|
<Scrollable.Scrollbar />
|
||||||
@@ -94,10 +99,31 @@ export const NotificationList = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NotificationListEmpty = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<div className={styles.listEmpty}>
|
||||||
|
<div className={styles.listEmptyIconContainer}>
|
||||||
|
<NotificationIcon width={24} height={24} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.listEmptyTitle}>
|
||||||
|
{t['com.affine.notification.empty']()}
|
||||||
|
</div>
|
||||||
|
<div className={styles.listEmptyDescription}>
|
||||||
|
{t['com.affine.notification.empty.description']()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const NotificationItemSkeleton = () => {
|
const NotificationItemSkeleton = () => {
|
||||||
return Array.from({ length: 3 }).map((_, i) => (
|
return Array.from({ length: 3 }).map((_, i) => (
|
||||||
// oxlint-disable-next-line no-array-index-key
|
<div
|
||||||
<div key={i} className={styles.itemContainer} data-disabled="true">
|
// oxlint-disable-next-line no-array-index-key
|
||||||
|
key={i}
|
||||||
|
className={clsx(styles.itemContainer, styles.itemSkeletonContainer)}
|
||||||
|
data-disabled="true"
|
||||||
|
>
|
||||||
<Skeleton variant="circular" width={22} height={22} />
|
<Skeleton variant="circular" width={22} height={22} />
|
||||||
<div className={styles.itemMain}>
|
<div className={styles.itemMain}>
|
||||||
<Skeleton variant="text" width={150} />
|
<Skeleton variant="text" width={150} />
|
||||||
@@ -183,7 +209,7 @@ const MentionNotificationItem = ({
|
|||||||
data-inactived={memberInactived}
|
data-inactived={memberInactived}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
2: <b className={styles.itemNameLabel} />,
|
2: <DocNameWithIcon mode={body.doc.mode} />,
|
||||||
}}
|
}}
|
||||||
values={{
|
values={{
|
||||||
username:
|
username:
|
||||||
@@ -419,12 +445,7 @@ const InvitationAcceptedNotificationItem = ({
|
|||||||
<Trans
|
<Trans
|
||||||
i18nKey={'com.affine.notification.invitation-accepted'}
|
i18nKey={'com.affine.notification.invitation-accepted'}
|
||||||
components={{
|
components={{
|
||||||
1: (
|
1: <WorkspaceNameWithIcon data-inactived={memberInactived} />,
|
||||||
<b
|
|
||||||
className={styles.itemNameLabel}
|
|
||||||
data-inactived={memberInactived}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
values={{
|
values={{
|
||||||
username:
|
username:
|
||||||
@@ -657,8 +678,50 @@ const WorkspaceNameWithIcon = ({
|
|||||||
}: React.PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>) => {
|
}: React.PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>) => {
|
||||||
return (
|
return (
|
||||||
<b className={styles.itemNameLabel} {...props}>
|
<b className={styles.itemNameLabel} {...props}>
|
||||||
<CollaborationIcon width={20} height={20} />
|
<CollaborationIcon
|
||||||
|
className={styles.itemNameLabelIcon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DocNameWithIcon = ({
|
||||||
|
children,
|
||||||
|
mode,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren<
|
||||||
|
React.HTMLAttributes<HTMLSpanElement> & { mode: 'page' | 'edgeless' }
|
||||||
|
>) => {
|
||||||
|
const { emoji, rest: titleWithoutEmoji } = useMemo(() => {
|
||||||
|
if (typeof children === 'string') {
|
||||||
|
return extractEmojiIcon(children);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
children instanceof Array &&
|
||||||
|
children.length === 1 &&
|
||||||
|
typeof children[0] === 'string'
|
||||||
|
) {
|
||||||
|
return extractEmojiIcon(children[0]);
|
||||||
|
}
|
||||||
|
return { rest: children, emoji: null };
|
||||||
|
}, [children]);
|
||||||
|
return (
|
||||||
|
<b className={styles.itemNameLabel} {...props}>
|
||||||
|
{emoji ? (
|
||||||
|
<span className={styles.itemNameLabelIcon}>{emoji}</span>
|
||||||
|
) : mode === 'page' ? (
|
||||||
|
<PageIcon className={styles.itemNameLabelIcon} width={20} height={20} />
|
||||||
|
) : (
|
||||||
|
<EdgelessIcon
|
||||||
|
className={styles.itemNameLabelIcon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{titleWithoutEmoji}
|
||||||
|
</b>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+1
@@ -60,6 +60,7 @@ export const NotificationSettings = () => {
|
|||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
title={t['com.affine.setting.notifications.header.title']()}
|
title={t['com.affine.setting.notifications.header.title']()}
|
||||||
|
subtitle={t['com.affine.setting.notifications.header.description']()}
|
||||||
/>
|
/>
|
||||||
<SettingWrapper
|
<SettingWrapper
|
||||||
title={t['com.affine.setting.notifications.email.title']()}
|
title={t['com.affine.setting.notifications.email.title']()}
|
||||||
|
|||||||
@@ -4663,6 +4663,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Notifications`
|
* `Notifications`
|
||||||
*/
|
*/
|
||||||
["com.affine.setting.notifications.header.title"](): string;
|
["com.affine.setting.notifications.header.title"](): string;
|
||||||
|
/**
|
||||||
|
* `Choose the types of updates you want to receive and where to get them.`
|
||||||
|
*/
|
||||||
|
["com.affine.setting.notifications.header.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Email notifications`
|
* `Email notifications`
|
||||||
*/
|
*/
|
||||||
@@ -7203,6 +7207,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `No new notifications`
|
* `No new notifications`
|
||||||
*/
|
*/
|
||||||
["com.affine.notification.empty"](): string;
|
["com.affine.notification.empty"](): string;
|
||||||
|
/**
|
||||||
|
* `You'll be notified here for @mentions and workspace invites.`
|
||||||
|
*/
|
||||||
|
["com.affine.notification.empty.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Open workspace`
|
* `Open workspace`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1159,6 +1159,7 @@
|
|||||||
"com.affine.selector-tag.search.placeholder": "Search tags...",
|
"com.affine.selector-tag.search.placeholder": "Search tags...",
|
||||||
"com.affine.setting.notifications": "Notifications",
|
"com.affine.setting.notifications": "Notifications",
|
||||||
"com.affine.setting.notifications.header.title": "Notifications",
|
"com.affine.setting.notifications.header.title": "Notifications",
|
||||||
|
"com.affine.setting.notifications.header.description": "Choose the types of updates you want to receive and where to get them.",
|
||||||
"com.affine.setting.notifications.email.title": "Email notifications",
|
"com.affine.setting.notifications.email.title": "Email notifications",
|
||||||
"com.affine.setting.notifications.email.mention.title": "Mention",
|
"com.affine.setting.notifications.email.mention.title": "Mention",
|
||||||
"com.affine.setting.notifications.email.mention.subtitle": "You will be notified through email when other members of the workspace @ you.",
|
"com.affine.setting.notifications.email.mention.subtitle": "You will be notified through email when other members of the workspace @ you.",
|
||||||
@@ -1791,6 +1792,7 @@
|
|||||||
"com.affine.notification.unsupported": "Unsupported message",
|
"com.affine.notification.unsupported": "Unsupported message",
|
||||||
"com.affine.notification.mention": "<1>{{username}}</1> mentioned you in <2>{{docTitle}}</2>",
|
"com.affine.notification.mention": "<1>{{username}}</1> mentioned you in <2>{{docTitle}}</2>",
|
||||||
"com.affine.notification.empty": "No new notifications",
|
"com.affine.notification.empty": "No new notifications",
|
||||||
|
"com.affine.notification.empty.description": "You'll be notified here for @mentions and workspace invites.",
|
||||||
"com.affine.notification.invitation-accepted": "<1>{{username}}</1> has accept your invitation",
|
"com.affine.notification.invitation-accepted": "<1>{{username}}</1> has accept your invitation",
|
||||||
"com.affine.notification.invitation-review-request": "<1>{{username}}</1> has requested to join <2>{{workspaceName}}</2>",
|
"com.affine.notification.invitation-review-request": "<1>{{username}}</1> has requested to join <2>{{workspaceName}}</2>",
|
||||||
"com.affine.notification.invitation-review-declined": "<1>{{username}}</1> has declined your request to join <2>{{workspaceName}}</2>",
|
"com.affine.notification.invitation-review-declined": "<1>{{username}}</1> has declined your request to join <2>{{workspaceName}}</2>",
|
||||||
|
|||||||
Reference in New Issue
Block a user