feat(core): add notifications settings (#11004)

This commit is contained in:
EYHN
2025-03-20 03:52:56 +00:00
parent 46ed76ecb0
commit 120e193c58
14 changed files with 334 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ import {
FolderIcon,
InformationIcon,
KeyboardIcon,
NotificationIcon,
PenIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useServices } from '@toeverything/infra';
@@ -22,6 +23,7 @@ import { BillingSettings } from './billing';
import { EditorSettings } from './editor';
import { ExperimentalFeatures } from './experimental-features';
import { PaymentIcon, UpgradeIcon } from './icons';
import { NotificationSettings } from './notifications';
import { AFFiNEPricingPlans } from './plans';
import { Shortcuts } from './shortcuts';
@@ -37,6 +39,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
FeatureFlagService,
});
const status = useLiveData(authService.session.status$);
const loggedIn = status === 'authenticated';
const hasPaymentFeature = useLiveData(
serverService.server.features$.map(f => f?.payment)
);
@@ -62,6 +65,14 @@ export const useGeneralSettingList = (): GeneralSettingList => {
testId: 'shortcuts-panel-trigger',
},
];
if (loggedIn) {
settings.push({
key: 'notifications',
title: t['com.affine.setting.notifications'](),
icon: <NotificationIcon />,
testId: 'notifications-panel-trigger',
});
}
if (enableEditorSettings) {
// add editor settings to second position
settings.splice(1, 0, {
@@ -73,14 +84,14 @@ export const useGeneralSettingList = (): GeneralSettingList => {
}
if (hasPaymentFeature) {
settings.splice(3, 0, {
settings.splice(4, 0, {
key: 'plans',
title: t['com.affine.payment.title'](),
icon: <UpgradeIcon />,
testId: 'plans-panel-trigger',
});
if (status === 'authenticated') {
settings.splice(3, 0, {
if (loggedIn) {
settings.splice(4, 0, {
key: 'billing',
title: t['com.affine.payment.billing-setting.title'](),
icon: <PaymentIcon />,
@@ -130,6 +141,8 @@ export const GeneralSetting = ({
switch (activeTab) {
case 'shortcuts':
return <Shortcuts />;
case 'notifications':
return <NotificationSettings />;
case 'editor':
return <EditorSettings />;
case 'appearance':

View File

@@ -0,0 +1,101 @@
import { notify, Switch } from '@affine/component';
import {
SettingHeader,
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import {
type UserSettings,
UserSettingsService,
} from '@affine/core/modules/cloud';
import { UserFriendlyError } from '@affine/error';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useState } from 'react';
import * as styles from './style.css';
export const NotificationSettings = () => {
const t = useI18n();
const userSettingsService = useService(UserSettingsService);
useEffect(() => {
userSettingsService.revalidate();
}, [userSettingsService]);
const userSettings = useLiveData(userSettingsService.userSettings$);
const [isMutating, setIsMutating] = useState(false);
const error = useLiveData(userSettingsService.error$);
const errorMessage = useMemo(() => {
if (error) {
console.log('error', error);
const userFriendlyError = UserFriendlyError.fromAny(error);
return t[`error.${userFriendlyError.name}`](userFriendlyError.data);
}
return null;
}, [error, t]);
const disable = !userSettings || isMutating;
const handleUpdate = useCallback(
(key: keyof UserSettings, value: boolean) => {
setIsMutating(true);
userSettingsService
.updateUserSettings({
[key]: value,
})
.catch(err => {
const userFriendlyError = UserFriendlyError.fromAny(err);
notify.error({
title: t[`error.${userFriendlyError.name}`](userFriendlyError.data),
});
})
.finally(() => {
setIsMutating(false);
});
},
[userSettingsService, t]
);
return (
<>
<SettingHeader
title={t['com.affine.setting.notifications.header.title']()}
/>
<SettingWrapper
title={t['com.affine.setting.notifications.email.title']()}
>
{errorMessage && (
<>
<div className={styles.errorMessage}>{errorMessage}</div>
<br />
</>
)}
<SettingRow
name={t['com.affine.setting.notifications.email.mention.title']()}
desc={t['com.affine.setting.notifications.email.mention.subtitle']()}
>
<Switch
data-testid="notification-email-mention-trigger"
checked={userSettings?.receiveMentionEmail ?? false}
disabled={disable}
onChange={checked => handleUpdate('receiveMentionEmail', checked)}
/>
</SettingRow>
<SettingRow
name={t['com.affine.setting.notifications.email.invites.title']()}
desc={t['com.affine.setting.notifications.email.invites.subtitle']()}
>
<Switch
data-testid="notification-email-invites-trigger"
checked={userSettings?.receiveInvitationEmail ?? false}
disabled={disable}
onChange={checked =>
handleUpdate('receiveInvitationEmail', checked)
}
/>
</SettingRow>
</SettingWrapper>
</>
);
};

View File

@@ -0,0 +1,6 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const errorMessage = style({
color: cssVar('errorColor'),
});

View File

@@ -24,6 +24,10 @@ export { SubscriptionService } from './services/subscription';
export { UserCopilotQuotaService } from './services/user-copilot-quota';
export { UserFeatureService } from './services/user-feature';
export { UserQuotaService } from './services/user-quota';
export {
type UserSettings,
UserSettingsService,
} from './services/user-settings';
export { WorkspaceInvoicesService } from './services/workspace-invoices';
export { WorkspaceServerService } from './services/workspace-server';
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
@@ -71,6 +75,7 @@ import { SubscriptionService } from './services/subscription';
import { UserCopilotQuotaService } from './services/user-copilot-quota';
import { UserFeatureService } from './services/user-feature';
import { UserQuotaService } from './services/user-quota';
import { UserSettingsService } from './services/user-settings';
import { WorkspaceInvoicesService } from './services/workspace-invoices';
import { WorkspaceServerService } from './services/workspace-server';
import { WorkspaceSubscriptionService } from './services/workspace-subscription';
@@ -88,6 +93,7 @@ import { SubscriptionStore } from './stores/subscription';
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
import { UserFeatureStore } from './stores/user-feature';
import { UserQuotaStore } from './stores/user-quota';
import { UserSettingsStore } from './stores/user-settings';
export function configureCloudModule(framework: Framework) {
configureDefaultAuthProvider(framework);
@@ -150,7 +156,9 @@ export function configureCloudModule(framework: Framework) {
.service(AcceptInviteService, [AcceptInviteStore, InviteInfoStore])
.store(AcceptInviteStore, [GraphQLService])
.service(PublicUserService, [PublicUserStore])
.store(PublicUserStore, [GraphQLService]);
.store(PublicUserStore, [GraphQLService])
.service(UserSettingsService, [UserSettingsStore])
.store(UserSettingsStore, [GraphQLService]);
framework
.scope(WorkspaceScope)

View File

@@ -0,0 +1,62 @@
import {
effect,
exhaustMapWithTrailing,
fromPromise,
LiveData,
onComplete,
onStart,
Service,
smartRetry,
} from '@toeverything/infra';
import { catchError, EMPTY, mergeMap } from 'rxjs';
import type {
UpdateUserSettingsInput,
UserSettings,
UserSettingsStore,
} from '../stores/user-settings';
export type { UserSettings };
export class UserSettingsService extends Service {
constructor(private readonly store: UserSettingsStore) {
super();
}
userSettings$ = new LiveData<UserSettings | undefined>(undefined);
isLoading$ = new LiveData<boolean>(false);
error$ = new LiveData<any | undefined>(undefined);
revalidate = effect(
exhaustMapWithTrailing(() => {
return fromPromise(() => {
return this.store.getUserSettings();
}).pipe(
smartRetry(),
mergeMap(settings => {
this.userSettings$.value = settings;
return EMPTY;
}),
catchError(error => {
this.error$.value = error;
return EMPTY;
}),
onStart(() => {
this.isLoading$.value = true;
}),
onComplete(() => {
this.isLoading$.value = false;
})
);
})
);
async updateUserSettings(settings: UpdateUserSettingsInput) {
await this.store.updateUserSettings(settings);
this.userSettings$.value = {
...this.userSettings$.value,
...(settings as UserSettings),
};
this.revalidate();
}
}

View File

@@ -0,0 +1,37 @@
import {
type GetUserSettingsQuery,
getUserSettingsQuery,
type UpdateSettingsInput,
updateUserSettingsMutation,
} from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../services/graphql';
export type UserSettings = NonNullable<
GetUserSettingsQuery['currentUser']
>['settings'];
export type UpdateUserSettingsInput = UpdateSettingsInput;
export class UserSettingsStore extends Store {
constructor(private readonly gqlService: GraphQLService) {
super();
}
async getUserSettings(): Promise<UserSettings | undefined> {
const result = await this.gqlService.gql({
query: getUserSettingsQuery,
});
return result.currentUser?.settings;
}
async updateUserSettings(settings: UpdateUserSettingsInput) {
await this.gqlService.gql({
query: updateUserSettingsMutation,
variables: {
input: settings,
},
});
}
}

View File

@@ -4,6 +4,7 @@ import type { WorkspaceMetadata } from '../workspace';
export type SettingTab =
| 'shortcuts'
| 'notifications'
| 'appearance'
| 'about'
| 'plans'

View File

@@ -6,7 +6,7 @@
"el-GR": 96,
"en": 100,
"es-AR": 96,
"es-CL": 98,
"es-CL": 97,
"es": 96,
"fa": 96,
"fr": 96,
@@ -14,7 +14,7 @@
"it-IT": 96,
"it": 1,
"ja": 96,
"ko": 61,
"ko": 60,
"pl": 96,
"pt-BR": 96,
"ru": 96,

View File

@@ -4651,6 +4651,34 @@ export function useAFFiNEI18N(): {
* `Search tags...`
*/
["com.affine.selector-tag.search.placeholder"](): string;
/**
* `Notifications`
*/
["com.affine.setting.notifications"](): string;
/**
* `Notifications`
*/
["com.affine.setting.notifications.header.title"](): string;
/**
* `Email notifications`
*/
["com.affine.setting.notifications.email.title"](): string;
/**
* `Mention`
*/
["com.affine.setting.notifications.email.mention.title"](): string;
/**
* `You will be notified through email when other members of the workspace @ you.`
*/
["com.affine.setting.notifications.email.mention.subtitle"](): string;
/**
* `Invites`
*/
["com.affine.setting.notifications.email.invites.title"](): string;
/**
* `Invitation related messages will be sent through emails.`
*/
["com.affine.setting.notifications.email.invites.subtitle"](): string;
/**
* `Account settings`
*/

View File

@@ -1156,6 +1156,13 @@
"com.affine.selectPage.title": "Add include doc",
"com.affine.selector-collection.search.placeholder": "Search collections...",
"com.affine.selector-tag.search.placeholder": "Search tags...",
"com.affine.setting.notifications": "Notifications",
"com.affine.setting.notifications.header.title": "Notifications",
"com.affine.setting.notifications.email.title": "Email notifications",
"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.invites.title": "Invites",
"com.affine.setting.notifications.email.invites.subtitle": "Invitation related messages will be sent through emails.",
"com.affine.setting.account": "Account settings",
"com.affine.setting.account.delete": "Delete account",
"com.affine.setting.account.delete.message": "Permanently delete this account and the Workspace data backup in AFFiNE Cloud. This action can not be undone.",