fix: launch review reminder (#4514)

* fix: dismiss menu on enter on no result

* fix: add drag handle to mobile reminder dialog

* fix: show reminder icon  in date cell in grid

* fix: auto select day when selecting reminder

* fix: increase height of notification hub

* fix: let some reminder options require time and show time

* fix: handling of non-time reminder options

* test: fix edit date time cell test

* fix: close popover when pressing child again

* fix: add time of now when setting include time

* fix: clean logic

* fix: tests

* fix: add test and include time in notification hub

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Mathias Mogensen 2024-01-31 14:17:36 +01:00 committed by GitHub
parent d27e2179cc
commit 247405ff51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 411 additions and 123 deletions

View File

@ -32,13 +32,13 @@ void main() {
await tester.findDateEditor(findsOneWidget); await tester.findDateEditor(findsOneWidget);
// Select date // Select date
await tester.selectLastDateInPicker(); final isToday = await tester.selectLastDateInPicker();
// Select Time of event reminder // Select "On day of event" reminder
await tester.selectReminderOption(ReminderOption.atTimeOfEvent); await tester.selectReminderOption(ReminderOption.onDayOfEvent);
// Expect Time of event to be displayed // Expect "On day of event" to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); tester.expectSelectedReminder(ReminderOption.onDayOfEvent);
// Dismiss the cell/date editor // Dismiss the cell/date editor
await tester.dismissCellEditor(); await tester.dismissCellEditor();
@ -47,14 +47,20 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget); await tester.findDateEditor(findsOneWidget);
// Expect Time of event to be displayed // Expect "On day of event" to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); tester.expectSelectedReminder(ReminderOption.onDayOfEvent);
// Dismiss the cell/date editor // Dismiss the cell/date editor
await tester.dismissCellEditor(); await tester.dismissCellEditor();
int tabIndex = 1;
final now = DateTime.now();
if (isToday && now.hour >= 9) {
tabIndex = 0;
}
// Open "Upcoming" in Notification hub // Open "Upcoming" in Notification hub
await tester.openNotificationHub(tabIndex: 1); await tester.openNotificationHub(tabIndex: tabIndex);
// Expect 1 notification // Expect 1 notification
tester.expectNotificationItems(1); tester.expectNotificationItems(1);
@ -80,13 +86,13 @@ void main() {
await tester.findDateEditor(findsOneWidget); await tester.findDateEditor(findsOneWidget);
// Select date // Select date
await tester.selectLastDateInPicker(); final isToday = await tester.selectLastDateInPicker();
// Select Time of event reminder // Select "On day of event"-reminder
await tester.selectReminderOption(ReminderOption.atTimeOfEvent); await tester.selectReminderOption(ReminderOption.onDayOfEvent);
// Expect Time of event to be displayed // Expect "On day of event" to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); tester.expectSelectedReminder(ReminderOption.onDayOfEvent);
// Dismiss the cell/date editor // Dismiss the cell/date editor
await tester.dismissCellEditor(); await tester.dismissCellEditor();
@ -95,8 +101,8 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget); await tester.findDateEditor(findsOneWidget);
// Expect Time of event to be displayed // Expect "On day of event" to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); tester.expectSelectedReminder(ReminderOption.onDayOfEvent);
// Dismiss the cell/date editor // Dismiss the cell/date editor
await tester.dismissCellEditor(); await tester.dismissCellEditor();
@ -105,8 +111,14 @@ void main() {
await tester.createNewPageWithNameUnderParent(); await tester.createNewPageWithNameUnderParent();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Open "Upcoming" in Notification hub int tabIndex = 1;
await tester.openNotificationHub(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 // Expect 1 notification
tester.expectNotificationItems(1); tester.expectNotificationItems(1);
@ -118,5 +130,77 @@ void main() {
// Expect to see Row Editor Dialog // Expect to see Row Editor Dialog
tester.expectToSeeRowDetailsPageDialog(); 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);
},
);
}); });
} }

View File

@ -322,20 +322,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
} }
Future<void> selectReminderOption(ReminderOption option) async { Future<void> selectReminderOption(ReminderOption option) async {
await hoverOnWidget(find.byType(ReminderSelector)); await tapButton(find.byType(ReminderSelector));
final finder = find.descendant( final finder = find.descendant(
of: find.byType(FlowyButton), of: find.byType(FlowyButton),
matching: find.text(option.label), matching: find.textContaining(option.label),
); );
await tapButton(finder); await tapButton(finder);
} }
Future<void> selectLastDateInPicker() async { Future<bool> selectLastDateInPicker() async {
final finder = find.byType(CellContent).last; final finder = find.byType(CellContent).last;
final w = widget(finder) as CellContent;
await tapButton(finder); await tapButton(finder);
return w.isToday;
} }
Future<void> toggleDateRange() async { Future<void> toggleDateRange() async {

View File

@ -191,4 +191,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac
COCOAPODS: 1.12.1 COCOAPODS: 1.14.3

View File

@ -95,6 +95,7 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
includeTime: state.includeTime, includeTime: state.includeTime,
use24hFormat: state.dateTypeOptionPB.timeFormat == use24hFormat: state.dateTypeOptionPB.timeFormat ==
TimeFormatPB.TwentyFourHour, TimeFormatPB.TwentyFourHour,
timeFormat: state.dateTypeOptionPB.timeFormat,
selectedReminderOption: state.reminderOption, selectedReminderOption: state.reminderOption,
onStartTimeChanged: (String? time) { onStartTimeChanged: (String? time) {
if (time != null) { if (time != null) {
@ -125,9 +126,14 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
onClearDate: () => context onClearDate: () => context
.read<DateCellEditorBloc>() .read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()), .add(const DateCellEditorEvent.clearDate()),
onReminderSelected: (option) => context onReminderSelected: (option) =>
.read<DateCellEditorBloc>() context.read<DateCellEditorBloc>().add(
.add(DateCellEditorEvent.setReminderOption(option: option)), DateCellEditorEvent.setReminderOption(
option: option,
selectedDay:
state.dateTime == null ? DateTime.now() : null,
),
),
); );
}, },
), ),

View File

@ -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-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.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:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart' import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension; show StringTranslateExtension;
@ -56,20 +57,28 @@ class DateCellEditorBloc
dateCellData.isRange == state.isRange && dateCellData.isRange dateCellData.isRange == state.isRange && dateCellData.isRange
? dateCellData.endDateTime ? dateCellData.endDateTime
: null; : null;
ReminderOption option = state.reminderOption;
if (dateCellData.dateTime != null && if (dateCellData.dateTime != null &&
(state.reminderId?.isEmpty ?? true) && (state.reminderId?.isEmpty ?? true) &&
(dateCellData.reminderId?.isNotEmpty ?? false) && (dateCellData.reminderId?.isNotEmpty ?? false) &&
state.reminderOption != ReminderOption.none) { state.reminderOption != ReminderOption.none) {
final date = state.reminderOption.withoutTime
? dateCellData.dateTime!.withoutTime
: dateCellData.dateTime!;
// Add Reminder // Add Reminder
_reminderBloc.add( _reminderBloc.add(
ReminderEvent.addById( ReminderEvent.addById(
reminderId: dateCellData.reminderId!, reminderId: dateCellData.reminderId!,
objectId: cellController.viewId, objectId: cellController.viewId,
meta: {ReminderMetaKeys.rowId: cellController.rowId}, meta: {
ReminderMetaKeys.includeTime: true.toString(),
ReminderMetaKeys.rowId: cellController.rowId,
},
scheduledAt: Int64( scheduledAt: Int64(
dateCellData.dateTime! state.reminderOption
.subtract(state.reminderOption.time) .fromDate(date)
.millisecondsSinceEpoch ~/ .millisecondsSinceEpoch ~/
1000, 1000,
), ),
@ -79,13 +88,25 @@ class DateCellEditorBloc
if ((dateCellData.reminderId?.isNotEmpty ?? false) && if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
dateCellData.dateTime != null) { 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 // Update Reminder
_reminderBloc.add( _reminderBloc.add(
ReminderEvent.update( ReminderEvent.update(
ReminderUpdate( ReminderUpdate(
id: state.reminderId!, id: dateCellData.reminderId!,
scheduledAt: dateCellData.dateTime! scheduledAt: scheduledAt,
.subtract(state.reminderOption.time), includeTime: true,
), ),
), ),
); );
@ -104,6 +125,7 @@ class DateCellEditorBloc
dateStr: dateCellData.dateStr, dateStr: dateCellData.dateStr,
endDateStr: dateCellData.endDateStr, endDateStr: dateCellData.endDateStr,
reminderId: dateCellData.reminderId, reminderId: dateCellData.reminderId,
reminderOption: option,
), ),
); );
}, },
@ -185,16 +207,21 @@ class DateCellEditorBloc
await _clearDate(); await _clearDate();
}, },
setReminderOption: (ReminderOption option) async { setReminderOption: (
ReminderOption option,
DateTime? selectedDay,
) async {
if (state.reminderId?.isEmpty ?? if (state.reminderId?.isEmpty ??
true && true &&
state.dateTime != null && (state.dateTime != null || selectedDay != null) &&
option != ReminderOption.none) { option != ReminderOption.none) {
// New Reminder // New Reminder
final reminderId = nanoid(); 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 && } else if (option == ReminderOption.none &&
(state.reminderId?.isNotEmpty ?? false)) { (state.reminderId?.isNotEmpty ?? false)) {
// Remove reminder // Remove reminder
@ -204,12 +231,15 @@ class DateCellEditorBloc
emit(state.copyWith(reminderOption: option)); emit(state.copyWith(reminderOption: option));
} else if (state.dateTime != null && } else if (state.dateTime != null &&
(state.reminderId?.isNotEmpty ?? false)) { (state.reminderId?.isNotEmpty ?? false)) {
final scheduledAt = option.fromDate(state.dateTime!);
// Update reminder // Update reminder
_reminderBloc.add( _reminderBloc.add(
ReminderEvent.update( ReminderEvent.update(
ReminderUpdate( ReminderUpdate(
id: state.reminderId!, 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({ const factory DateCellEditorEvent.setReminderOption({
required ReminderOption option, required ReminderOption option,
@Default(null) DateTime? selectedDay,
}) = _SetReminderOption; }) = _SetReminderOption;
const factory DateCellEditorEvent.removeReminder() = _RemoveReminder; const factory DateCellEditorEvent.removeReminder() = _RemoveReminder;
@ -483,8 +514,11 @@ class DateCellEditorState with _$DateCellEditorState {
final reminder = reminderBloc.state.reminders final reminder = reminderBloc.state.reminders
.firstWhereOrNull((r) => r.id == dateCellData.reminderId); .firstWhereOrNull((r) => r.id == dateCellData.reminderId);
if (reminder != null) { if (reminder != null) {
final eventDate = dateCellData.includeTime
? dateCellData.dateTime!
: dateCellData.dateTime!.withoutTime;
reminderOption = ReminderOption.fromDateDifference( reminderOption = ReminderOption.fromDateDifference(
dateCellData.dateTime!, eventDate,
reminder.scheduledAt.toDateTime(), reminder.scheduledAt.toDateTime(),
); );
} }

View File

@ -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/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.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'; 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/plugins/database/application/cell/bloc/date_cell_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileGridDateCellSkin extends IEditableDateCellSkin { class MobileGridDateCellSkin extends IEditableDateCellSkin {
@override @override
@ -25,9 +27,17 @@ class MobileGridDateCellSkin extends IEditableDateCellSkin {
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: FlowyText( child: Row(
state.dateStr, children: [
fontSize: 15, if (state.data?.reminderId.isNotEmpty ?? false) ...[
const FlowySvg(FlowySvgs.clock_alarm_s),
const HSpace(6),
],
FlowyText(
state.dateStr,
fontSize: 15,
),
],
), ),
), ),
), ),

View File

@ -67,8 +67,12 @@ class _DateCellEditor extends State<DateCellEditor> {
parseEndTimeError: state.parseEndTimeError, parseEndTimeError: state.parseEndTimeError,
parseTimeError: state.parseTimeError, parseTimeError: state.parseTimeError,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onReminderSelected: (option) => dateCellBloc onReminderSelected: (option) => dateCellBloc.add(
.add(DateCellEditorEvent.setReminderOption(option: option)), DateCellEditorEvent.setReminderOption(
option: option,
selectedDay: state.dateTime == null ? DateTime.now() : null,
),
),
selectedReminderOption: state.reminderOption, selectedReminderOption: state.reminderOption,
options: [ options: [
OptionGroup( OptionGroup(

View File

@ -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/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.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/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/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/mobile_date_header.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
@ -192,6 +193,7 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
UserTimeFormatPB.TwentyFourHour, UserTimeFormatPB.TwentyFourHour,
rebuildOnDaySelected: true, rebuildOnDaySelected: true,
rebuildOnTimeChanged: true, rebuildOnTimeChanged: true,
timeFormat: options.timeFormat.simplified,
selectedReminderOption: widget.reminderOption, selectedReminderOption: widget.reminderOption,
onDaySelected: options.onDaySelected, onDaySelected: options.onDaySelected,
onStartTimeChanged: (time) => options onStartTimeChanged: (time) => options
@ -342,7 +344,7 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
ReminderEvent.update( ReminderEvent.update(
ReminderUpdate( ReminderUpdate(
id: widget.reminderId!, id: widget.reminderId!,
scheduledAt: parsedDate!.subtract(reminderOption.time), scheduledAt: reminderOption.fromDate(parsedDate!),
), ),
), ),
); );

View File

@ -104,6 +104,9 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
: 0; : 0;
if (invalidCounter >= _invalidSearchesAmount) { if (invalidCounter >= _invalidSearchesAmount) {
// Workaround to bring focus back to editor
await widget.editorState
.updateSelectionWithReason(widget.editorState.selection);
return widget.onDismiss(); return widget.onDismiss();
} }
@ -192,7 +195,8 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
int get groupLength => results.length; 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) => InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) =>
results[groupIndex].results[handlerIndex]; results[groupIndex].results[handlerIndex];
@ -224,7 +228,21 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
widget.onDismiss(); widget.onDismiss();
return KeyEventResult.handled; 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) { } else if (event.logicalKey == LogicalKeyboardKey.escape) {
// Workaround to bring focus back to editor
widget.editorState
.updateSelectionWithReason(widget.editorState.selection);
widget.onDismiss(); widget.onDismiss();
} else if (event.logicalKey == LogicalKeyboardKey.backspace) { } else if (event.logicalKey == LogicalKeyboardKey.backspace) {
if (_search.isEmpty) { if (_search.isEmpty) {

View File

@ -31,7 +31,7 @@ class NotificationButton extends StatelessWidget {
child: AppFlowyPopover( child: AppFlowyPopover(
mutex: mutex, mutex: mutex,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 425), constraints: const BoxConstraints(maxHeight: 500, maxWidth: 425),
windowPadding: EdgeInsets.zero, windowPadding: EdgeInsets.zero,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
popupBuilder: (_) => popupBuilder: (_) =>

View File

@ -62,10 +62,24 @@ class _NotificationItemState extends State<NotificationItem> {
bool _isHovering = false; bool _isHovering = false;
int? path; int? path;
late final String infoString;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.block?.then((b) => path = b?.path.first); 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 @override
@ -135,10 +149,7 @@ class _NotificationItemState extends State<NotificationItem> {
), ),
// TODO(Xazin): Relative time // TODO(Xazin): Relative time
FlowyText.regular( FlowyText.regular(
'${_scheduledString( infoString,
widget.scheduled,
widget.includeTime,
)}${widget.view != null ? " - ${widget.view!.name}" : ""}',
fontSize: fontSize:
PlatformExtension.isMobile ? 12 : 10, PlatformExtension.isMobile ? 12 : 10,
), ),

View File

@ -135,6 +135,18 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
late DateTime? _selectedDay = widget.selectedDay; late DateTime? _selectedDay = widget.selectedDay;
late ReminderOption _selectedReminderOption = widget.selectedReminderOption; 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 @override
Widget build(BuildContext context) => Widget build(BuildContext context) =>
PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker(); PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker();
@ -222,6 +234,8 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
const _GroupSeparator(), const _GroupSeparator(),
ReminderSelector( ReminderSelector(
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
hasTime: widget.includeTime,
timeFormat: widget.timeFormat,
selectedOption: _selectedReminderOption, selectedOption: _selectedReminderOption,
onOptionSelected: (option) { onOptionSelected: (option) {
setState(() => _selectedReminderOption = option); setState(() => _selectedReminderOption = option);

View File

@ -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/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_mobile_option_decorate_box.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.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/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/mobile_date_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
@ -31,6 +33,7 @@ class MobileAppFlowyDatePicker extends StatefulWidget {
this.rebuildOnTimeChanged = false, this.rebuildOnTimeChanged = false,
required this.includeTime, required this.includeTime,
required this.use24hFormat, required this.use24hFormat,
required this.timeFormat,
this.selectedReminderOption, this.selectedReminderOption,
required this.onStartTimeChanged, required this.onStartTimeChanged,
this.onEndTimeChanged, this.onEndTimeChanged,
@ -59,6 +62,8 @@ class MobileAppFlowyDatePicker extends StatefulWidget {
final bool rebuildOnTimeChanged; final bool rebuildOnTimeChanged;
final bool use24hFormat; final bool use24hFormat;
final TimeFormatPB timeFormat;
final ReminderOption? selectedReminderOption; final ReminderOption? selectedReminderOption;
final Function(String? time) onStartTimeChanged; final Function(String? time) onStartTimeChanged;
@ -143,6 +148,8 @@ class _MobileAppFlowyDatePickerState extends State<MobileAppFlowyDatePicker> {
widget.onReminderSelected!.call(option); widget.onReminderSelected!.call(option);
setState(() => _reminderOption = option); setState(() => _reminderOption = option);
}, },
timeFormat: widget.timeFormat,
hasTime: widget.includeTime,
), ),
], ],
if (widget.onClearDate != null) ...[ if (widget.onClearDate != null) ...[
@ -166,10 +173,14 @@ class _ReminderSelector extends StatelessWidget {
const _ReminderSelector({ const _ReminderSelector({
this.selectedReminderOption, this.selectedReminderOption,
required this.onReminderSelected, required this.onReminderSelected,
required this.timeFormat,
this.hasTime = false,
}); });
final ReminderOption? selectedReminderOption; final ReminderOption? selectedReminderOption;
final OnReminderSelected onReminderSelected; final OnReminderSelected onReminderSelected;
final TimeFormatPB timeFormat;
final bool hasTime;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -180,8 +191,12 @@ class _ReminderSelector extends StatelessWidget {
availableOptions.remove(ReminderOption.custom); availableOptions.remove(ReminderOption.custom);
} }
availableOptions.removeWhere(
(o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime),
);
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: 'Reminder', text: LocaleKeys.datePicker_reminderLabel.tr(),
trailing: Row( trailing: Row(
children: [ children: [
const HSpace(6.0), const HSpace(6.0),
@ -200,39 +215,51 @@ class _ReminderSelector extends StatelessWidget {
onTap: () => showMobileBottomSheet( onTap: () => showMobileBottomSheet(
context, context,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
builder: (context) { builder: (_) => DraggableScrollableSheet(
return DraggableScrollableSheet( expand: false,
expand: false, snap: true,
snap: true, initialChildSize: 0.7,
initialChildSize: 0.7, minChildSize: 0.7,
minChildSize: 0.7, builder: (context, controller) => Column(
builder: (context, controller) => Column( children: [
children: [ ColoredBox(
const _ReminderSelectHeader(), color: Theme.of(context).colorScheme.surface,
const VSpace(12.0), child: const Center(child: DragHandler()),
Flexible( ),
child: SingleChildScrollView( const _ReminderSelectHeader(),
controller: controller, Flexible(
child: Column( child: SingleChildScrollView(
children: availableOptions controller: controller,
.map( child: Column(
(o) => FlowyOptionTile.text( children: availableOptions.map<Widget>(
text: o.label, (o) {
showTopBorder: o == ReminderOption.none, String label = o.label;
onTap: () { if (o.withoutTime && !o.timeExempt) {
onReminderSelected(o); const time = "09:00";
context.pop(); final t = timeFormat == TimeFormatPB.TwelveHour
}, ? "$time AM"
), : time;
)
.toList(), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Container(
height: 56, height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -252,8 +287,8 @@ class _ReminderSelectHeader extends StatelessWidget {
width: 120, width: 120,
child: AppBarCancelButton(onTap: context.pop), child: AppBarCancelButton(onTap: context.pop),
), ),
const FlowyText.medium( FlowyText.medium(
'Select reminder', LocaleKeys.datePicker_selectReminder.tr(),
fontSize: 17.0, fontSize: 17.0,
), ),
const HSpace(120), const HSpace(120),

View File

@ -31,7 +31,6 @@ class DateTypeOptionButton extends StatelessWidget {
"${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}"; "${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}";
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(140, 100)), constraints: BoxConstraints.loose(const Size(140, 100)),

View File

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.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:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -15,11 +17,15 @@ class ReminderSelector extends StatelessWidget {
required this.mutex, required this.mutex,
required this.selectedOption, required this.selectedOption,
required this.onOptionSelected, required this.onOptionSelected,
required this.timeFormat,
this.hasTime = false,
}); });
final PopoverMutex? mutex; final PopoverMutex? mutex;
final ReminderOption selectedOption; final ReminderOption selectedOption;
final OnReminderSelected? onOptionSelected; final OnReminderSelected? onOptionSelected;
final TimeFormatPB timeFormat;
final bool hasTime;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,39 +34,53 @@ class ReminderSelector extends StatelessWidget {
options.remove(ReminderOption.custom); options.remove(ReminderOption.custom);
} }
final optionWidgets = options options.removeWhere(
.map( (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime),
(o) => SizedBox( );
height: DatePickerSize.itemHeight,
child: FlowyButton( final optionWidgets = options.map(
text: FlowyText.medium(o.label), (o) {
rightIcon: o == selectedOption String label = o.label;
? const FlowySvg(FlowySvgs.check_s) if (o.withoutTime && !o.timeExempt) {
: null, const time = "09:00";
onTap: () { final t = timeFormat == TimeFormatPB.TwelveHour ? "$time AM" : time;
if (o != selectedOption) {
onOptionSelected?.call(o); label = "$label ($t)";
mutex?.close(); }
}
}, 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( return AppFlowyPopover(
mutex: mutex, mutex: mutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(8, 0),
offset: const Offset(8, -155),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(150, 310)), constraints: const BoxConstraints(maxHeight: 400, maxWidth: 205),
popupBuilder: (_) => Padding( popupBuilder: (_) => Column(
padding: const EdgeInsets.all(6.0), mainAxisSize: MainAxisSize.min,
child: ListView.separated( children: [
itemCount: options.length, Padding(
separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight), padding: const EdgeInsets.all(6.0),
itemBuilder: (_, index) => optionWidgets[index], child: SeparatedColumn(
), children: optionWidgets,
separatorBuilder: () => VSpace(DatePickerSize.seperatorHeight),
),
),
],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
@ -90,13 +110,29 @@ enum ReminderOption {
thirtyMinsBefore(time: Duration(minutes: 30)), thirtyMinsBefore(time: Duration(minutes: 30)),
oneHourBefore(time: Duration(hours: 1)), oneHourBefore(time: Duration(hours: 1)),
twoHoursBefore(time: Duration(hours: 2)), twoHoursBefore(time: Duration(hours: 2)),
oneDayBefore(time: Duration(days: 1)), onDayOfEvent(
twoDaysBefore(time: Duration(days: 2)), 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()); custom(time: Duration());
const ReminderOption({required this.time}); const ReminderOption({
required this.time,
this.withoutTime = false,
this.requiresNoTime = false,
});
final Duration time; final Duration time;
final bool withoutTime;
final bool requiresNoTime;
bool get timeExempt =>
[ReminderOption.none, ReminderOption.custom].contains(this);
String get label => switch (this) { String get label => switch (this) {
ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(), ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(),
@ -114,10 +150,14 @@ enum ReminderOption {
LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(), LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(),
ReminderOption.twoHoursBefore => ReminderOption.twoHoursBefore =>
LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(), LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(),
ReminderOption.onDayOfEvent =>
LocaleKeys.datePicker_reminderOptions_onDayOfEvent.tr(),
ReminderOption.oneDayBefore => ReminderOption.oneDayBefore =>
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
ReminderOption.twoDaysBefore => ReminderOption.twoDaysBefore =>
LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(), LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(),
ReminderOption.oneWeekBefore =>
LocaleKeys.datePicker_reminderOptions_oneWeekBefore.tr(),
ReminderOption.custom => ReminderOption.custom =>
LocaleKeys.datePicker_reminderOptions_custom.tr(), LocaleKeys.datePicker_reminderOptions_custom.tr(),
}; };
@ -125,8 +165,15 @@ enum ReminderOption {
static ReminderOption fromDateDifference( static ReminderOption fromDateDifference(
DateTime eventDate, DateTime eventDate,
DateTime reminderDate, 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) { static ReminderOption fromMinutes(int minutes) => switch (minutes) {
0 => ReminderOption.atTimeOfEvent, 0 => ReminderOption.atTimeOfEvent,
@ -136,8 +183,18 @@ enum ReminderOption {
30 => ReminderOption.thirtyMinsBefore, 30 => ReminderOption.thirtyMinsBefore,
60 => ReminderOption.oneHourBefore, 60 => ReminderOption.oneHourBefore,
120 => ReminderOption.twoHoursBefore, 120 => ReminderOption.twoHoursBefore,
1440 => ReminderOption.oneDayBefore, // Negative because Event Day Today + 940 minutes
2880 => ReminderOption.twoDaysBefore, -540 => ReminderOption.onDayOfEvent,
900 => ReminderOption.oneDayBefore,
2340 => ReminderOption.twoDaysBefore,
9540 => ReminderOption.oneWeekBefore,
_ => ReminderOption.custom, _ => ReminderOption.custom,
}; };
DateTime fromDate(DateTime date) => switch (withoutTime) {
true => requiresNoTime
? date.withoutTime.add(time)
: date.withoutTime.subtract(time),
_ => date.subtract(time),
};
} }

View File

@ -235,16 +235,24 @@ class PopoverState extends State<Popover> {
switch (widget.clickHandler) { switch (widget.clickHandler) {
case PopoverClickHandler.listener: case PopoverClickHandler.listener:
return Listener( return Listener(
onPointerDown: (_) => handler(), onPointerDown: (_) => _callHandler(handler),
child: child, child: child,
); );
case PopoverClickHandler.gestureDetector: case PopoverClickHandler.gestureDetector:
return GestureDetector( return GestureDetector(
onTap: handler, onTap: () => _callHandler(handler),
child: child, child: child,
); );
} }
} }
void _callHandler(VoidCallback handler) {
if (_rootEntry.contains(this)) {
close();
} else {
handler();
}
}
} }
class PopoverContainer extends StatefulWidget { class PopoverContainer extends StatefulWidget {

View File

@ -1037,6 +1037,7 @@
"timeFormat": "Time format", "timeFormat": "Time format",
"clearDate": "Clear date", "clearDate": "Clear date",
"reminderLabel": "Reminder", "reminderLabel": "Reminder",
"selectReminder": "Select reminder",
"reminderOptions": { "reminderOptions": {
"none": "None", "none": "None",
"atTimeOfEvent": "Time of event", "atTimeOfEvent": "Time of event",
@ -1046,8 +1047,10 @@
"thirtyMinsBefore": "30 mins before", "thirtyMinsBefore": "30 mins before",
"oneHourBefore": "1 hour before", "oneHourBefore": "1 hour before",
"twoHoursBefore": "2 hours before", "twoHoursBefore": "2 hours before",
"onDayOfEvent": "On day of event",
"oneDayBefore": "1 day before", "oneDayBefore": "1 day before",
"twoDaysBefore": "2 days before", "twoDaysBefore": "2 days before",
"oneWeekBefore": "1 week before",
"custom": "Custom" "custom": "Custom"
} }
}, },
@ -1247,4 +1250,4 @@
"userIcon": "User icon" "userIcon": "User icon"
}, },
"noLogFiles": "There're no log files" "noLogFiles": "There're no log files"
} }