Pārlūkot izejas kodu

feat: toggle notifications on/off (#3672)

Mathias Mogensen 1 gadu atpakaļ
vecāks
revīzija
ebe112581d
34 mainītis faili ar 315 papildinājumiem un 76 dzēšanām
  1. 1 1
      frontend/appflowy_flutter/integration_test/document/document_text_direction_test.dart
  2. 1 1
      frontend/appflowy_flutter/integration_test/util/settings.dart
  3. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  4. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart
  5. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart
  6. 12 3
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  7. 19 12
      frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart
  8. 18 10
      frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart
  9. 16 0
      frontend/appflowy_flutter/lib/user/application/user_settings_service.dart
  10. 1 1
      frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart
  11. 1 1
      frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart
  12. 1 1
      frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart
  13. 39 30
      frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart
  14. 62 0
      frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart
  15. 1 0
      frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart
  16. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart
  17. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart
  18. 3 0
      frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart
  19. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/brightness_setting.dart
  20. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart
  21. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart
  22. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart
  23. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart
  24. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart
  25. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart
  26. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart
  27. 7 0
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart
  28. 45 0
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart
  29. 2 1
      frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart
  30. 1 1
      frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart
  31. 7 0
      frontend/resources/translations/en.json
  32. 14 0
      frontend/rust-lib/flowy-user/src/entities/user_setting.rs
  33. 37 1
      frontend/rust-lib/flowy-user/src/event_handler.rs
  34. 14 0
      frontend/rust-lib/flowy-user/src/event_map.rs

+ 1 - 1
frontend/appflowy_flutter/integration_test/document/document_text_direction_test.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';

+ 1 - 1
frontend/appflowy_flutter/integration_test/util/settings.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/settings/prelude.dart';
 import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -7,7 +7,7 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
 import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
 import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
 import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
 import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';

+ 12 - 3
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -24,6 +24,7 @@ import 'package:appflowy/user/presentation/router.dart';
 import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
 import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
 import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
+import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
 import 'package:appflowy/workspace/application/settings/prelude.dart';
 import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
 import 'package:appflowy/workspace/application/user/prelude.dart';
@@ -168,14 +169,22 @@ void _resolveHomeDeps(GetIt getIt) {
 
   getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
 
-  getIt.registerSingleton<ReminderBloc>(ReminderBloc());
+  getIt.registerSingleton<NotificationSettingsCubit>(
+    NotificationSettingsCubit(),
+  );
+
+  getIt.registerSingleton<ReminderBloc>(
+    ReminderBloc(notificationSettings: getIt<NotificationSettingsCubit>()),
+  );
 }
 
 void _resolveFolderDeps(GetIt getIt) {
   //workspace
   getIt.registerFactoryParam<WorkspaceListener, UserProfilePB, String>(
-    (user, workspaceId) =>
-        WorkspaceListener(user: user, workspaceId: workspaceId),
+    (user, workspaceId) => WorkspaceListener(
+      user: user,
+      workspaceId: workspaceId,
+    ),
   );
 
   getIt.registerFactoryParam<ViewBloc, ViewPB, void>(

+ 19 - 12
frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart

@@ -1,19 +1,23 @@
-import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
-import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
-import 'package:appflowy/startup/tasks/prelude.dart';
+import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
+
+import 'prelude.dart';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:go_router/go_router.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:go_router/go_router.dart';
 
-import '../../user/application/user_settings_service.dart';
-import '../../workspace/application/appearance.dart';
-import '../startup.dart';
+import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
+import 'package:appflowy/user/application/user_settings_service.dart';
+import 'package:appflowy/startup/startup.dart';
 
 class InitAppWidgetTask extends LaunchTask {
   const InitAppWidgetTask();
@@ -95,8 +99,8 @@ class ApplicationWidget extends StatefulWidget {
   });
 
   final Widget child;
-  final AppearanceSettingsPB appearanceSetting;
   final AppTheme appTheme;
+  final AppearanceSettingsPB appearanceSetting;
   final DateTimeSettingsPB dateTimeSettings;
 
   @override
@@ -125,6 +129,9 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
             widget.appTheme,
           )..readLocaleWhenAppLaunch(context),
         ),
+        BlocProvider<NotificationSettingsCubit>(
+          create: (_) => getIt<NotificationSettingsCubit>(),
+        ),
         BlocProvider<DocumentAppearanceCubit>(
           create: (_) => DocumentAppearanceCubit()..fetch(),
         ),

+ 18 - 10
frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart

@@ -6,6 +6,7 @@ import 'package:appflowy/user/application/reminder/reminder_service.dart';
 import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
 import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
 import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
+import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
 import 'package:bloc/bloc.dart';
@@ -18,11 +19,16 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 part 'reminder_bloc.freezed.dart';
 
 class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
+  final NotificationSettingsCubit _notificationSettings;
+
   late final NotificationActionBloc actionBloc;
   late final ReminderService reminderService;
   late final Timer timer;
 
-  ReminderBloc() : super(ReminderState()) {
+  ReminderBloc({
+    required NotificationSettingsCubit notificationSettings,
+  })  : _notificationSettings = notificationSettings,
+        super(ReminderState()) {
     actionBloc = getIt<NotificationActionBloc>();
     reminderService = const ReminderService();
     timer = _periodicCheck();
@@ -124,16 +130,18 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
           );
 
           if (scheduledAt.isBefore(now)) {
-            NotificationMessage(
-              identifier: reminder.id,
-              title: LocaleKeys.reminderNotification_title.tr(),
-              body: LocaleKeys.reminderNotification_message.tr(),
-              onClick: () => actionBloc.add(
-                NotificationActionEvent.performAction(
-                  action: NotificationAction(objectId: reminder.objectId),
+            if (_notificationSettings.state.isNotificationsEnabled) {
+              NotificationMessage(
+                identifier: reminder.id,
+                title: LocaleKeys.reminderNotification_title.tr(),
+                body: LocaleKeys.reminderNotification_message.tr(),
+                onClick: () => actionBloc.add(
+                  NotificationActionEvent.performAction(
+                    action: NotificationAction(objectId: reminder.objectId),
+                  ),
                 ),
-              ),
-            );
+              );
+            }
 
             add(
               ReminderEvent.update(

+ 16 - 0
frontend/appflowy_flutter/lib/user/application/user_settings_service.dart

@@ -41,4 +41,20 @@ class UserSettingsBackendService {
   ) async {
     return (await UserEventSetDateTimeSettings(settings).send()).swap();
   }
+
+  Future<Either<FlowyError, Unit>> setNotificationSettings(
+    NotificationSettingsPB settings,
+  ) async {
+    return (await UserEventSetNotificationSettings(settings).send()).swap();
+  }
+
+  Future<NotificationSettingsPB> getNotificationSettings() async {
+    final result = await UserEventGetNotificationSettings().send();
+
+    return result.fold(
+      (NotificationSettingsPB setting) => setting,
+      (error) =>
+          throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty),
+    );
+  }
 }

+ 1 - 1
frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart

@@ -6,7 +6,7 @@ import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/user/application/sign_in_bloc.dart';
 import 'package:appflowy/user/presentation/presentation.dart';
 import 'package:appflowy/util/platform_extension.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart

@@ -8,7 +8,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/user/application/historical_user_bloc.dart';
 import 'package:appflowy/user/presentation/router.dart';
 import 'package:appflowy/user/presentation/widgets/widgets.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy/user/application/user_listener.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/edit_panel/edit_context.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'
     show WorkspaceSettingPB;

+ 39 - 30
frontend/appflowy_flutter/lib/workspace/application/appearance.dart → frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart

@@ -16,33 +16,40 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:google_fonts/google_fonts.dart';
 
-part 'appearance.freezed.dart';
+part 'appearance_cubit.freezed.dart';
 
 const _white = Color(0xFFFFFFFF);
 
 /// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.
-/// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale].
+/// It includes:
+/// - [AppTheme]
+/// - [ThemeMode]
+/// - [TextStyle]'s
+/// - [Locale]
+/// - [UserDateFormatPB]
+/// - [UserTimeFormatPB]
+///
 class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
-  final AppearanceSettingsPB _setting;
+  final AppearanceSettingsPB _appearanceSettings;
   final DateTimeSettingsPB _dateTimeSettings;
 
   AppearanceSettingsCubit(
-    AppearanceSettingsPB setting,
+    AppearanceSettingsPB appearanceSettings,
     DateTimeSettingsPB dateTimeSettings,
     AppTheme appTheme,
-  )   : _setting = setting,
+  )   : _appearanceSettings = appearanceSettings,
         _dateTimeSettings = dateTimeSettings,
         super(
           AppearanceSettingsState.initial(
             appTheme,
-            setting.themeMode,
-            setting.font,
-            setting.monospaceFont,
-            setting.layoutDirection,
-            setting.textDirection,
-            setting.locale,
-            setting.isMenuCollapsed,
-            setting.menuOffset,
+            appearanceSettings.themeMode,
+            appearanceSettings.font,
+            appearanceSettings.monospaceFont,
+            appearanceSettings.layoutDirection,
+            appearanceSettings.textDirection,
+            appearanceSettings.locale,
+            appearanceSettings.isMenuCollapsed,
+            appearanceSettings.menuOffset,
             dateTimeSettings.dateFormat,
             dateTimeSettings.timeFormat,
             dateTimeSettings.timezoneId,
@@ -52,7 +59,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
   /// Update selected theme in the user's settings and emit an updated state
   /// with the AppTheme named [themeName].
   Future<void> setTheme(String themeName) async {
-    _setting.theme = themeName;
+    _appearanceSettings.theme = themeName;
     _saveAppearanceSettings();
     emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
   }
@@ -63,7 +70,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
 
   /// Update the theme mode in the user's settings and emit an updated state.
   void setThemeMode(ThemeMode themeMode) {
-    _setting.themeMode = _themeModeToPB(themeMode);
+    _appearanceSettings.themeMode = _themeModeToPB(themeMode);
     _saveAppearanceSettings();
     emit(state.copyWith(themeMode: themeMode));
   }
@@ -81,13 +88,13 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
   }
 
   void setLayoutDirection(LayoutDirection layoutDirection) {
-    _setting.layoutDirection = layoutDirection.toLayoutDirectionPB();
+    _appearanceSettings.layoutDirection = layoutDirection.toLayoutDirectionPB();
     _saveAppearanceSettings();
     emit(state.copyWith(layoutDirection: layoutDirection));
   }
 
   void setTextDirection(AppFlowyTextDirection? textDirection) {
-    _setting.textDirection =
+    _appearanceSettings.textDirection =
         textDirection?.toTextDirectionPB() ?? TextDirectionPB.FALLBACK;
     _saveAppearanceSettings();
     emit(state.copyWith(textDirection: textDirection));
@@ -96,7 +103,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
   /// Update selected font in the user's settings and emit an updated state
   /// with the font name.
   void setFontFamily(String fontFamilyName) {
-    _setting.font = fontFamilyName;
+    _appearanceSettings.font = fontFamilyName;
     _saveAppearanceSettings();
     emit(state.copyWith(font: fontFamilyName));
   }
@@ -118,8 +125,8 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
     });
 
     if (state.locale != newLocale) {
-      _setting.locale.languageCode = newLocale.languageCode;
-      _setting.locale.countryCode = newLocale.countryCode ?? "";
+      _appearanceSettings.locale.languageCode = newLocale.languageCode;
+      _appearanceSettings.locale.countryCode = newLocale.countryCode ?? "";
       _saveAppearanceSettings();
       emit(state.copyWith(locale: newLocale));
     }
@@ -127,13 +134,13 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
 
   // Saves the menus current visibility
   void saveIsMenuCollapsed(bool collapsed) {
-    _setting.isMenuCollapsed = collapsed;
+    _appearanceSettings.isMenuCollapsed = collapsed;
     _saveAppearanceSettings();
   }
 
   // Saves the current resize offset of the menu
   void saveMenuOffset(double offset) {
-    _setting.menuOffset = offset;
+    _appearanceSettings.menuOffset = offset;
     _saveAppearanceSettings();
   }
 
@@ -146,14 +153,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
     }
 
     if (value == null) {
-      _setting.settingKeyValue.remove(key);
+      _appearanceSettings.settingKeyValue.remove(key);
     }
 
-    if (_setting.settingKeyValue[key] != value) {
+    if (_appearanceSettings.settingKeyValue[key] != value) {
       if (value == null) {
-        _setting.settingKeyValue.remove(key);
+        _appearanceSettings.settingKeyValue.remove(key);
       } else {
-        _setting.settingKeyValue[key] = value;
+        _appearanceSettings.settingKeyValue[key] = value;
       }
     }
     _saveAppearanceSettings();
@@ -164,14 +171,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
       Log.warn("The key should not be empty");
       return null;
     }
-    return _setting.settingKeyValue[key];
+    return _appearanceSettings.settingKeyValue[key];
   }
 
   /// Called when the application launches.
   /// Uses the device locale when the application is opened for the first time.
   void readLocaleWhenAppLaunch(BuildContext context) {
-    if (_setting.resetToDefault) {
-      _setting.resetToDefault = false;
+    if (_appearanceSettings.resetToDefault) {
+      _appearanceSettings.resetToDefault = false;
       _saveAppearanceSettings();
       setLocale(context, context.deviceLocale);
       return;
@@ -204,7 +211,9 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
   }
 
   Future<void> _saveAppearanceSettings() async {
-    UserSettingsBackendService().setAppearanceSetting(_setting).then((result) {
+    UserSettingsBackendService()
+        .setAppearanceSetting(_appearanceSettings)
+        .then((result) {
       result.fold(
         (l) => null,
         (error) => Log.error(error),

+ 62 - 0
frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart

@@ -0,0 +1,62 @@
+import 'dart:async';
+
+import 'package:appflowy/user/application/user_settings_service.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'notification_settings_cubit.freezed.dart';
+
+class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
+  final Completer<void> _initCompleter = Completer();
+
+  late final NotificationSettingsPB _notificationSettings;
+
+  NotificationSettingsCubit() : super(NotificationSettingsState.initial()) {
+    UserSettingsBackendService()
+        .getNotificationSettings()
+        .then((notificationSettings) {
+      _notificationSettings = notificationSettings;
+      _initCompleter.complete();
+    });
+  }
+
+  Future<void> toggleNotificationsEnabled() async {
+    await _initCompleter.future;
+
+    _notificationSettings.notificationsEnabled = !state.isNotificationsEnabled;
+    _saveNotificationSettings();
+
+    emit(
+      state.copyWith(
+        isNotificationsEnabled: _notificationSettings.notificationsEnabled,
+      ),
+    );
+  }
+
+  Future<void> _saveNotificationSettings() async {
+    await _initCompleter.future;
+
+    UserSettingsBackendService()
+        .setNotificationSettings(_notificationSettings)
+        .then((result) {
+      result.fold(
+        (error) => Log.error(error),
+        (r) => null,
+      );
+    });
+  }
+}
+
+@freezed
+class NotificationSettingsState with _$NotificationSettingsState {
+  const NotificationSettingsState._();
+
+  const factory NotificationSettingsState({
+    required bool isNotificationsEnabled,
+  }) = _NotificationSettingsState;
+
+  factory NotificationSettingsState.initial() =>
+      const NotificationSettingsState(isNotificationsEnabled: true);
+}

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart

@@ -13,6 +13,7 @@ enum SettingsPage {
   language,
   files,
   user,
+  notifications,
   syncSetting,
   shortcuts,
 }

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart

@@ -3,7 +3,7 @@ import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/home/home_bloc.dart';
 import 'package:appflowy/workspace/application/home/home_service.dart';
 import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart

@@ -1,5 +1,5 @@
 import 'dart:io';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
 import 'package:flutter/material.dart';

+ 3 - 0
frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart

@@ -1,5 +1,6 @@
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
@@ -101,6 +102,8 @@ class SettingsDialog extends StatelessWidget {
           didLogout: didLogout,
           didOpenUser: didOpenUser,
         );
+      case SettingsPage.notifications:
+        return const SettingsNotificationsView();
       case SettingsPage.syncSetting:
         return SyncSettingView(userId: user.id.toString());
       case SettingsPage.shortcuts:

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/brightness_setting.dart

@@ -2,7 +2,7 @@ import 'dart:io';
 
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/presentation/home/toast.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart

@@ -1,7 +1,7 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy/util/google_font_family_extension.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/application/appearance_defaults.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:collection/collection.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 7 - 0
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart

@@ -61,6 +61,13 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.account_box_outlined,
           changeSelectedPage: changeSelectedPage,
         ),
+        SettingsMenuElement(
+          page: SettingsPage.notifications,
+          selectedPage: currentPage,
+          label: LocaleKeys.settings_menu_notifications.tr(),
+          icon: Icons.notifications_outlined,
+          changeSelectedPage: changeSelectedPage,
+        ),
         if (showSyncSetting)
           const SizedBox(
             height: 10,

+ 45 - 0
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart

@@ -0,0 +1,45 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class SettingsNotificationsView extends StatelessWidget {
+  const SettingsNotificationsView({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
+      builder: (context, state) {
+        return SingleChildScrollView(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              ThemeSettingEntryTemplateWidget(
+                label: LocaleKeys
+                    .settings_notifications_enableNotifications_label
+                    .tr(),
+                hint: LocaleKeys.settings_notifications_enableNotifications_hint
+                    .tr(),
+                trailing: [
+                  Switch(
+                    value: state.isNotificationsEnabled,
+                    splashRadius: 0,
+                    activeColor: Theme.of(context).colorScheme.primary,
+                    onChanged: (value) {
+                      context
+                          .read<NotificationSettingsCubit>()
+                          .toggleNotificationsEnabled();
+                    },
+                  )
+                ],
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}

+ 2 - 1
frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy/user/application/user_settings_service.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:bloc_test/bloc_test.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
 import 'package:flowy_infra/theme.dart';
@@ -18,6 +18,7 @@ void main() {
   group('$AppearanceSettingsCubit', () {
     late AppearanceSettingsPB appearanceSetting;
     late DateTimeSettingsPB dateTimeSettings;
+
     setUp(() async {
       appearanceSetting =
           await UserSettingsBackendService().getAppearanceSetting();

+ 1 - 1
frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
-import 'package:appflowy/workspace/application/appearance.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';

+ 7 - 0
frontend/resources/translations/en.json

@@ -241,6 +241,7 @@
       "language": "Language",
       "user": "User",
       "files": "Files",
+      "notifications": "Notifications",
       "open": "Open Settings",
       "logout": "Logout",
       "logoutPrompt": "Are you sure to logout?",
@@ -256,6 +257,12 @@
       "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button",
       "openHistoricalUser": "Click to open the anonymous account"
     },
+    "notifications": {
+      "enableNotifications": {
+        "label": "Enable notifications",
+        "hint": "Turn off to stop local notifications from appearing."
+      }
+    },
     "appearance": {
       "resetSetting": "Reset this setting",
       "fontFamily": {

+ 14 - 0
frontend/rust-lib/flowy-user/src/entities/user_setting.rs

@@ -237,3 +237,17 @@ impl std::default::Default for DateTimeSettingsPB {
     }
   }
 }
+
+#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)]
+pub struct NotificationSettingsPB {
+  #[pb(index = 1)]
+  pub notifications_enabled: bool,
+}
+
+impl std::default::Default for NotificationSettingsPB {
+  fn default() -> Self {
+    NotificationSettingsPB {
+      notifications_enabled: true,
+    }
+  }
+}

+ 37 - 1
frontend/rust-lib/flowy-user/src/event_handler.rs

@@ -195,7 +195,7 @@ pub async fn get_date_time_settings(
         Ok(setting) => setting,
         Err(e) => {
           tracing::error!(
-            "Deserialize AppearanceSettings failed: {:?}, fallback to default",
+            "Deserialize DateTimeSettings failed: {:?}, fallback to default",
             e
           );
           DateTimeSettingsPB::default()
@@ -206,6 +206,42 @@ pub async fn get_date_time_settings(
   }
 }
 
+const NOTIFICATION_SETTINGS_CACHE_KEY: &str = "notification_settings";
+
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub async fn set_notification_settings(
+  store_preferences: AFPluginState<Weak<StorePreferences>>,
+  data: AFPluginData<NotificationSettingsPB>,
+) -> Result<(), FlowyError> {
+  let store_preferences = upgrade_store_preferences(store_preferences)?;
+  let setting = data.into_inner();
+  store_preferences.set_object(NOTIFICATION_SETTINGS_CACHE_KEY, setting)?;
+  Ok(())
+}
+
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub async fn get_notification_settings(
+  store_preferences: AFPluginState<Weak<StorePreferences>>,
+) -> DataResult<NotificationSettingsPB, FlowyError> {
+  let store_preferences = upgrade_store_preferences(store_preferences)?;
+  match store_preferences.get_str(NOTIFICATION_SETTINGS_CACHE_KEY) {
+    None => data_result_ok(NotificationSettingsPB::default()),
+    Some(s) => {
+      let setting = match serde_json::from_str(&s) {
+        Ok(setting) => setting,
+        Err(e) => {
+          tracing::error!(
+            "Deserialize NotificationSettings failed: {:?}, fallback to default",
+            e
+          );
+          NotificationSettingsPB::default()
+        },
+      };
+      data_result_ok(setting)
+    },
+  }
+}
+
 #[tracing::instrument(level = "debug", skip_all, err)]
 pub async fn get_user_setting(
   manager: AFPluginState<Weak<UserManager>>,

+ 14 - 0
frontend/rust-lib/flowy-user/src/event_map.rs

@@ -65,6 +65,14 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin {
     .event(UserEvent::ResetWorkspace, reset_workspace_handler)
     .event(UserEvent::SetDateTimeSettings, set_date_time_settings)
     .event(UserEvent::GetDateTimeSettings, get_date_time_settings)
+    .event(
+      UserEvent::SetNotificationSettings,
+      set_notification_settings,
+    )
+    .event(
+      UserEvent::GetNotificationSettings,
+      get_notification_settings,
+    )
 }
 
 pub struct SignUpContext {
@@ -317,4 +325,10 @@ pub enum UserEvent {
   /// Retrieve the Date/Time formats
   #[event(output = "DateTimeSettingsPB")]
   GetDateTimeSettings = 34,
+
+  #[event(input = "NotificationSettingsPB")]
+  SetNotificationSettings = 35,
+
+  #[event(output = "NotificationSettingsPB")]
+  GetNotificationSettings = 36,
 }