diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 528181870d..5a293fd2c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/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 diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 2f5eda11b4..d154e71a8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/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().add( ReminderEvent.update( - ReminderUpdate(id: reminderId, scheduledAt: selectedDay), + ReminderUpdate( + id: reminderId, + scheduledAt: selectedDay, + includeTime: includeTime, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index d022bca662..79f46c4237 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/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()), ); diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 0c99885763..a14ada4100 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 8eb2b889c0..9a0d8ef039 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart b/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart index fb668c8cd7..53ba478b41 100644 --- a/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart +++ b/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((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 get props => [showUnreadsOnly, groupByDate, sortBy]; + bool get hasFilters => showUnreadsOnly != false; } diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart index 6ec98ac558..92d2e47db1 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ b/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 { on((event, emit) async { await event.when( + markAllRead: () async { + final unreadReminders = + state.pastReminders.where((reminder) => !reminder.isRead); + + final reminders = [...state.reminders]; + final updatedReminders = []; + 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, ); } } diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart new file mode 100644 index 0000000000..b169d627ad --- /dev/null +++ b/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; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action.dart b/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action.dart rename to frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart similarity index 91% rename from frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action_bloc.dart rename to frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart index e3af97103c..18f7df669c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_action_bloc.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/application/local_notifications/notification_service.dart rename to frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 3bb4a5e1b6..1e96a5685e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index 32479e1ffe..d726d92806 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart index da546ff8a7..8e2f8d6c3e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart +++ b/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 { - List sortByScheduledAt({ - bool isDescending = true, - }) => - sorted( - (a, b) => isDescending - ? b.scheduledAt.compareTo(a.scheduledAt) - : a.scheduledAt.compareTo(b.scheduledAt), - ); + List sortByScheduledAt() => + sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt)); } class NotificationDialog extends StatefulWidget { @@ -78,112 +72,44 @@ class _NotificationDialogState extends State builder: (context, filterState) => BlocBuilder( builder: (context, state) { - final sortDescending = - filterState.sortBy == NotificationSortOption.descending; - final List pastReminders = state.pastReminders .where((r) => filterState.showUnreadsOnly ? !r.isRead : true) - .sortByScheduledAt(isDescending: sortDescending); + .sortByScheduledAt(); - final List upcomingReminders = state.upcomingReminders - .sortByScheduledAt(isDescending: sortDescending); + final List 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: pastReminders, + reminderBloc: _reminderBloc, + views: widget.views, + onDelete: _onDelete, + onAction: _onAction, + onReadChanged: _onReadChanged, + actionBar: _InboxActionBar( + hasUnreads: state.hasUnreads, + showUnreadsOnly: filterState.showUnreadsOnly, ), - NotificationsView( - shownReminders: upcomingReminders, - reminderBloc: _reminderBloc, - views: widget.views, - isUpcoming: true, - onAction: _onAction, - ), - ] else ...[ - NotificationsGroupView( - groupedReminders: groupBy( - pastReminders, - (r) => DateTime.fromMillisecondsSinceEpoch( - r.scheduledAt.toInt() * 1000, - ).withoutTime, - ), - reminderBloc: _reminderBloc, - views: widget.views, - onAction: _onAction, - onDelete: _onDelete, - onReadChanged: _onReadChanged, - ), - NotificationsGroupView( - groupedReminders: groupBy( - 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 } } -class NotificationViewFilters extends StatelessWidget { - NotificationViewFilters({super.key}); - final PopoverMutex _mutex = PopoverMutex(); +class _InboxActionBar extends StatelessWidget { + const _InboxActionBar({ + required this.hasUnreads, + required this.showUnreadsOnly, + }); + + final bool hasUnreads; + final bool showUnreadsOnly; @override Widget build(BuildContext context) { - return BlocProvider.value( - value: context.read(), - child: BlocBuilder( - 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(), - ), - ); - }, - child: FlowyIconButton( - isSelected: state.hasFilters, - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - icon: const FlowySvg(FlowySvgs.filter_s), + 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() + .add(const ReminderEvent.markAllRead()), ), - ); - }, + _ToggleUnreadsButton( + showUnreadsOnly: showUnreadsOnly, + onToggled: (showUnreadsOnly) => context + .read() + .add(const NotificationFilterEvent.toggleShowUnreadsOnly()), + ), + ], + ), ), ); } } -class NotificationFilterPopover extends StatelessWidget { - const NotificationFilterPopover({ - super.key, - 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 + State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState(); +} + +class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> { + late bool showUnreadsOnly = widget.showUnreadsOnly; @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _SortByOption(bloc: bloc), - _ShowUnreadsToggle(bloc: bloc), - _GroupByDateToggle(bloc: bloc), - BlocProvider.value( - value: bloc, - child: BlocBuilder( - 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 SegmentedButton( + onSelectionChanged: (Set 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( + (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( + (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( + value: false, + label: Text( + LocaleKeys.notificationHub_actions_showAll.tr(), + style: const TextStyle(fontSize: 12), + ), + ), + ButtonSegment( + value: true, + label: Text( + LocaleKeys.notificationHub_actions_showUnreads.tr(), + style: const TextStyle(fontSize: 12), ), ), ], + selected: {showUnreadsOnly}, ); } } -class _ShowUnreadsToggle extends StatelessWidget { - const _ShowUnreadsToggle({required this.bloc}); +class _MarkAsReadButton extends StatefulWidget { + final VoidCallback? onMarkAllRead; - final NotificationFilterBloc bloc; + const _MarkAsReadButton({this.onMarkAllRead}); @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - 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<_MarkAsReadButton> createState() => _MarkAsReadButtonState(); } -class _GroupByDateToggle extends StatelessWidget { - const _GroupByDateToggle({required this.bloc}); - - final NotificationFilterBloc bloc; - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - 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, - ), - ], - ); - }, - ), - ); - } -} - -class _SortByOption extends StatefulWidget { - const _SortByOption({required this.bloc}); - - final NotificationFilterBloc bloc; - - @override - State<_SortByOption> createState() => _SortByOptionState(); -} - -class _SortByOptionState extends State<_SortByOption> { +class _MarkAsReadButtonState extends State<_MarkAsReadButton> { bool _isHovering = false; @override Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.bloc, - child: BlocBuilder( - 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, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_group.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_group.dart deleted file mode 100644 index 4cd9ade0d6..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_group.dart +++ /dev/null @@ -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 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(), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_grouped_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_grouped_view.dart deleted file mode 100644 index 77a420e0a6..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_grouped_view.dart +++ /dev/null @@ -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> groupedReminders; - final ReminderBloc reminderBloc; - final List 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().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, - ); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notifications_hub_empty.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notifications_hub_empty.dart deleted file mode 100644 index a0971051f9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notifications_hub_empty.dart +++ /dev/null @@ -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(), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart similarity index 95% rename from frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_button.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index 8c644ba9e1..63d0fe5a05 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_button.dart +++ b/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), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart new file mode 100644 index 0000000000..0434cc56d0 --- /dev/null +++ b/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, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart similarity index 66% rename from frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_item.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart index 71c902d962..f79c0119e4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_item.dart +++ b/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 { 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), - ), - ), - ], - ), - const HSpace(10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - FlowyText.semibold( - widget.title, - fontSize: 14, - ), - const HSpace(8), - FlowyText.regular( - _scheduledString(widget.scheduled), - fontSize: 10, - ), - ], - ), - const VSpace(5), - FlowyText.regular(widget.body, maxLines: 4), - ], + 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, + ), + // TODO(Xazin): Relative time + View Name + FlowyText.regular( + _scheduledString( + widget.scheduled, + widget.includeTime, + ), + fontSize: 10, + ), + const VSpace(5), + FlowyText.regular(widget.body, maxLines: 4), + ], + ), + ), + ], + ), ), ), ), @@ -129,14 +131,14 @@ class _NotificationItemState extends State { ); } - String _scheduledString(Int64 secondsSinceEpoch) => context - .read() - .state - .dateFormat - .formatDate( - DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000), - true, - ); + String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) { + final appearance = context.read().state; + return appearance.dateFormat.formatDate( + DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000), + includeTime, + appearance.timeFormat, + ); + } void _onHover(bool isHovering) => setState(() => _isHovering = isHovering); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart new file mode 100644 index 0000000000..f1a1df1368 --- /dev/null +++ b/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, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart similarity index 72% rename from frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_view.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart index 02212d7245..334f1c06cd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_view.dart +++ b/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 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), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart new file mode 100644 index 0000000000..d1f87019d6 --- /dev/null +++ b/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(), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 1418a891c9..b53b8b0fea 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/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, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index df8b1a3951..6f4abf3b95 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/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) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart index d231e2b304..adc16649df 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart +++ b/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 { late bool _includeTime = widget.includeTime; + bool _showTimeTooltip = false; String? _timeString; @override @@ -76,6 +78,35 @@ class _IncludeTimeButtonState extends State { ), 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 { } } +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, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index c1d871c203..24135d9228 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/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 createState() => FlowyTextFieldState(); @@ -133,7 +135,7 @@ class FlowyTextFieldState extends State { .textTheme .bodySmall! .copyWith(color: Theme.of(context).hintColor), - suffixText: _suffixText(), + suffixText: widget.showCounter ? _suffixText() : "", counterText: "", focusedBorder: OutlineInputBorder( borderSide: BorderSide( diff --git a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart index 0b58a239c4..406639f897 100644 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart +++ b/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); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e261c509e1..78057d8637 100644 --- a/frontend/resources/translations/en.json +++ b/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",