fix: reminder launch review (#3716)

This commit is contained in:
Mathias Mogensen 2023-10-17 08:48:58 +02:00 committed by GitHub
parent 3647af44c9
commit 966547faa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 524 additions and 571 deletions

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';

View File

@ -14,7 +14,7 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart'; import 'package:appflowy/workspace/application/notifications/notification_service.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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