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);
// 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);
},
);
});
}

View File

@ -322,20 +322,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> 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<void> selectLastDateInPicker() async {
Future<bool> selectLastDateInPicker() async {
final finder = find.byType(CellContent).last;
final w = widget(finder) as CellContent;
await tapButton(finder);
return w.isToday;
}
Future<void> toggleDateRange() async {

View File

@ -191,4 +191,4 @@ SPEC CHECKSUMS:
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,
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<MobileDateCellEditScreen> {
onClearDate: () => context
.read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()),
onReminderSelected: (option) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setReminderOption(option: option)),
onReminderSelected: (option) =>
context.read<DateCellEditorBloc>().add(
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-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(),
);
}

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/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,
),
],
),
),
),

View File

@ -67,8 +67,12 @@ class _DateCellEditor extends State<DateCellEditor> {
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(

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/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<MentionDateBlock> {
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<MentionDateBlock> {
ReminderEvent.update(
ReminderUpdate(
id: widget.reminderId!,
scheduledAt: parsedDate!.subtract(reminderOption.time),
scheduledAt: reminderOption.fromDate(parsedDate!),
),
),
);

View File

@ -104,6 +104,9 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
: 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<InlineActionsHandler> {
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<InlineActionsHandler> {
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) {

View File

@ -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: (_) =>

View File

@ -62,10 +62,24 @@ class _NotificationItemState extends State<NotificationItem> {
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<NotificationItem> {
),
// TODO(Xazin): Relative time
FlowyText.regular(
'${_scheduledString(
widget.scheduled,
widget.includeTime,
)}${widget.view != null ? " - ${widget.view!.name}" : ""}',
infoString,
fontSize:
PlatformExtension.isMobile ? 12 : 10,
),

View File

@ -135,6 +135,18 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
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<AppFlowyDatePicker> {
const _GroupSeparator(),
ReminderSelector(
mutex: widget.popoverMutex,
hasTime: widget.includeTime,
timeFormat: widget.timeFormat,
selectedOption: _selectedReminderOption,
onOptionSelected: (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/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<MobileAppFlowyDatePicker> {
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<Widget>(
(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),

View File

@ -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)),

View File

@ -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),
};
}

View File

@ -235,16 +235,24 @@ class PopoverState extends State<Popover> {
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 {

View File

@ -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"
}
}