From 120e193c58f5f99f8a0189e6500793ba4039cfdd Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 20 Mar 2025 03:52:56 +0000 Subject: [PATCH] feat(core): add notifications settings (#11004) --- .../graphql/src/graphql/get-user-settings.gql | 8 ++ packages/common/graphql/src/graphql/index.ts | 21 ++++ .../src/graphql/update-user-settings.gql | 3 + packages/common/graphql/src/schema.ts | 33 ++++++ .../dialogs/setting/general-setting/index.tsx | 19 +++- .../general-setting/notifications/index.tsx | 101 ++++++++++++++++++ .../notifications/style.css.ts | 6 ++ .../frontend/core/src/modules/cloud/index.ts | 10 +- .../modules/cloud/services/user-settings.ts | 62 +++++++++++ .../src/modules/cloud/stores/user-settings.ts | 37 +++++++ .../core/src/modules/dialogs/constant.ts | 1 + .../i18n/src/i18n-completenesses.json | 4 +- packages/frontend/i18n/src/i18n.gen.ts | 28 +++++ packages/frontend/i18n/src/resources/en.json | 7 ++ 14 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 packages/common/graphql/src/graphql/get-user-settings.gql create mode 100644 packages/common/graphql/src/graphql/update-user-settings.gql create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/index.tsx create mode 100644 packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/style.css.ts create mode 100644 packages/frontend/core/src/modules/cloud/services/user-settings.ts create mode 100644 packages/frontend/core/src/modules/cloud/stores/user-settings.ts diff --git a/packages/common/graphql/src/graphql/get-user-settings.gql b/packages/common/graphql/src/graphql/get-user-settings.gql new file mode 100644 index 0000000000..7dc59e57fa --- /dev/null +++ b/packages/common/graphql/src/graphql/get-user-settings.gql @@ -0,0 +1,8 @@ +query getUserSettings { + currentUser { + settings { + receiveInvitationEmail + receiveMentionEmail + } + } +} \ No newline at end of file diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index 5a8312bd0f..713c99b488 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -850,6 +850,19 @@ export const getUserFeaturesQuery = { }`, }; +export const getUserSettingsQuery = { + id: 'getUserSettingsQuery' as const, + op: 'getUserSettings', + query: `query getUserSettings { + currentUser { + settings { + receiveInvitationEmail + receiveMentionEmail + } + } +}`, +}; + export const getUserQuery = { id: 'getUserQuery' as const, op: 'getUser', @@ -1436,6 +1449,14 @@ export const updateUserProfileMutation = { }`, }; +export const updateUserSettingsMutation = { + id: 'updateUserSettingsMutation' as const, + op: 'updateUserSettings', + query: `mutation updateUserSettings($input: UpdateSettingsInput!) { + updateSettings(input: $input) +}`, +}; + export const uploadAvatarMutation = { id: 'uploadAvatarMutation' as const, op: 'uploadAvatar', diff --git a/packages/common/graphql/src/graphql/update-user-settings.gql b/packages/common/graphql/src/graphql/update-user-settings.gql new file mode 100644 index 0000000000..6641496feb --- /dev/null +++ b/packages/common/graphql/src/graphql/update-user-settings.gql @@ -0,0 +1,3 @@ +mutation updateUserSettings($input: UpdateSettingsInput!) { + updateSettings(input: $input) +} \ No newline at end of file diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index abc1313c59..f5b6d8d993 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -3213,6 +3213,20 @@ export type GetUserFeaturesQuery = { } | null; }; +export type GetUserSettingsQueryVariables = Exact<{ [key: string]: never }>; + +export type GetUserSettingsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + settings: { + __typename?: 'SettingsType'; + receiveInvitationEmail: boolean; + receiveMentionEmail: boolean; + }; + } | null; +}; + export type GetUserQueryVariables = Exact<{ email: Scalars['String']['input']; }>; @@ -3839,6 +3853,15 @@ export type UpdateUserProfileMutation = { updateProfile: { __typename?: 'UserType'; id: string; name: string }; }; +export type UpdateUserSettingsMutationVariables = Exact<{ + input: UpdateSettingsInput; +}>; + +export type UpdateUserSettingsMutation = { + __typename?: 'Mutation'; + updateSettings: boolean; +}; + export type UploadAvatarMutationVariables = Exact<{ avatar: Scalars['Upload']['input']; }>; @@ -4230,6 +4253,11 @@ export type Queries = variables: GetUserFeaturesQueryVariables; response: GetUserFeaturesQuery; } + | { + name: 'getUserSettingsQuery'; + variables: GetUserSettingsQueryVariables; + response: GetUserSettingsQuery; + } | { name: 'getUserQuery'; variables: GetUserQueryVariables; @@ -4647,6 +4675,11 @@ export type Mutations = variables: UpdateUserProfileMutationVariables; response: UpdateUserProfileMutation; } + | { + name: 'updateUserSettingsMutation'; + variables: UpdateUserSettingsMutationVariables; + response: UpdateUserSettingsMutation; + } | { name: 'uploadAvatarMutation'; variables: UploadAvatarMutationVariables; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/index.tsx index 0aa6e2d935..871c74b5b1 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/index.tsx @@ -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: , + 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: , 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: , @@ -130,6 +141,8 @@ export const GeneralSetting = ({ switch (activeTab) { case 'shortcuts': return ; + case 'notifications': + return ; case 'editor': return ; case 'appearance': diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/index.tsx new file mode 100644 index 0000000000..1ea805b420 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/index.tsx @@ -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 ( + <> + + + {errorMessage && ( + <> +
{errorMessage}
+
+ + )} + + handleUpdate('receiveMentionEmail', checked)} + /> + + + + handleUpdate('receiveInvitationEmail', checked) + } + /> + +
+ + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/style.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/style.css.ts new file mode 100644 index 0000000000..704e8be448 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/notifications/style.css.ts @@ -0,0 +1,6 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const errorMessage = style({ + color: cssVar('errorColor'), +}); diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 195e70ae82..fa11f67815 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -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) diff --git a/packages/frontend/core/src/modules/cloud/services/user-settings.ts b/packages/frontend/core/src/modules/cloud/services/user-settings.ts new file mode 100644 index 0000000000..15cd26b9c2 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/user-settings.ts @@ -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(undefined); + isLoading$ = new LiveData(false); + error$ = new LiveData(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(); + } +} diff --git a/packages/frontend/core/src/modules/cloud/stores/user-settings.ts b/packages/frontend/core/src/modules/cloud/stores/user-settings.ts new file mode 100644 index 0000000000..29c4ca73e3 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/stores/user-settings.ts @@ -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 { + 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, + }, + }); + } +} diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 02173d069e..96e5d35b87 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -4,6 +4,7 @@ import type { WorkspaceMetadata } from '../workspace'; export type SettingTab = | 'shortcuts' + | 'notifications' | 'appearance' | 'about' | 'plans' diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 61ca20f799..3d2b8a9925 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -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, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 1e4cae42c1..313a01a2bd 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -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` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index aa661bddaf..f96453a24c 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -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.",