L-Sun
2025-04-23 05:41:14 +00:00
parent 9b2cf5cafa
commit 27ff9ab9f4
21 changed files with 304 additions and 339 deletions

View File

@@ -109,12 +109,15 @@ export function copyAsImage(std: BlockStdScope) {
notify.error({
title: I18n.t('com.affine.copy.asImage.notAvailable.title'),
message: I18n.t('com.affine.copy.asImage.notAvailable.message'),
action: {
label: I18n.t('com.affine.copy.asImage.notAvailable.action'),
onClick: () => {
window.open('https://affine.pro/download');
actions: [
{
key: 'download',
label: I18n.t('com.affine.copy.asImage.notAvailable.action'),
onClick: () => {
window.open('https://affine.pro/download');
},
},
},
],
});
return;
}

View File

@@ -1,5 +1,6 @@
import {
Input,
type Notification,
notify,
toast,
type ToastOptions,
@@ -96,17 +97,25 @@ export function patchNotificationService({
throw new Error('Invalid notification accent');
}
const toAffineNotificationActions = (
actions: (typeof notification)['actions']
): Notification['actions'] => {
if (!actions) return undefined;
return actions.map(({ label, onClick, key }) => {
return {
key,
label: toReactNode(label),
onClick,
};
});
};
const toastId = fn(
{
title: toReactNode(notification.title),
message: toReactNode(notification.message),
footer: toReactNode(notification.footer),
action: notification.action?.onClick
? {
label: toReactNode(notification.action?.label),
onClick: notification.action.onClick,
}
: undefined,
actions: toAffineNotificationActions(notification.actions),
onDismiss: notification.onClose,
},
{

View File

@@ -20,10 +20,6 @@ export const thumbContent = style({
height: 'calc(100% + 4px)',
});
export const actionButton = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
});
export const getStartedButtonText = style({
color: cssVar('textSecondaryColor'),
});

View File

@@ -1,4 +1,5 @@
import { Button, FlexWrapper, notify } from '@affine/component';
import { notify } from '@affine/component';
import { type Notification } from '@affine/component/ui/notification';
import { SubscriptionService } from '@affine/core/modules/cloud';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { EditorService } from '@affine/core/modules/editor';
@@ -63,6 +64,38 @@ export const AIOnboardingEdgeless = () => {
});
}, [workspaceDialogService]);
const actions = useMemo(() => {
const result: NonNullable<Notification['actions']> = [
{
key: 'get-started',
label: (
<span className={styles.getStartedButtonText}>
{t['com.affine.ai-onboarding.edgeless.get-started']()}
</span>
),
onClick: () => {
toggleEdgelessAIOnboarding(false);
},
},
];
if (!aiSubscription) {
result.push({
key: 'purchase',
label: (
<span className={styles.purchaseButtonText}>
{t['com.affine.ai-onboarding.edgeless.purchase']()}
</span>
),
onClick: () => {
goToPricingPlans();
toggleEdgelessAIOnboarding(false);
},
});
}
return result;
}, [aiSubscription, goToPricingPlans, t]);
useEffect(() => {
if (generalAIOnboardingOpened) return;
if (notifyId) return;
@@ -83,50 +116,13 @@ export const AIOnboardingEdgeless = () => {
thumb: <EdgelessOnboardingAnimation />,
alignMessage: 'icon',
onDismiss: () => toggleEdgelessAIOnboarding(false),
footer: (
<FlexWrapper marginTop={8} justifyContent="flex-end" gap="12px">
<Button
onClick={() => {
notify.dismiss(id);
toggleEdgelessAIOnboarding(false);
}}
variant="plain"
className={styles.actionButton}
>
<span className={styles.getStartedButtonText}>
{t['com.affine.ai-onboarding.edgeless.get-started']()}
</span>
</Button>
{aiSubscription ? null : (
<Button
className={styles.actionButton}
variant="plain"
onClick={() => {
goToPricingPlans();
notify.dismiss(id);
toggleEdgelessAIOnboarding(false);
}}
>
<span className={styles.purchaseButtonText}>
{t['com.affine.ai-onboarding.edgeless.purchase']()}
</span>
</Button>
)}
</FlexWrapper>
),
actions,
},
{ duration: 1000 * 60 * 10 }
);
edgelessNotifyId$.next(id);
}, 1000);
}, [
aiSubscription,
generalAIOnboardingOpened,
goToPricingPlans,
mode,
notifyId,
t,
]);
}, [actions, generalAIOnboardingOpened, mode, notifyId, t]);
return null;
};

View File

@@ -1,4 +1,4 @@
import { Button, notify } from '@affine/component';
import { type Notification, notify } from '@affine/component';
import {
RouteLogic,
useNavigateHelper,
@@ -8,7 +8,7 @@ import { useI18n } from '@affine/i18n';
import { AiIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { toggleLocalAIOnboarding } from './apis';
import * as styles from './local.dialog.css';
@@ -29,51 +29,41 @@ const LocalOnboardingAnimation = () => {
);
};
const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
const t = useI18n();
const authService = useService(AuthService);
const loginStatus = useLiveData(authService.session.status$);
const loggedIn = loginStatus === 'authenticated';
const { jumpToSignIn } = useNavigateHelper();
return (
<div className={styles.footerActions}>
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
<Button
className={styles.actionButton}
variant="plain"
onClick={onDismiss}
>
{t['com.affine.ai-onboarding.local.action-learn-more']()}
</Button>
</a>
{loggedIn ? null : (
<Button
className={styles.actionButton}
variant="plain"
onClick={() => {
onDismiss();
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });
}}
>
{t['com.affine.ai-onboarding.local.action-get-started']()}
</Button>
)}
</div>
);
};
export const AIOnboardingLocal = () => {
const t = useI18n();
const authService = useService(AuthService);
const notifyId = useLiveData(localNotifyId$);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const { jumpToSignIn } = useNavigateHelper();
const loginStatus = useLiveData(authService.session.status$);
const notSignedIn = loginStatus !== 'authenticated';
const actions = useMemo(() => {
const result: NonNullable<Notification['actions']> = [
{
key: 'learn-more',
label: t['com.affine.ai-onboarding.local.action-learn-more'](),
onClick: () => {
window.open('https://ai.affine.pro', '_blank', 'noreferrer');
},
},
];
if (notSignedIn) {
result.push({
key: 'get-started',
label: t['com.affine.ai-onboarding.local.action-get-started'](),
onClick: () => {
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });
},
});
}
return result;
}, [t, jumpToSignIn, notSignedIn]);
useEffect(() => {
if (!notSignedIn) return;
// if (!notSignedIn) return;
if (notifyId) return;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
@@ -95,21 +85,14 @@ export const AIOnboardingLocal = () => {
thumb: <LocalOnboardingAnimation />,
alignMessage: 'icon',
onDismiss: () => toggleLocalAIOnboarding(false),
footer: (
<FooterActions
onDismiss={() => {
toggleLocalAIOnboarding(false);
notify.dismiss(id);
}}
/>
),
actions,
rootAttrs: { className: styles.card },
},
{ duration: 1000 * 60 * 10 }
);
localNotifyId$.next(id);
}, 1000);
}, [notSignedIn, notifyId, t]);
}, [actions, notSignedIn, notifyId, t]);
return null;
};

View File

@@ -6,13 +6,6 @@ export const notifyHeader = style({
fontSize: 15,
});
export const notifyFooter = style({
display: 'flex',
justifyContent: 'end',
gap: 12,
paddingTop: 8,
});
export const actionButton = style({
fontSize: cssVar('fontSm'),
fontWeight: 500,

View File

@@ -1,4 +1,4 @@
import { Button, notify } from '@affine/component';
import { type Notification, notify } from '@affine/component';
import { useI18n } from '@affine/i18n';
import clsx from 'clsx';
import { useCallback, useRef } from 'react';
@@ -7,48 +7,9 @@ import {
actionButton,
cancelButton,
confirmButton,
notifyFooter,
notifyHeader,
} from './notify.css';
interface SubscriptionChangedNotifyFooterProps {
onCancel: () => void;
onConfirm?: () => void;
to: string;
okText: string;
cancelText: string;
}
const SubscriptionChangedNotifyFooter = ({
to,
okText,
cancelText,
onCancel,
onConfirm,
}: SubscriptionChangedNotifyFooterProps) => {
return (
<div className={notifyFooter}>
<Button
className={clsx(actionButton, cancelButton)}
size={'default'}
onClick={onCancel}
variant="plain"
>
{cancelText}
</Button>
<a href={to} target="_blank" rel="noreferrer">
<Button
onClick={onConfirm}
className={clsx(actionButton, confirmButton)}
variant="plain"
>
{okText}
</Button>
</a>
</div>
);
};
export const useDowngradeNotify = () => {
const t = useI18n();
const prevNotifyIdRef = useRef<string | number | null>(null);
@@ -56,6 +17,30 @@ export const useDowngradeNotify = () => {
return useCallback(
(link: string) => {
prevNotifyIdRef.current && notify.dismiss(prevNotifyIdRef.current);
const actions: Notification['actions'] = [
{
key: 'later',
label: t['com.affine.payment.downgraded-notify.later'](),
onClick: () => {},
buttonProps: {
className: clsx(actionButton, cancelButton),
},
},
{
key: 'ok',
label: BUILD_CONFIG.isElectron
? t['com.affine.payment.downgraded-notify.ok-client']()
: t['com.affine.payment.downgraded-notify.ok-web'](),
onClick: () => {
window.open(link, '_blank', 'noreferrer');
},
buttonProps: {
className: clsx(actionButton, confirmButton),
},
},
];
const id = notify(
{
title: (
@@ -66,19 +51,7 @@ export const useDowngradeNotify = () => {
message: t['com.affine.payment.downgraded-notify.content'](),
alignMessage: 'title',
icon: null,
footer: (
<SubscriptionChangedNotifyFooter
to={link}
okText={
BUILD_CONFIG.isElectron
? t['com.affine.payment.downgraded-notify.ok-client']()
: t['com.affine.payment.downgraded-notify.ok-web']()
}
cancelText={t['com.affine.payment.downgraded-notify.later']()}
onCancel={() => notify.dismiss(id)}
onConfirm={() => notify.dismiss(id)}
/>
),
actions,
},
{ duration: 24 * 60 * 60 * 1000 }
);

View File

@@ -43,10 +43,13 @@ export const OverCapacityNotification = () => {
title: t['com.affine.payment.storage-limit.new-title'](),
message:
t['com.affine.payment.storage-limit.new-description.owner'](),
action: {
label: t['com.affine.payment.upgrade'](),
onClick: jumpToPricePlan,
},
actions: [
{
key: 'upgrade',
label: t['com.affine.payment.upgrade'](),
onClick: jumpToPricePlan,
},
],
});
} else {
notify.warning({

View File

@@ -87,14 +87,17 @@ const BackupWorkspaceItem = ({ item }: { item: BackupWorkspaceItem }) => {
}
notify.success({
title: t['com.affine.settings.workspace.backup.import.success'](),
action: {
label:
t['com.affine.settings.workspace.backup.import.success.action'](),
onClick: () => {
jumpToPage(workspaceId, 'all');
actions: [
{
key: 'open',
label:
t['com.affine.settings.workspace.backup.import.success.action'](),
onClick: () => {
jumpToPage(workspaceId, 'all');
},
autoClose: false,
},
autoClose: false,
},
],
});
setMenuOpen(false);
setImporting(false);

View File

@@ -459,45 +459,49 @@ export class AtMenuConfigService extends Service {
]({
username,
}),
action: {
label: 'Invite',
onClick: async () => {
track.$.sharePanel.$.inviteUserDocRole({
control: 'member list',
role: 'reader',
});
try {
await this.docGrantedUsersService.updateUserRole(
id,
DocRole.Reader
);
await notificationService.mentionUser(
id,
workspaceId,
{
id: docId,
title:
this.docDisplayMetaService.title$(docId).value,
blockId: block.blockId,
mode: mode as GraphqlDocMode,
}
);
notify.success({
title: I18n.t(
'com.affine.editor.at-menu.invited-and-notified'
),
actions: [
{
key: 'invite',
label: 'Invite',
onClick: async () => {
track.$.sharePanel.$.inviteUserDocRole({
control: 'member list',
role: 'reader',
});
} catch (error) {
const err = UserFriendlyError.fromAny(error);
notify.error({
title: I18n[`error.${err.name}`](err.data),
});
}
try {
await this.docGrantedUsersService.updateUserRole(
id,
DocRole.Reader
);
await notificationService.mentionUser(
id,
workspaceId,
{
id: docId,
title:
this.docDisplayMetaService.title$(docId)
.value,
blockId: block.blockId,
mode: mode as GraphqlDocMode,
}
);
notify.success({
title: I18n.t(
'com.affine.editor.at-menu.invited-and-notified'
),
});
} catch (error) {
const err = UserFriendlyError.fromAny(error);
notify.error({
title: I18n[`error.${err.name}`](err.data),
});
}
},
},
},
],
});
} else {
notify.error({