diff --git a/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart b/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart index 4fdfec6cbf..33b6eb67c7 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart @@ -32,13 +32,13 @@ void main() { await tester.findDateEditor(findsOneWidget); // Select date - await tester.selectLastDateInPicker(); + final isToday = await tester.selectLastDateInPicker(); - // Select Time of event reminder - await tester.selectReminderOption(ReminderOption.atTimeOfEvent); + // Select "On day of event" reminder + await tester.selectReminderOption(ReminderOption.onDayOfEvent); - // Expect Time of event to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); // Dismiss the cell/date editor await tester.dismissCellEditor(); @@ -47,14 +47,20 @@ void main() { await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.findDateEditor(findsOneWidget); - // Expect Time of event to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); // Dismiss the cell/date editor await tester.dismissCellEditor(); + int tabIndex = 1; + final now = DateTime.now(); + if (isToday && now.hour >= 9) { + tabIndex = 0; + } + // Open "Upcoming" in Notification hub - await tester.openNotificationHub(tabIndex: 1); + await tester.openNotificationHub(tabIndex: tabIndex); // Expect 1 notification tester.expectNotificationItems(1); @@ -80,13 +86,13 @@ void main() { await tester.findDateEditor(findsOneWidget); // Select date - await tester.selectLastDateInPicker(); + final isToday = await tester.selectLastDateInPicker(); - // Select Time of event reminder - await tester.selectReminderOption(ReminderOption.atTimeOfEvent); + // Select "On day of event"-reminder + await tester.selectReminderOption(ReminderOption.onDayOfEvent); - // Expect Time of event to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); // Dismiss the cell/date editor await tester.dismissCellEditor(); @@ -95,8 +101,8 @@ void main() { await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.findDateEditor(findsOneWidget); - // Expect Time of event to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); // Dismiss the cell/date editor await tester.dismissCellEditor(); @@ -105,8 +111,14 @@ void main() { await tester.createNewPageWithNameUnderParent(); await tester.pumpAndSettle(); - // Open "Upcoming" in Notification hub - await tester.openNotificationHub(tabIndex: 1); + int tabIndex = 1; + final now = DateTime.now(); + if (isToday && now.hour >= 9) { + tabIndex = 0; + } + + // Open correct tab in Notification hub + await tester.openNotificationHub(tabIndex: tabIndex); // Expect 1 notification tester.expectNotificationItems(1); @@ -118,5 +130,77 @@ void main() { // Expect to see Row Editor Dialog tester.expectToSeeRowDetailsPageDialog(); }); + + testWidgets( + 'toggle include time sets reminder option correctly', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithNameUnderParent( + layout: ViewLayoutPB.Grid, + ); + + // Invoke the field editor + await tester.tapGridFieldWithName('Type'); + await tester.tapEditFieldButton(); + + // Change to date type + await tester.tapSwitchFieldTypeButton(); + await tester.selectFieldType(FieldType.DateTime); + await tester.dismissFieldEditor(); + + // Open date picker + await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); + await tester.findDateEditor(findsOneWidget); + + // Select date + await tester.selectLastDateInPicker(); + + // Select "On day of event"-reminder + await tester.selectReminderOption(ReminderOption.onDayOfEvent); + + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); + + // Dismiss the cell/date editor + await tester.dismissCellEditor(); + + // Open date picker again + await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); + await tester.findDateEditor(findsOneWidget); + + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); + + // Toggle include time on + await tester.toggleIncludeTime(); + + // Expect "At time of event" to be displayed + tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + + // Dismiss the cell/date editor + await tester.dismissCellEditor(); + + // Open date picker again + await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); + await tester.findDateEditor(findsOneWidget); + + // Expect "At time of event" to be displayed + tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); + + // Select "One hour before"-reminder + await tester.selectReminderOption(ReminderOption.oneHourBefore); + + // Expect "One hour before" to be displayed + tester.expectSelectedReminder(ReminderOption.oneHourBefore); + + // Toggle include time off + await tester.toggleIncludeTime(); + + // Expect "On day of event" to be displayed + tester.expectSelectedReminder(ReminderOption.onDayOfEvent); + }, + ); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 447f91f07b..bdeb79dacf 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -322,20 +322,23 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future selectReminderOption(ReminderOption option) async { - await hoverOnWidget(find.byType(ReminderSelector)); + await tapButton(find.byType(ReminderSelector)); final finder = find.descendant( of: find.byType(FlowyButton), - matching: find.text(option.label), + matching: find.textContaining(option.label), ); await tapButton(finder); } - Future selectLastDateInPicker() async { + Future selectLastDateInPicker() async { final finder = find.byType(CellContent).last; + final w = widget(finder) as CellContent; await tapButton(finder); + + return w.isToday; } Future toggleDateRange() async { diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 97f3d764e6..ec1acc8e93 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -191,4 +191,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index 23b6208a59..5ac3d49c12 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -95,6 +95,7 @@ class _MobileDateCellEditScreenState extends State { includeTime: state.includeTime, use24hFormat: state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwentyFourHour, + timeFormat: state.dateTypeOptionPB.timeFormat, selectedReminderOption: state.reminderOption, onStartTimeChanged: (String? time) { if (time != null) { @@ -125,9 +126,14 @@ class _MobileDateCellEditScreenState extends State { onClearDate: () => context .read() .add(const DateCellEditorEvent.clearDate()), - onReminderSelected: (option) => context - .read() - .add(DateCellEditorEvent.setReminderOption(option: option)), + onReminderSelected: (option) => + context.read().add( + DateCellEditorEvent.setReminderOption( + option: option, + selectedDay: + state.dateTime == null ? DateTime.now() : null, + ), + ), ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart index a83a4635f9..6b133a3a61 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart @@ -13,6 +13,7 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:calendar_view/calendar_view.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; @@ -56,20 +57,28 @@ class DateCellEditorBloc dateCellData.isRange == state.isRange && dateCellData.isRange ? dateCellData.endDateTime : null; + ReminderOption option = state.reminderOption; if (dateCellData.dateTime != null && (state.reminderId?.isEmpty ?? true) && (dateCellData.reminderId?.isNotEmpty ?? false) && state.reminderOption != ReminderOption.none) { + final date = state.reminderOption.withoutTime + ? dateCellData.dateTime!.withoutTime + : dateCellData.dateTime!; + // Add Reminder _reminderBloc.add( ReminderEvent.addById( reminderId: dateCellData.reminderId!, objectId: cellController.viewId, - meta: {ReminderMetaKeys.rowId: cellController.rowId}, + meta: { + ReminderMetaKeys.includeTime: true.toString(), + ReminderMetaKeys.rowId: cellController.rowId, + }, scheduledAt: Int64( - dateCellData.dateTime! - .subtract(state.reminderOption.time) + state.reminderOption + .fromDate(date) .millisecondsSinceEpoch ~/ 1000, ), @@ -79,13 +88,25 @@ class DateCellEditorBloc if ((dateCellData.reminderId?.isNotEmpty ?? false) && dateCellData.dateTime != null) { + if (option.requiresNoTime && dateCellData.includeTime) { + option = ReminderOption.atTimeOfEvent; + } else if (!option.withoutTime && !dateCellData.includeTime) { + option = ReminderOption.onDayOfEvent; + } + + final date = option.withoutTime + ? dateCellData.dateTime!.withoutTime + : dateCellData.dateTime!; + + final scheduledAt = option.fromDate(date); + // Update Reminder _reminderBloc.add( ReminderEvent.update( ReminderUpdate( - id: state.reminderId!, - scheduledAt: dateCellData.dateTime! - .subtract(state.reminderOption.time), + id: dateCellData.reminderId!, + scheduledAt: scheduledAt, + includeTime: true, ), ), ); @@ -104,6 +125,7 @@ class DateCellEditorBloc dateStr: dateCellData.dateStr, endDateStr: dateCellData.endDateStr, reminderId: dateCellData.reminderId, + reminderOption: option, ), ); }, @@ -185,16 +207,21 @@ class DateCellEditorBloc await _clearDate(); }, - setReminderOption: (ReminderOption option) async { + setReminderOption: ( + ReminderOption option, + DateTime? selectedDay, + ) async { if (state.reminderId?.isEmpty ?? true && - state.dateTime != null && + (state.dateTime != null || selectedDay != null) && option != ReminderOption.none) { // New Reminder final reminderId = nanoid(); - await _updateDateData(reminderId: reminderId); + await _updateDateData(reminderId: reminderId, date: selectedDay); - emit(state.copyWith(reminderOption: option)); + emit( + state.copyWith(reminderOption: option, dateTime: selectedDay), + ); } else if (option == ReminderOption.none && (state.reminderId?.isNotEmpty ?? false)) { // Remove reminder @@ -204,12 +231,15 @@ class DateCellEditorBloc emit(state.copyWith(reminderOption: option)); } else if (state.dateTime != null && (state.reminderId?.isNotEmpty ?? false)) { + final scheduledAt = option.fromDate(state.dateTime!); + // Update reminder _reminderBloc.add( ReminderEvent.update( ReminderUpdate( id: state.reminderId!, - scheduledAt: state.dateTime!.subtract(option.time), + scheduledAt: scheduledAt, + includeTime: true, ), ), ); @@ -427,6 +457,7 @@ class DateCellEditorEvent with _$DateCellEditorEvent { const factory DateCellEditorEvent.setReminderOption({ required ReminderOption option, + @Default(null) DateTime? selectedDay, }) = _SetReminderOption; const factory DateCellEditorEvent.removeReminder() = _RemoveReminder; @@ -483,8 +514,11 @@ class DateCellEditorState with _$DateCellEditorState { final reminder = reminderBloc.state.reminders .firstWhereOrNull((r) => r.id == dateCellData.reminderId); if (reminder != null) { + final eventDate = dateCellData.includeTime + ? dateCellData.dateTime! + : dateCellData.dateTime!.withoutTime; reminderOption = ReminderOption.fromDateDifference( - dateCellData.dateTime!, + eventDate, reminder.scheduledAt.toDateTime(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart index 3eca97ea77..3a4d52a07d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; @@ -5,7 +8,6 @@ import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart' import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class MobileGridDateCellSkin extends IEditableDateCellSkin { @override @@ -25,9 +27,17 @@ class MobileGridDateCellSkin extends IEditableDateCellSkin { child: SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: FlowyText( - state.dateStr, - fontSize: 15, + child: Row( + children: [ + if (state.data?.reminderId.isNotEmpty ?? false) ...[ + const FlowySvg(FlowySvgs.clock_alarm_s), + const HSpace(6), + ], + FlowyText( + state.dateStr, + fontSize: 15, + ), + ], ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart index 63953d03aa..f2258d2f78 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart @@ -67,8 +67,12 @@ class _DateCellEditor extends State { parseEndTimeError: state.parseEndTimeError, parseTimeError: state.parseTimeError, popoverMutex: popoverMutex, - onReminderSelected: (option) => dateCellBloc - .add(DateCellEditorEvent.setReminderOption(option: option)), + onReminderSelected: (option) => dateCellBloc.add( + DateCellEditorEvent.setReminderOption( + option: option, + selectedDay: state.dateTime == null ? DateTime.now() : null, + ), + ), selectedReminderOption: state.reminderOption, options: [ OptionGroup( 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 6b10756b9e..001135f447 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 @@ -12,6 +12,7 @@ import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; @@ -192,6 +193,7 @@ class _MentionDateBlockState extends State { UserTimeFormatPB.TwentyFourHour, rebuildOnDaySelected: true, rebuildOnTimeChanged: true, + timeFormat: options.timeFormat.simplified, selectedReminderOption: widget.reminderOption, onDaySelected: options.onDaySelected, onStartTimeChanged: (time) => options @@ -342,7 +344,7 @@ class _MentionDateBlockState extends State { ReminderEvent.update( ReminderUpdate( id: widget.reminderId!, - scheduledAt: parsedDate!.subtract(reminderOption.time), + scheduledAt: reminderOption.fromDate(parsedDate!), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index cdc326afea..03dce860bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -104,6 +104,9 @@ class _InlineActionsHandlerState extends State { : 0; if (invalidCounter >= _invalidSearchesAmount) { + // Workaround to bring focus back to editor + await widget.editorState + .updateSelectionWithReason(widget.editorState.selection); return widget.onDismiss(); } @@ -192,7 +195,8 @@ class _InlineActionsHandlerState extends State { int get groupLength => results.length; - int lengthOfGroup(int index) => results[index].results.length; + int lengthOfGroup(int index) => + results.length > index ? results[index].results.length : -1; InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) => results[groupIndex].results[handlerIndex]; @@ -224,7 +228,21 @@ class _InlineActionsHandlerState extends State { widget.onDismiss(); return KeyEventResult.handled; } + + if (noResults) { + // Workaround to bring focus back to editor + widget.editorState + .updateSelectionWithReason(widget.editorState.selection); + widget.editorState.insertNewLine(); + + widget.onDismiss(); + return KeyEventResult.handled; + } } else if (event.logicalKey == LogicalKeyboardKey.escape) { + // Workaround to bring focus back to editor + widget.editorState + .updateSelectionWithReason(widget.editorState.selection); + widget.onDismiss(); } else if (event.logicalKey == LogicalKeyboardKey.backspace) { if (_search.isEmpty) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index 90c118e5ea..45ac056a4f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -31,7 +31,7 @@ class NotificationButton extends StatelessWidget { child: AppFlowyPopover( mutex: mutex, direction: PopoverDirection.bottomWithLeftAligned, - constraints: const BoxConstraints(maxHeight: 250, maxWidth: 425), + constraints: const BoxConstraints(maxHeight: 500, maxWidth: 425), windowPadding: EdgeInsets.zero, margin: EdgeInsets.zero, popupBuilder: (_) => diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart index be5ad2e43b..0f69d0e679 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart @@ -62,10 +62,24 @@ class _NotificationItemState extends State { bool _isHovering = false; int? path; + late final String infoString; + @override void initState() { super.initState(); widget.block?.then((b) => path = b?.path.first); + infoString = _buildInfoString(); + } + + String _buildInfoString() { + String scheduledString = + _scheduledString(widget.scheduled, widget.includeTime); + + if (widget.view != null) { + scheduledString = '$scheduledString - ${widget.view!.name}'; + } + + return scheduledString; } @override @@ -135,10 +149,7 @@ class _NotificationItemState extends State { ), // TODO(Xazin): Relative time FlowyText.regular( - '${_scheduledString( - widget.scheduled, - widget.includeTime, - )}${widget.view != null ? " - ${widget.view!.name}" : ""}', + infoString, fontSize: PlatformExtension.isMobile ? 12 : 10, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart index 12d223af93..93ab2b2754 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart @@ -135,6 +135,18 @@ class _AppFlowyDatePickerState extends State { late DateTime? _selectedDay = widget.selectedDay; late ReminderOption _selectedReminderOption = widget.selectedReminderOption; + @override + void didUpdateWidget(covariant AppFlowyDatePicker oldWidget) { + _selectedDay = oldWidget.selectedDay != widget.selectedDay + ? widget.selectedDay + : _selectedDay; + _selectedReminderOption = + oldWidget.selectedReminderOption != widget.selectedReminderOption + ? widget.selectedReminderOption + : _selectedReminderOption; + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) => PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker(); @@ -222,6 +234,8 @@ class _AppFlowyDatePickerState extends State { const _GroupSeparator(), ReminderSelector( mutex: widget.popoverMutex, + hasTime: widget.includeTime, + timeFormat: widget.timeFormat, selectedOption: _selectedReminderOption, onOptionSelected: (option) { setState(() => _selectedReminderOption = option); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart index 2b2cc702e4..e1d0411b09 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart @@ -7,9 +7,11 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.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'; @@ -31,6 +33,7 @@ class MobileAppFlowyDatePicker extends StatefulWidget { this.rebuildOnTimeChanged = false, required this.includeTime, required this.use24hFormat, + required this.timeFormat, this.selectedReminderOption, required this.onStartTimeChanged, this.onEndTimeChanged, @@ -59,6 +62,8 @@ class MobileAppFlowyDatePicker extends StatefulWidget { final bool rebuildOnTimeChanged; final bool use24hFormat; + final TimeFormatPB timeFormat; + final ReminderOption? selectedReminderOption; final Function(String? time) onStartTimeChanged; @@ -143,6 +148,8 @@ class _MobileAppFlowyDatePickerState extends State { widget.onReminderSelected!.call(option); setState(() => _reminderOption = option); }, + timeFormat: widget.timeFormat, + hasTime: widget.includeTime, ), ], if (widget.onClearDate != null) ...[ @@ -166,10 +173,14 @@ class _ReminderSelector extends StatelessWidget { const _ReminderSelector({ this.selectedReminderOption, required this.onReminderSelected, + required this.timeFormat, + this.hasTime = false, }); final ReminderOption? selectedReminderOption; final OnReminderSelected onReminderSelected; + final TimeFormatPB timeFormat; + final bool hasTime; @override Widget build(BuildContext context) { @@ -180,8 +191,12 @@ class _ReminderSelector extends StatelessWidget { availableOptions.remove(ReminderOption.custom); } + availableOptions.removeWhere( + (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime), + ); + return FlowyOptionTile.text( - text: 'Reminder', + text: LocaleKeys.datePicker_reminderLabel.tr(), trailing: Row( children: [ const HSpace(6.0), @@ -200,39 +215,51 @@ class _ReminderSelector extends StatelessWidget { onTap: () => showMobileBottomSheet( context, padding: EdgeInsets.zero, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.7, - minChildSize: 0.7, - builder: (context, controller) => Column( - children: [ - const _ReminderSelectHeader(), - const VSpace(12.0), - Flexible( - child: SingleChildScrollView( - controller: controller, - child: Column( - children: availableOptions - .map( - (o) => FlowyOptionTile.text( - text: o.label, - showTopBorder: o == ReminderOption.none, - onTap: () { - onReminderSelected(o); - context.pop(); - }, - ), - ) - .toList(), - ), + builder: (_) => DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.7, + builder: (context, controller) => Column( + children: [ + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: const Center(child: DragHandler()), + ), + const _ReminderSelectHeader(), + Flexible( + child: SingleChildScrollView( + controller: controller, + child: Column( + children: availableOptions.map( + (o) { + String label = o.label; + if (o.withoutTime && !o.timeExempt) { + const time = "09:00"; + final t = timeFormat == TimeFormatPB.TwelveHour + ? "$time AM" + : time; + + label = "$label ($t)"; + } + + return FlowyOptionTile.text( + text: label, + showTopBorder: o == ReminderOption.none, + onTap: () { + onReminderSelected(o); + context.pop(); + }, + ); + }, + ).toList() + ..insert(0, const _Divider()), ), ), - ], - ), - ); - }, + ), + ], + ), + ), ), ); } @@ -243,8 +270,16 @@ class _ReminderSelectHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( + return Container( height: 56, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -252,8 +287,8 @@ class _ReminderSelectHeader extends StatelessWidget { width: 120, child: AppBarCancelButton(onTap: context.pop), ), - const FlowyText.medium( - 'Select reminder', + FlowyText.medium( + LocaleKeys.datePicker_selectReminder.tr(), fontSize: 17.0, ), const HSpace(120), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart index 31cdbd7778..b4224bb05a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart @@ -31,7 +31,6 @@ class DateTypeOptionButton extends StatelessWidget { "${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}"; return AppFlowyPopover( mutex: popoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(8, 0), margin: EdgeInsets.zero, constraints: BoxConstraints.loose(const Size(140, 100)), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart index 4043264aee..d7f88e8d12 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -15,11 +17,15 @@ class ReminderSelector extends StatelessWidget { required this.mutex, required this.selectedOption, required this.onOptionSelected, + required this.timeFormat, + this.hasTime = false, }); final PopoverMutex? mutex; final ReminderOption selectedOption; final OnReminderSelected? onOptionSelected; + final TimeFormatPB timeFormat; + final bool hasTime; @override Widget build(BuildContext context) { @@ -28,39 +34,53 @@ class ReminderSelector extends StatelessWidget { options.remove(ReminderOption.custom); } - final optionWidgets = options - .map( - (o) => SizedBox( - height: DatePickerSize.itemHeight, - child: FlowyButton( - text: FlowyText.medium(o.label), - rightIcon: o == selectedOption - ? const FlowySvg(FlowySvgs.check_s) - : null, - onTap: () { - if (o != selectedOption) { - onOptionSelected?.call(o); - mutex?.close(); - } - }, - ), + options.removeWhere( + (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime), + ); + + final optionWidgets = options.map( + (o) { + String label = o.label; + if (o.withoutTime && !o.timeExempt) { + const time = "09:00"; + final t = timeFormat == TimeFormatPB.TwelveHour ? "$time AM" : time; + + label = "$label ($t)"; + } + + return SizedBox( + height: DatePickerSize.itemHeight, + child: FlowyButton( + text: FlowyText.medium(label), + rightIcon: + o == selectedOption ? const FlowySvg(FlowySvgs.check_s) : null, + onTap: () { + if (o != selectedOption) { + onOptionSelected?.call(o); + mutex?.close(); + } + }, ), - ) - .toList(); + ); + }, + ).toList(); return AppFlowyPopover( mutex: mutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, -155), + offset: const Offset(8, 0), margin: EdgeInsets.zero, - constraints: BoxConstraints.loose(const Size(150, 310)), - popupBuilder: (_) => Padding( - padding: const EdgeInsets.all(6.0), - child: ListView.separated( - itemCount: options.length, - separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight), - itemBuilder: (_, index) => optionWidgets[index], - ), + constraints: const BoxConstraints(maxHeight: 400, maxWidth: 205), + popupBuilder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: SeparatedColumn( + children: optionWidgets, + separatorBuilder: () => VSpace(DatePickerSize.seperatorHeight), + ), + ), + ], ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -90,13 +110,29 @@ enum ReminderOption { thirtyMinsBefore(time: Duration(minutes: 30)), oneHourBefore(time: Duration(hours: 1)), twoHoursBefore(time: Duration(hours: 2)), - oneDayBefore(time: Duration(days: 1)), - twoDaysBefore(time: Duration(days: 2)), + onDayOfEvent( + time: Duration(hours: 9), + withoutTime: true, + requiresNoTime: true, + ), + // 9:00 AM the day before (24-9) + oneDayBefore(time: Duration(hours: 15), withoutTime: true), + twoDaysBefore(time: Duration(days: 1, hours: 15), withoutTime: true), + oneWeekBefore(time: Duration(days: 6, hours: 15), withoutTime: true), custom(time: Duration()); - const ReminderOption({required this.time}); + const ReminderOption({ + required this.time, + this.withoutTime = false, + this.requiresNoTime = false, + }); final Duration time; + final bool withoutTime; + final bool requiresNoTime; + + bool get timeExempt => + [ReminderOption.none, ReminderOption.custom].contains(this); String get label => switch (this) { ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(), @@ -114,10 +150,14 @@ enum ReminderOption { LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(), ReminderOption.twoHoursBefore => LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(), + ReminderOption.onDayOfEvent => + LocaleKeys.datePicker_reminderOptions_onDayOfEvent.tr(), ReminderOption.oneDayBefore => LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), ReminderOption.twoDaysBefore => LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(), + ReminderOption.oneWeekBefore => + LocaleKeys.datePicker_reminderOptions_oneWeekBefore.tr(), ReminderOption.custom => LocaleKeys.datePicker_reminderOptions_custom.tr(), }; @@ -125,8 +165,15 @@ enum ReminderOption { static ReminderOption fromDateDifference( DateTime eventDate, DateTime reminderDate, - ) => - fromMinutes(eventDate.difference(reminderDate).inMinutes); + ) { + final def = fromMinutes(eventDate.difference(reminderDate).inMinutes); + if (def != ReminderOption.custom) { + return def; + } + + final diff = eventDate.withoutTime.difference(reminderDate).inMinutes; + return fromMinutes(diff); + } static ReminderOption fromMinutes(int minutes) => switch (minutes) { 0 => ReminderOption.atTimeOfEvent, @@ -136,8 +183,18 @@ enum ReminderOption { 30 => ReminderOption.thirtyMinsBefore, 60 => ReminderOption.oneHourBefore, 120 => ReminderOption.twoHoursBefore, - 1440 => ReminderOption.oneDayBefore, - 2880 => ReminderOption.twoDaysBefore, + // Negative because Event Day Today + 940 minutes + -540 => ReminderOption.onDayOfEvent, + 900 => ReminderOption.oneDayBefore, + 2340 => ReminderOption.twoDaysBefore, + 9540 => ReminderOption.oneWeekBefore, _ => ReminderOption.custom, }; + + DateTime fromDate(DateTime date) => switch (withoutTime) { + true => requiresNoTime + ? date.withoutTime.add(time) + : date.withoutTime.subtract(time), + _ => date.subtract(time), + }; } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index af7a3766bd..e55a6f6643 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -235,16 +235,24 @@ class PopoverState extends State { switch (widget.clickHandler) { case PopoverClickHandler.listener: return Listener( - onPointerDown: (_) => handler(), + onPointerDown: (_) => _callHandler(handler), child: child, ); case PopoverClickHandler.gestureDetector: return GestureDetector( - onTap: handler, + onTap: () => _callHandler(handler), child: child, ); } } + + void _callHandler(VoidCallback handler) { + if (_rootEntry.contains(this)) { + close(); + } else { + handler(); + } + } } class PopoverContainer extends StatefulWidget { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index c41b9b067a..7c1a1ef750 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1037,6 +1037,7 @@ "timeFormat": "Time format", "clearDate": "Clear date", "reminderLabel": "Reminder", + "selectReminder": "Select reminder", "reminderOptions": { "none": "None", "atTimeOfEvent": "Time of event", @@ -1046,8 +1047,10 @@ "thirtyMinsBefore": "30 mins before", "oneHourBefore": "1 hour before", "twoHoursBefore": "2 hours before", + "onDayOfEvent": "On day of event", "oneDayBefore": "1 day before", "twoDaysBefore": "2 days before", + "oneWeekBefore": "1 week before", "custom": "Custom" } }, @@ -1247,4 +1250,4 @@ "userIcon": "User icon" }, "noLogFiles": "There're no log files" -} \ No newline at end of file +}