Browse Source

fix: reminder launch review (#3716)

Mathias Mogensen 1 year ago
parent
commit
966547faa0
29 changed files with 519 additions and 566 deletions
  1. 8 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart
  2. 12 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart
  3. 4 1
      frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart
  4. 1 1
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  5. 1 1
      frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart
  6. 2 29
      frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart
  7. 33 4
      frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart
  8. 17 0
      frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart
  9. 0 0
      frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart
  10. 1 1
      frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart
  11. 0 0
      frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart
  12. 2 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart
  13. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart
  14. 154 287
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart
  15. 0 58
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_group.dart
  16. 0 65
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_grouped_view.dart
  17. 0 20
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/notifications_hub_empty.dart
  18. 3 1
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart
  19. 23 0
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart
  20. 56 54
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart
  21. 78 0
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart
  22. 15 4
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart
  23. 32 0
      frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart
  24. 8 17
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart
  25. 12 8
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart
  26. 41 3
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart
  27. 5 3
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart
  28. 0 1
      frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart
  29. 10 1
      frontend/resources/translations/en.json

+ 8 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart

@@ -366,6 +366,9 @@ class EndTimeButton extends StatelessWidget {
   }
 }
 
+const _maxLengthTwelveHour = 8;
+const _maxLengthTwentyFourHour = 5;
+
 class _TimeTextField extends StatefulWidget {
   final bool isEndTime;
   final String? timeStr;
@@ -433,6 +436,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
             errorText: widget.isEndTime
                 ? state.parseEndTimeError
                 : state.parseTimeError,
+            maxLength:
+                state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwelveHour
+                    ? _maxLengthTwelveHour
+                    : _maxLengthTwentyFourHour,
+            showCounter: false,
             onSubmitted: (timeStr) {
               if (widget.isEndTime) {
                 context

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

@@ -82,11 +82,13 @@ class MentionDateBlock extends StatelessWidget {
                 _updateBlock(parsedDate!.withoutTime, includeTime);
 
                 // We can remove time from the date/reminder
-                //  block when toggled off.
-                if (!includeTime && isReminder) {
+                // block when toggled off.
+                if (isReminder) {
                   _updateScheduledAt(
                     reminderId: reminderId!,
-                    selectedDay: parsedDate!.withoutTime,
+                    selectedDay:
+                        includeTime ? parsedDate! : parsedDate!.withoutTime,
+                    includeTime: includeTime,
                   );
                 }
               },
@@ -99,6 +101,7 @@ class MentionDateBlock extends StatelessWidget {
                   _updateScheduledAt(
                     reminderId: reminderId!,
                     selectedDay: selectedDay,
+                    includeTime: includeTime,
                   );
                 }
               },
@@ -171,10 +174,15 @@ class MentionDateBlock extends StatelessWidget {
   void _updateScheduledAt({
     required String reminderId,
     required DateTime selectedDay,
+    bool? includeTime,
   }) {
     editorContext.read<ReminderBloc>().add(
           ReminderEvent.update(
-            ReminderUpdate(id: reminderId, scheduledAt: selectedDay),
+            ReminderUpdate(
+              id: reminderId,
+              scheduledAt: selectedDay,
+              includeTime: includeTime,
+            ),
           ),
         );
   }

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart

@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
 import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -209,7 +210,9 @@ class ReminderReferenceService {
       objectId: viewId,
       title: LocaleKeys.reminderNotification_title.tr(),
       message: LocaleKeys.reminderNotification_message.tr(),
-      meta: {"document_id": viewId},
+      meta: {
+        ReminderMetaKeys.includeTime.name: false.toString(),
+      },
       scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
       isAck: date.isBefore(DateTime.now()),
     );

+ 1 - 1
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -23,7 +23,7 @@ import 'package:appflowy/user/application/user_service.dart';
 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/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';

+ 1 - 1
frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart

@@ -14,7 +14,7 @@ import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.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/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';

+ 2 - 29
frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart

@@ -1,5 +1,4 @@
 import 'package:bloc/bloc.dart';
-import 'package:equatable/equatable.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 
 part 'notification_filter_bloc.freezed.dart';
@@ -10,12 +9,6 @@ class NotificationFilterBloc
     on<NotificationFilterEvent>((event, emit) async {
       event.when(
         reset: () => emit(const NotificationFilterState()),
-        changeSortBy: (NotificationSortOption sortBy) => emit(
-          state.copyWith(sortBy: sortBy),
-        ),
-        toggleGroupByDate: () => emit(
-          state.copyWith(groupByDate: !state.groupByDate),
-        ),
         toggleShowUnreadsOnly: () => emit(
           state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),
         ),
@@ -24,42 +17,22 @@ class NotificationFilterBloc
   }
 }
 
-enum NotificationSortOption {
-  descending,
-  ascending,
-}
-
 @freezed
 class NotificationFilterEvent with _$NotificationFilterEvent {
   const factory NotificationFilterEvent.toggleShowUnreadsOnly() =
       _ToggleShowUnreadsOnly;
 
-  const factory NotificationFilterEvent.toggleGroupByDate() =
-      _ToggleGroupByDate;
-
-  const factory NotificationFilterEvent.changeSortBy(
-    NotificationSortOption sortBy,
-  ) = _ChangeSortBy;
-
   const factory NotificationFilterEvent.reset() = _Reset;
 }
 
 @freezed
-class NotificationFilterState extends Equatable with _$NotificationFilterState {
+class NotificationFilterState with _$NotificationFilterState {
   const NotificationFilterState._();
 
   const factory NotificationFilterState({
     @Default(false) bool showUnreadsOnly,
-    @Default(false) bool groupByDate,
-    @Default(NotificationSortOption.descending) NotificationSortOption sortBy,
   }) = _NotificationFilterState;
 
   // If state is not default values, then there are custom changes
-  bool get hasFilters =>
-      showUnreadsOnly != false ||
-      groupByDate != false ||
-      sortBy != NotificationSortOption.descending;
-
-  @override
-  List<Object?> get props => [showUnreadsOnly, groupByDate, sortBy];
+  bool get hasFilters => showUnreadsOnly != false;
 }

+ 33 - 4
frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart

@@ -2,10 +2,11 @@ import 'dart:async';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
 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/notifications/notification_action.dart';
+import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
+import 'package:appflowy/workspace/application/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';
@@ -35,6 +36,24 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
 
     on<ReminderEvent>((event, emit) async {
       await event.when(
+        markAllRead: () async {
+          final unreadReminders =
+              state.pastReminders.where((reminder) => !reminder.isRead);
+
+          final reminders = [...state.reminders];
+          final updatedReminders = <ReminderPB>[];
+          for (final reminder in unreadReminders) {
+            reminders.remove(reminder);
+
+            reminder.isRead = true;
+            await reminderService.updateReminder(reminder: reminder);
+
+            updatedReminders.add(reminder);
+          }
+
+          reminders.addAll(updatedReminders);
+          emit(state.copyWith(reminders: reminders));
+        },
         started: () async {
           final remindersOrFailure = await reminderService.fetchReminders();
 
@@ -169,6 +188,9 @@ class ReminderEvent with _$ReminderEvent {
   // Update a reminder (eg. isAck, isRead, etc.)
   const factory ReminderEvent.update(ReminderUpdate update) = _Update;
 
+  // Mark all unread reminders as read
+  const factory ReminderEvent.markAllRead() = _MarkAllRead;
+
   const factory ReminderEvent.pressReminder({required String reminderId}) =
       _PressReminder;
 }
@@ -181,12 +203,14 @@ class ReminderUpdate {
   final bool? isAck;
   final bool? isRead;
   final DateTime? scheduledAt;
+  final bool? includeTime;
 
   ReminderUpdate({
     required this.id,
     this.isAck,
     this.isRead,
     this.scheduledAt,
+    this.includeTime,
   });
 
   ReminderPB merge({required ReminderPB a}) {
@@ -194,6 +218,11 @@ class ReminderUpdate {
         ? scheduledAt!.isBefore(DateTime.now())
         : a.isAck;
 
+    final meta = a.meta;
+    if (includeTime != a.includeTime) {
+      meta[ReminderMetaKeys.includeTime.name] = includeTime.toString();
+    }
+
     return ReminderPB(
       id: a.id,
       objectId: a.objectId,
@@ -204,7 +233,7 @@ class ReminderUpdate {
       isRead: isRead ?? a.isRead,
       title: a.title,
       message: a.message,
-      meta: a.meta,
+      meta: meta,
     );
   }
 }

+ 17 - 0
frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart

@@ -0,0 +1,17 @@
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+
+enum ReminderMetaKeys {
+  includeTime("include_time");
+
+  const ReminderMetaKeys(this.name);
+
+  final String name;
+}
+
+extension ReminderExtension on ReminderPB {
+  bool? get includeTime {
+    final String? includeTimeStr = meta[ReminderMetaKeys.includeTime.name];
+
+    return includeTimeStr != null ? includeTimeStr == true.toString() : null;
+  }
+}

+ 0 - 0
frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action.dart → frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart


+ 1 - 1
frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action_bloc.dart → frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
+import 'package:appflowy/workspace/application/notifications/notification_action.dart';
 import 'package:bloc/bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 

+ 0 - 0
frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_service.dart → frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart


+ 2 - 2
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart

@@ -1,7 +1,7 @@
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/workspace/application/favorite/favorite_bloc.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/notifications/notification_action.dart';
+import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
 import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
 import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
 import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
-import 'package:appflowy/workspace/presentation/notifications/notification_button.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
 import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';

+ 154 - 287
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart

@@ -3,30 +3,24 @@ import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
-import 'package:appflowy/workspace/presentation/notifications/notification_grouped_view.dart';
-import 'package:appflowy/workspace/presentation/notifications/notification_view.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:calendar_view/calendar_view.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 extension _ReminderSort on Iterable<ReminderPB> {
-  List<ReminderPB> sortByScheduledAt({
-    bool isDescending = true,
-  }) =>
-      sorted(
-        (a, b) => isDescending
-            ? b.scheduledAt.compareTo(a.scheduledAt)
-            : a.scheduledAt.compareTo(b.scheduledAt),
-      );
+  List<ReminderPB> sortByScheduledAt() =>
+      sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
 }
 
 class NotificationDialog extends StatefulWidget {
@@ -78,112 +72,44 @@ class _NotificationDialogState extends State<NotificationDialog>
         builder: (context, filterState) =>
             BlocBuilder<ReminderBloc, ReminderState>(
           builder: (context, state) {
-            final sortDescending =
-                filterState.sortBy == NotificationSortOption.descending;
-
             final List<ReminderPB> pastReminders = state.pastReminders
                 .where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
-                .sortByScheduledAt(isDescending: sortDescending);
+                .sortByScheduledAt();
 
-            final List<ReminderPB> upcomingReminders = state.upcomingReminders
-                .sortByScheduledAt(isDescending: sortDescending);
+            final List<ReminderPB> upcomingReminders =
+                state.upcomingReminders.sortByScheduledAt();
 
             return Column(
               mainAxisSize: MainAxisSize.min,
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
-                Row(
-                  children: [
-                    DecoratedBox(
-                      decoration: BoxDecoration(
-                        border: Border(
-                          bottom: BorderSide(
-                            color: Theme.of(context).dividerColor,
-                          ),
-                        ),
-                      ),
-                      child: SizedBox(
-                        width: 215,
-                        child: TabBar(
-                          controller: _controller,
-                          indicator: UnderlineTabIndicator(
-                            borderRadius: BorderRadius.circular(4),
-                            borderSide: BorderSide(
-                              width: 1,
-                              color: Theme.of(context).colorScheme.primary,
-                            ),
-                          ),
-                          tabs: [
-                            Tab(
-                              height: 26,
-                              child: FlowyText.regular(
-                                LocaleKeys.notificationHub_tabs_inbox.tr(),
-                              ),
-                            ),
-                            Tab(
-                              height: 26,
-                              child: FlowyText.regular(
-                                LocaleKeys.notificationHub_tabs_upcoming.tr(),
-                              ),
-                            ),
-                          ],
-                        ),
-                      ),
-                    ),
-                    const Spacer(),
-                    NotificationViewFilters(),
-                  ],
-                ),
-                const VSpace(4),
+                const NotificationHubTitle(),
+                NotificationTabBar(tabController: _controller),
                 // TODO(Xazin): Resolve issue with taking up
                 //  max amount of vertical space
                 Expanded(
                   child: TabBarView(
                     controller: _controller,
                     children: [
-                      if (!filterState.groupByDate) ...[
-                        NotificationsView(
-                          shownReminders: pastReminders,
-                          reminderBloc: _reminderBloc,
-                          views: widget.views,
-                          onDelete: _onDelete,
-                          onAction: _onAction,
-                          onReadChanged: _onReadChanged,
-                        ),
-                        NotificationsView(
-                          shownReminders: upcomingReminders,
-                          reminderBloc: _reminderBloc,
-                          views: widget.views,
-                          isUpcoming: true,
-                          onAction: _onAction,
-                        ),
-                      ] else ...[
-                        NotificationsGroupView(
-                          groupedReminders: groupBy<ReminderPB, DateTime>(
-                            pastReminders,
-                            (r) => DateTime.fromMillisecondsSinceEpoch(
-                              r.scheduledAt.toInt() * 1000,
-                            ).withoutTime,
-                          ),
-                          reminderBloc: _reminderBloc,
-                          views: widget.views,
-                          onAction: _onAction,
-                          onDelete: _onDelete,
-                          onReadChanged: _onReadChanged,
+                      NotificationsView(
+                        shownReminders: pastReminders,
+                        reminderBloc: _reminderBloc,
+                        views: widget.views,
+                        onDelete: _onDelete,
+                        onAction: _onAction,
+                        onReadChanged: _onReadChanged,
+                        actionBar: _InboxActionBar(
+                          hasUnreads: state.hasUnreads,
+                          showUnreadsOnly: filterState.showUnreadsOnly,
                         ),
-                        NotificationsGroupView(
-                          groupedReminders: groupBy<ReminderPB, DateTime>(
-                            upcomingReminders,
-                            (r) => DateTime.fromMillisecondsSinceEpoch(
-                              r.scheduledAt.toInt() * 1000,
-                            ).withoutTime,
-                          ),
-                          reminderBloc: _reminderBloc,
-                          views: widget.views,
-                          isUpcoming: true,
-                          onAction: _onAction,
-                        ),
-                      ],
+                      ),
+                      NotificationsView(
+                        shownReminders: upcomingReminders,
+                        reminderBloc: _reminderBloc,
+                        views: widget.views,
+                        isUpcoming: true,
+                        onAction: _onAction,
+                      ),
                     ],
                   ),
                 ),
@@ -222,222 +148,163 @@ class _NotificationDialogState extends State<NotificationDialog>
   }
 }
 
-class NotificationViewFilters extends StatelessWidget {
-  NotificationViewFilters({super.key});
-  final PopoverMutex _mutex = PopoverMutex();
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider<NotificationFilterBloc>.value(
-      value: context.read<NotificationFilterBloc>(),
-      child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
-        builder: (context, state) {
-          return AppFlowyPopover(
-            mutex: _mutex,
-            offset: const Offset(0, 5),
-            constraints: BoxConstraints.loose(const Size(225, 200)),
-            direction: PopoverDirection.bottomWithLeftAligned,
-            popupBuilder: (popoverContext) {
-              // TODO(Xazin): This is a workaround until we have resolved
-              //  the issues with closing popovers on leave/outside-clicks
-              return MouseRegion(
-                onExit: (_) => _mutex.close(),
-                child: NotificationFilterPopover(
-                  bloc: context.read<NotificationFilterBloc>(),
-                ),
-              );
-            },
-            child: FlowyIconButton(
-              isSelected: state.hasFilters,
-              iconColorOnHover: Theme.of(context).colorScheme.onSurface,
-              icon: const FlowySvg(FlowySvgs.filter_s),
-            ),
-          );
-        },
-      ),
-    );
-  }
-}
-
-class NotificationFilterPopover extends StatelessWidget {
-  const NotificationFilterPopover({
-    super.key,
-    required this.bloc,
+class _InboxActionBar extends StatelessWidget {
+  const _InboxActionBar({
+    required this.hasUnreads,
+    required this.showUnreadsOnly,
   });
 
-  final NotificationFilterBloc bloc;
+  final bool hasUnreads;
+  final bool showUnreadsOnly;
 
   @override
   Widget build(BuildContext context) {
-    return Column(
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        _SortByOption(bloc: bloc),
-        _ShowUnreadsToggle(bloc: bloc),
-        _GroupByDateToggle(bloc: bloc),
-        BlocProvider<NotificationFilterBloc>.value(
-          value: bloc,
-          child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
-            builder: (context, state) {
-              return Row(
-                mainAxisAlignment: MainAxisAlignment.end,
-                children: [
-                  SizedBox(
-                    width: 115,
-                    child: FlowyButton(
-                      disable: !state.hasFilters,
-                      onTap: state.hasFilters
-                          ? () =>
-                              bloc.add(const NotificationFilterEvent.reset())
-                          : null,
-                      text: FlowyText(
-                        LocaleKeys.notificationHub_filters_resetToDefault.tr(),
-                      ),
-                    ),
-                  ),
-                ],
-              );
-            },
+    return DecoratedBox(
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: BorderSide(
+            color: Theme.of(context).dividerColor,
           ),
         ),
-      ],
+      ),
+      child: Padding(
+        padding: const EdgeInsets.symmetric(
+          horizontal: 16,
+          vertical: 8,
+        ),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            _MarkAsReadButton(
+              onMarkAllRead: !hasUnreads
+                  ? null
+                  : () => context
+                      .read<ReminderBloc>()
+                      .add(const ReminderEvent.markAllRead()),
+            ),
+            _ToggleUnreadsButton(
+              showUnreadsOnly: showUnreadsOnly,
+              onToggled: (showUnreadsOnly) => context
+                  .read<NotificationFilterBloc>()
+                  .add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
+            ),
+          ],
+        ),
+      ),
     );
   }
 }
 
-class _ShowUnreadsToggle extends StatelessWidget {
-  const _ShowUnreadsToggle({required this.bloc});
+class _ToggleUnreadsButton extends StatefulWidget {
+  const _ToggleUnreadsButton({
+    required this.onToggled,
+    this.showUnreadsOnly = false,
+  });
 
-  final NotificationFilterBloc bloc;
+  final Function(bool) onToggled;
+  final bool showUnreadsOnly;
 
   @override
-  Widget build(BuildContext context) {
-    return BlocProvider<NotificationFilterBloc>.value(
-      value: bloc,
-      child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
-        builder: (context, state) {
-          return Row(
-            children: [
-              const HSpace(4),
-              Expanded(
-                child: FlowyText(
-                  LocaleKeys.notificationHub_filters_showUnreadsOnly.tr(),
-                ),
-              ),
-              Toggle(
-                style: ToggleStyle.big,
-                onChanged: (value) => bloc
-                    .add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
-                value: state.showUnreadsOnly,
-              ),
-            ],
-          );
-        },
-      ),
-    );
-  }
+  State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
 }
 
-class _GroupByDateToggle extends StatelessWidget {
-  const _GroupByDateToggle({required this.bloc});
-
-  final NotificationFilterBloc bloc;
+class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
+  late bool showUnreadsOnly = widget.showUnreadsOnly;
 
   @override
   Widget build(BuildContext context) {
-    return BlocProvider<NotificationFilterBloc>.value(
-      value: bloc,
-      child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
-        builder: (context, state) {
-          return Row(
-            children: [
-              const HSpace(4),
-              Expanded(
-                child: FlowyText(
-                  LocaleKeys.notificationHub_filters_groupByDate.tr(),
-                ),
-              ),
-              Toggle(
-                style: ToggleStyle.big,
-                onChanged: (value) =>
-                    bloc.add(const NotificationFilterEvent.toggleGroupByDate()),
-                value: state.groupByDate,
-              ),
-            ],
-          );
-        },
+    return SegmentedButton<bool>(
+      onSelectionChanged: (Set<bool> newSelection) {
+        setState(() => showUnreadsOnly = newSelection.first);
+        widget.onToggled(showUnreadsOnly);
+      },
+      showSelectedIcon: false,
+      style: ButtonStyle(
+        side: MaterialStatePropertyAll(
+          BorderSide(color: Theme.of(context).dividerColor),
+        ),
+        shape: const MaterialStatePropertyAll(
+          RoundedRectangleBorder(borderRadius: Corners.s6Border),
+        ),
+        foregroundColor: MaterialStateProperty.resolveWith<Color>(
+          (state) {
+            if (state.contains(MaterialState.hovered) ||
+                state.contains(MaterialState.selected) ||
+                state.contains(MaterialState.pressed)) {
+              return Theme.of(context).colorScheme.onSurface;
+            }
+
+            return AFThemeExtension.of(context).textColor;
+          },
+        ),
+        backgroundColor: MaterialStateProperty.resolveWith<Color>(
+          (state) {
+            if (state.contains(MaterialState.hovered) ||
+                state.contains(MaterialState.selected) ||
+                state.contains(MaterialState.pressed)) {
+              return Theme.of(context).colorScheme.primary;
+            }
+
+            return Theme.of(context).cardColor;
+          },
+        ),
       ),
+      segments: [
+        ButtonSegment<bool>(
+          value: false,
+          label: Text(
+            LocaleKeys.notificationHub_actions_showAll.tr(),
+            style: const TextStyle(fontSize: 12),
+          ),
+        ),
+        ButtonSegment<bool>(
+          value: true,
+          label: Text(
+            LocaleKeys.notificationHub_actions_showUnreads.tr(),
+            style: const TextStyle(fontSize: 12),
+          ),
+        ),
+      ],
+      selected: <bool>{showUnreadsOnly},
     );
   }
 }
 
-class _SortByOption extends StatefulWidget {
-  const _SortByOption({required this.bloc});
+class _MarkAsReadButton extends StatefulWidget {
+  final VoidCallback? onMarkAllRead;
 
-  final NotificationFilterBloc bloc;
+  const _MarkAsReadButton({this.onMarkAllRead});
 
   @override
-  State<_SortByOption> createState() => _SortByOptionState();
+  State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
 }
 
-class _SortByOptionState extends State<_SortByOption> {
+class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
   bool _isHovering = false;
 
   @override
   Widget build(BuildContext context) {
-    return BlocProvider<NotificationFilterBloc>.value(
-      value: widget.bloc,
-      child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
-        builder: (context, state) {
-          final isSortDescending =
-              state.sortBy == NotificationSortOption.descending;
-
-          return Row(
-            children: [
-              const Expanded(
-                child: Padding(
-                  padding: EdgeInsets.only(left: 4.0),
-                  child: FlowyText('Sort'),
-                ),
-              ),
-              const Spacer(),
-              SizedBox(
-                width: 115,
-                child: FlowyHover(
-                  resetHoverOnRebuild: false,
-                  child: FlowyButton(
-                    onHover: (isHovering) => isHovering != _isHovering
-                        ? setState(() => _isHovering = isHovering)
-                        : null,
-                    onTap: () => widget.bloc.add(
-                      NotificationFilterEvent.changeSortBy(
-                        isSortDescending
-                            ? NotificationSortOption.ascending
-                            : NotificationSortOption.descending,
-                      ),
-                    ),
-                    leftIcon: FlowySvg(
-                      isSortDescending
-                          ? FlowySvgs.sort_descending_s
-                          : FlowySvgs.sort_ascending_s,
-                      color: _isHovering
-                          ? Theme.of(context).colorScheme.onSurface
-                          : Theme.of(context).iconTheme.color,
-                    ),
-                    text: FlowyText.regular(
-                      isSortDescending
-                          ? LocaleKeys.notificationHub_filters_descending.tr()
-                          : LocaleKeys.notificationHub_filters_ascending.tr(),
-                      color: _isHovering
-                          ? Theme.of(context).colorScheme.onSurface
-                          : Theme.of(context).textTheme.bodyMedium?.color,
-                    ),
-                  ),
-                ),
-              ),
-            ],
-          );
-        },
+    return Opacity(
+      opacity: widget.onMarkAllRead != null ? 1 : 0.5,
+      child: FlowyHover(
+        onHover: (isHovering) => setState(() => _isHovering = isHovering),
+        resetHoverOnRebuild: false,
+        child: FlowyTextButton(
+          LocaleKeys.notificationHub_actions_markAllRead.tr(),
+          fontColor: widget.onMarkAllRead != null && _isHovering
+              ? Theme.of(context).colorScheme.onSurface
+              : AFThemeExtension.of(context).textColor,
+          heading: FlowySvg(
+            FlowySvgs.checklist_s,
+            color: widget.onMarkAllRead != null && _isHovering
+                ? Theme.of(context).colorScheme.onSurface
+                : AFThemeExtension.of(context).textColor,
+          ),
+          hoverColor: widget.onMarkAllRead != null && _isHovering
+              ? Theme.of(context).colorScheme.primary
+              : null,
+          onPressed: widget.onMarkAllRead,
+        ),
       ),
     );
   }

+ 0 - 58
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_group.dart

@@ -1,58 +0,0 @@
-import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter/material.dart';
-
-class NotificationGroup extends StatelessWidget {
-  const NotificationGroup({
-    super.key,
-    required this.reminders,
-    required this.formattedDate,
-    required this.isUpcoming,
-    required this.onReadChanged,
-    required this.onDelete,
-    required this.onAction,
-  });
-
-  final List<ReminderPB> reminders;
-  final String formattedDate;
-  final bool isUpcoming;
-  final Function(ReminderPB reminder, bool isRead)? onReadChanged;
-  final Function(ReminderPB reminder)? onDelete;
-  final Function(ReminderPB reminder)? onAction;
-
-  @override
-  Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.only(bottom: 8),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          Padding(
-            padding: const EdgeInsets.only(left: 8),
-            child: FlowyText(formattedDate),
-          ),
-          const VSpace(4),
-          ...reminders
-              .map(
-                (reminder) => NotificationItem(
-                  reminderId: reminder.id,
-                  key: ValueKey(reminder.id),
-                  title: reminder.title,
-                  scheduled: reminder.scheduledAt,
-                  body: reminder.message,
-                  isRead: reminder.isRead,
-                  readOnly: isUpcoming,
-                  onReadChanged: (isRead) =>
-                      onReadChanged?.call(reminder, isRead),
-                  onDelete: () => onDelete?.call(reminder),
-                  onAction: () => onAction?.call(reminder),
-                ),
-              )
-              .toList(),
-        ],
-      ),
-    );
-  }
-}

+ 0 - 65
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_grouped_view.dart

@@ -1,65 +0,0 @@
-import 'package:appflowy/user/application/reminder/reminder_bloc.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/notifications/notification_group.dart';
-import 'package:appflowy/workspace/presentation/notifications/notifications_hub_empty.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
-import 'package:collection/collection.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-class NotificationsGroupView extends StatelessWidget {
-  const NotificationsGroupView({
-    super.key,
-    required this.groupedReminders,
-    required this.reminderBloc,
-    required this.views,
-    this.isUpcoming = false,
-    this.onAction,
-    this.onDelete,
-    this.onReadChanged,
-  });
-
-  final Map<DateTime, List<ReminderPB>> groupedReminders;
-  final ReminderBloc reminderBloc;
-  final List<ViewPB> views;
-  final bool isUpcoming;
-  final Function(ReminderPB reminder)? onAction;
-  final Function(ReminderPB reminder)? onDelete;
-  final Function(ReminderPB reminder, bool isRead)? onReadChanged;
-
-  @override
-  Widget build(BuildContext context) {
-    if (groupedReminders.isEmpty) {
-      return const Center(child: NotificationsHubEmpty());
-    }
-
-    final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;
-
-    return SingleChildScrollView(
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          ...groupedReminders.values.mapIndexed(
-            (index, reminders) {
-              final formattedDate = dateFormat.formatDate(
-                groupedReminders.keys.elementAt(index),
-                false,
-              );
-
-              return NotificationGroup(
-                reminders: reminders,
-                formattedDate: formattedDate,
-                isUpcoming: isUpcoming,
-                onReadChanged: onReadChanged,
-                onDelete: onDelete,
-                onAction: onAction,
-              );
-            },
-          ),
-        ],
-      ),
-    );
-  }
-}

+ 0 - 20
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notifications_hub_empty.dart

@@ -1,20 +0,0 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flutter/material.dart';
-
-class NotificationsHubEmpty extends StatelessWidget {
-  const NotificationsHubEmpty({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(vertical: 12),
-      child: Center(
-        child: FlowyText.regular(
-          LocaleKeys.notificationHub_empty.tr(),
-        ),
-      ),
-    );
-  }
-}

+ 3 - 1
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_button.dart → frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart

@@ -31,7 +31,9 @@ class NotificationButton extends StatelessWidget {
             child: AppFlowyPopover(
               mutex: mutex,
               direction: PopoverDirection.bottomWithLeftAligned,
-              constraints: const BoxConstraints(maxHeight: 250, maxWidth: 350),
+              constraints: const BoxConstraints(maxHeight: 250, maxWidth: 400),
+              windowPadding: EdgeInsets.zero,
+              margin: EdgeInsets.zero,
               popupBuilder: (_) =>
                   NotificationDialog(views: views, mutex: mutex),
               child: _buildNotificationIcon(context, state.hasUnreads),

+ 23 - 0
frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart

@@ -0,0 +1,23 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+
+class NotificationHubTitle extends StatelessWidget {
+  const NotificationHubTitle({
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16) +
+          const EdgeInsets.only(top: 12, bottom: 4),
+      child: FlowyText.semibold(
+        LocaleKeys.notificationHub_title.tr(),
+        color: Theme.of(context).colorScheme.tertiary,
+        fontSize: 16,
+      ),
+    );
+  }
+}

+ 56 - 54
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_item.dart → frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart

@@ -18,6 +18,7 @@ class NotificationItem extends StatefulWidget {
     required this.scheduled,
     required this.body,
     required this.isRead,
+    this.includeTime = false,
     this.readOnly = false,
     this.onAction,
     this.onDelete,
@@ -28,8 +29,9 @@ class NotificationItem extends StatefulWidget {
   final String title;
   final Int64 scheduled;
   final String body;
-  final bool isRead;
+  final bool includeTime;
   final bool readOnly;
+  final bool isRead;
 
   final VoidCallback? onAction;
   final VoidCallback? onDelete;
@@ -57,59 +59,59 @@ class _NotificationItemState extends State<NotificationItem> {
             onTap: widget.onAction,
             child: Opacity(
               opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
-              child: Container(
-                padding: const EdgeInsets.all(10),
+              child: DecoratedBox(
                 decoration: BoxDecoration(
-                  borderRadius: const BorderRadius.all(Radius.circular(6)),
                   color: _isHovering && widget.onAction != null
                       ? AFThemeExtension.of(context).lightGreyHover
                       : Colors.transparent,
+                  border: widget.isRead || widget.readOnly
+                      ? null
+                      : Border(
+                          left: BorderSide(
+                            width: 2,
+                            color: Theme.of(context).colorScheme.primary,
+                          ),
+                        ),
                 ),
-                child: Row(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Stack(
-                      children: [
-                        const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
-                        if (!widget.isRead && !widget.readOnly)
-                          Positioned(
-                            bottom: 1,
-                            right: 1,
-                            child: DecoratedBox(
-                              decoration: BoxDecoration(
-                                shape: BoxShape.circle,
-                                color: AFThemeExtension.of(context).warning,
-                              ),
-                              child: const SizedBox(height: 8, width: 8),
+                child: Padding(
+                  padding: const EdgeInsets.symmetric(
+                    vertical: 10,
+                    horizontal: 16,
+                  ),
+                  child: Row(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      FlowySvg(
+                        FlowySvgs.time_s,
+                        size: const Size.square(20),
+                        color: Theme.of(context).colorScheme.tertiary,
+                      ),
+                      const HSpace(16),
+                      Expanded(
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            FlowyText.semibold(
+                              widget.title,
+                              fontSize: 14,
+                              color: Theme.of(context).colorScheme.tertiary,
                             ),
-                          ),
-                      ],
-                    ),
-                    const HSpace(10),
-                    Expanded(
-                      child: Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        mainAxisAlignment: MainAxisAlignment.center,
-                        children: [
-                          Row(
-                            children: [
-                              FlowyText.semibold(
-                                widget.title,
-                                fontSize: 14,
+                            // TODO(Xazin): Relative time + View Name
+                            FlowyText.regular(
+                              _scheduledString(
+                                widget.scheduled,
+                                widget.includeTime,
                               ),
-                              const HSpace(8),
-                              FlowyText.regular(
-                                _scheduledString(widget.scheduled),
-                                fontSize: 10,
-                              ),
-                            ],
-                          ),
-                          const VSpace(5),
-                          FlowyText.regular(widget.body, maxLines: 4),
-                        ],
+                              fontSize: 10,
+                            ),
+                            const VSpace(5),
+                            FlowyText.regular(widget.body, maxLines: 4),
+                          ],
+                        ),
                       ),
-                    ),
-                  ],
+                    ],
+                  ),
                 ),
               ),
             ),
@@ -129,14 +131,14 @@ class _NotificationItemState extends State<NotificationItem> {
     );
   }
 
-  String _scheduledString(Int64 secondsSinceEpoch) => context
-      .read<AppearanceSettingsCubit>()
-      .state
-      .dateFormat
-      .formatDate(
-        DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
-        true,
-      );
+  String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) {
+    final appearance = context.read<AppearanceSettingsCubit>().state;
+    return appearance.dateFormat.formatDate(
+      DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
+      includeTime,
+      appearance.timeFormat,
+    );
+  }
 
   void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
 }

+ 78 - 0
frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart

@@ -0,0 +1,78 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+
+class NotificationTabBar extends StatelessWidget {
+  final TabController tabController;
+
+  const NotificationTabBar({
+    super.key,
+    required this.tabController,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return DecoratedBox(
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: BorderSide(
+            color: Theme.of(context).dividerColor,
+          ),
+        ),
+      ),
+      child: Row(
+        children: [
+          Expanded(
+            child: TabBar(
+              controller: tabController,
+              padding: const EdgeInsets.symmetric(horizontal: 8),
+              labelPadding: EdgeInsets.zero,
+              indicatorSize: TabBarIndicatorSize.label,
+              indicator: UnderlineTabIndicator(
+                borderSide: BorderSide(
+                  color: Theme.of(context).colorScheme.primary,
+                ),
+              ),
+              isScrollable: true,
+              tabs: [
+                _FlowyTab(
+                  label: LocaleKeys.notificationHub_tabs_inbox.tr(),
+                  isSelected: tabController.index == 0,
+                ),
+                _FlowyTab(
+                  label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
+                  isSelected: tabController.index == 1,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _FlowyTab extends StatelessWidget {
+  final String label;
+  final bool isSelected;
+
+  const _FlowyTab({
+    required this.label,
+    required this.isSelected,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Tab(
+      height: 26,
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 8),
+        child: FlowyText.regular(
+          label,
+          color: isSelected ? Theme.of(context).colorScheme.tertiary : null,
+        ),
+      ),
+    );
+  }
+}

+ 15 - 4
frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_view.dart → frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart

@@ -1,6 +1,7 @@
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
-import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
-import 'package:appflowy/workspace/presentation/notifications/notifications_hub_empty.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
+import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
 import 'package:flutter/material.dart';
@@ -15,6 +16,7 @@ class NotificationsView extends StatelessWidget {
     this.onAction,
     this.onDelete,
     this.onReadChanged,
+    this.actionBar,
   });
 
   final List<ReminderPB> shownReminders;
@@ -24,19 +26,27 @@ class NotificationsView extends StatelessWidget {
   final Function(ReminderPB reminder)? onAction;
   final Function(ReminderPB reminder)? onDelete;
   final Function(ReminderPB reminder, bool isRead)? onReadChanged;
+  final Widget? actionBar;
 
   @override
   Widget build(BuildContext context) {
     if (shownReminders.isEmpty) {
-      return const Center(child: NotificationsHubEmpty());
+      return Column(
+        mainAxisSize: MainAxisSize.max,
+        children: [
+          if (actionBar != null) actionBar!,
+          const Expanded(child: NotificationsHubEmpty()),
+        ],
+      );
     }
 
     return SingleChildScrollView(
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
+          if (actionBar != null) actionBar!,
           ...shownReminders.map(
-            (reminder) {
+            (ReminderPB reminder) {
               return NotificationItem(
                 reminderId: reminder.id,
                 key: ValueKey(reminder.id),
@@ -44,6 +54,7 @@ class NotificationsView extends StatelessWidget {
                 scheduled: reminder.scheduledAt,
                 body: reminder.message,
                 isRead: reminder.isRead,
+                includeTime: reminder.includeTime ?? false,
                 readOnly: isUpcoming,
                 onReadChanged: (isRead) =>
                     onReadChanged?.call(reminder, isRead),

+ 32 - 0
frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart

@@ -0,0 +1,32 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+
+class NotificationsHubEmpty extends StatelessWidget {
+  const NotificationsHubEmpty({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: Padding(
+        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            FlowyText(
+              LocaleKeys.notificationHub_emptyTitle.tr(),
+              fontWeight: FontWeight.w700,
+              fontSize: 14,
+            ),
+            const VSpace(8),
+            FlowyText.regular(
+              LocaleKeys.notificationHub_emptyBody.tr(),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 8 - 17
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart

@@ -31,9 +31,7 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.brightness_4,
           changeSelectedPage: changeSelectedPage,
         ),
-        const SizedBox(
-          height: 10,
-        ),
+        const SizedBox(height: 10),
         SettingsMenuElement(
           page: SettingsPage.language,
           selectedPage: currentPage,
@@ -41,9 +39,7 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.translate,
           changeSelectedPage: changeSelectedPage,
         ),
-        const SizedBox(
-          height: 10,
-        ),
+        const SizedBox(height: 10),
         SettingsMenuElement(
           page: SettingsPage.files,
           selectedPage: currentPage,
@@ -51,9 +47,7 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.file_present_outlined,
           changeSelectedPage: changeSelectedPage,
         ),
-        const SizedBox(
-          height: 10,
-        ),
+        const SizedBox(height: 10),
         SettingsMenuElement(
           page: SettingsPage.user,
           selectedPage: currentPage,
@@ -61,6 +55,7 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.account_box_outlined,
           changeSelectedPage: changeSelectedPage,
         ),
+        const SizedBox(height: 10),
         SettingsMenuElement(
           page: SettingsPage.notifications,
           selectedPage: currentPage,
@@ -68,12 +63,9 @@ class SettingsMenu extends StatelessWidget {
           icon: Icons.notifications_outlined,
           changeSelectedPage: changeSelectedPage,
         ),
-        if (showSyncSetting)
-          const SizedBox(
-            height: 10,
-          ),
         // Only show supabase setting if supabase is enabled and the current auth type is not local
-        if (showSyncSetting)
+        if (showSyncSetting) ...[
+          const SizedBox(height: 10),
           SettingsMenuElement(
             page: SettingsPage.syncSetting,
             selectedPage: currentPage,
@@ -81,9 +73,8 @@ class SettingsMenu extends StatelessWidget {
             icon: Icons.sync,
             changeSelectedPage: changeSelectedPage,
           ),
-        const SizedBox(
-          height: 10,
-        ),
+        ],
+        const SizedBox(height: 10),
         SettingsMenuElement(
           page: SettingsPage.shortcuts,
           selectedPage: currentPage,

+ 12 - 8
frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart

@@ -77,21 +77,25 @@ class DatePickerMenu extends DatePickerService {
   }) {
     dismiss();
 
-    // Use MediaQuery, since Stack takes up all window space
-    // and not just the space of the current Editor
-    final windowSize = MediaQuery.of(context).size;
+    final editorSize = editorState.renderBox!.size;
 
     double offsetX = offset.dx;
     double offsetY = offset.dy;
 
-    final showRight = (offset.dx + _datePickerWidth) < windowSize.width;
+    final showRight = (offset.dx + _datePickerWidth) < editorSize.width;
     if (!showRight) {
       offsetX = offset.dx - _datePickerWidth;
     }
 
-    final showBelow = (offset.dy + _datePickerHeight) < windowSize.height;
+    final showBelow = (offset.dy + _datePickerHeight) < editorSize.height;
     if (!showBelow) {
-      offsetY = offset.dy - _datePickerHeight;
+      if ((offset.dy - _datePickerHeight) < 0) {
+        // Show dialog in the middle
+        offsetY = offset.dy - (_datePickerHeight / 3);
+      } else {
+        // Show above
+        offsetY = offset.dy - _datePickerHeight;
+      }
     }
 
     _menuEntry = OverlayEntry(
@@ -99,8 +103,8 @@ class DatePickerMenu extends DatePickerService {
         return Material(
           type: MaterialType.transparency,
           child: SizedBox(
-            height: windowSize.height,
-            width: windowSize.width,
+            height: editorSize.height,
+            width: editorSize.width,
             child: RawKeyboardListener(
               focusNode: FocusNode()..requestFocus(),
               onKey: (event) {

+ 41 - 3
frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart

@@ -8,9 +8,10 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart
 import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/style_widget/text_field.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
 import 'package:flutter/material.dart';
 
 class IncludeTimeButton extends StatefulWidget {
@@ -37,6 +38,7 @@ class IncludeTimeButton extends StatefulWidget {
 
 class _IncludeTimeButtonState extends State<IncludeTimeButton> {
   late bool _includeTime = widget.includeTime;
+  bool _showTimeTooltip = false;
   String? _timeString;
 
   @override
@@ -76,6 +78,35 @@ class _IncludeTimeButtonState extends State<IncludeTimeButton> {
                   ),
                   const HSpace(6),
                   FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
+                  const HSpace(6),
+                  FlowyTooltip(
+                    message: LocaleKeys.datePicker_dateTimeFormatTooltip.tr(),
+                    child: FlowyHover(
+                      resetHoverOnRebuild: false,
+                      style: HoverStyle(
+                        foregroundColorOnHover:
+                            Theme.of(context).colorScheme.primary,
+                        borderRadius: Corners.s10Border,
+                      ),
+                      onHover: (isHovering) => setState(
+                        () => _showTimeTooltip = isHovering,
+                      ),
+                      child: FlowyTextButton(
+                        '?',
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 8,
+                          vertical: 2,
+                        ),
+                        fontColor: _showTimeTooltip
+                            ? Theme.of(context).colorScheme.onSurface
+                            : null,
+                        fillColor: _showTimeTooltip
+                            ? Theme.of(context).colorScheme.primary
+                            : null,
+                        radius: Corners.s12Border,
+                      ),
+                    ),
+                  ),
                   const Spacer(),
                   Toggle(
                     value: _includeTime,
@@ -96,6 +127,9 @@ class _IncludeTimeButtonState extends State<IncludeTimeButton> {
   }
 }
 
+const _maxLengthTwelveHour = 8;
+const _maxLengthTwentyFourHour = 5;
+
 class _TimeTextField extends StatefulWidget {
   const _TimeTextField({
     required this.timeStr,
@@ -152,6 +186,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
             text: _timeString ?? "",
             focusNode: _focusNode,
             controller: _textController,
+            maxLength: widget.timeFormat == UserTimeFormatPB.TwelveHour
+                ? _maxLengthTwelveHour
+                : _maxLengthTwentyFourHour,
+            showCounter: false,
             submitOnLeave: true,
             hintText: hintText,
             errorText: errorText,

+ 5 - 3
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart

@@ -21,8 +21,10 @@ class FlowyTextField extends StatefulWidget {
   final Duration? debounceDuration;
   final String? errorText;
   final int maxLines;
+  final bool showCounter;
 
   const FlowyTextField({
+    super.key,
     this.hintText = "",
     this.text,
     this.textStyle,
@@ -39,8 +41,8 @@ class FlowyTextField extends StatefulWidget {
     this.debounceDuration,
     this.errorText,
     this.maxLines = 1,
-    Key? key,
-  }) : super(key: key);
+    this.showCounter = true,
+  });
 
   @override
   State<FlowyTextField> createState() => FlowyTextFieldState();
@@ -133,7 +135,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
             .textTheme
             .bodySmall!
             .copyWith(color: Theme.of(context).hintColor),
-        suffixText: _suffixText(),
+        suffixText: widget.showCounter ? _suffixText() : "",
         counterText: "",
         focusedBorder: OutlineInputBorder(
           borderSide: BorderSide(

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

@@ -35,7 +35,6 @@ void main() {
         AppTheme.fallback,
       ),
       verify: (bloc) {
-        // expect(bloc.state.appTheme.info.name, "light");
         expect(bloc.state.font, 'Poppins');
         expect(bloc.state.monospaceFont, 'SF Mono');
         expect(bloc.state.themeMode, ThemeMode.system);

+ 10 - 1
frontend/resources/translations/en.json

@@ -814,6 +814,9 @@
       "shortKeyword": "remind"
     }
   },
+  "datePicker": {
+    "dateTimeFormatTooltip": "Change the date and time format in settings"
+  },
   "relativeDates": {
     "yesterday": "Yesterday",
     "today": "Today",
@@ -822,11 +825,17 @@
   },
   "notificationHub": {
     "title": "Notifications",
-    "empty": "Nothing to see here!",
+    "emptyTitle": "All caught up!",
+    "emptyBody": "No pending notifications or actions. Enjoy the calm.",
     "tabs": {
       "inbox": "Inbox",
       "upcoming": "Upcoming"
     },
+    "actions": {
+      "markAllRead": "Mark all as read",
+      "showAll": "All",
+      "showUnreads": "Unread"
+    },
     "filters": {
       "ascending": "Ascending",
       "descending": "Descending",