mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): add notifications settings (#11004)
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const errorMessage = style({
|
||||
color: cssVar('errorColor'),
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { WorkspaceMetadata } from '../workspace';
|
||||
|
||||
export type SettingTab =
|
||||
| 'shortcuts'
|
||||
| 'notifications'
|
||||
| 'appearance'
|
||||
| 'about'
|
||||
| 'plans'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
*/
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user