mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: reminder improvements (#3658)
This commit is contained in:
@ -67,7 +67,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
|||||||
with GridCellAccessoryState {
|
with GridCellAccessoryState {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 26,
|
width: 26,
|
||||||
|
@ -21,7 +21,7 @@ class BlockActionButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
preferBelow: false,
|
preferBelow: false,
|
||||||
richMessage: richMessage,
|
richMessage: richMessage,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
|
@ -156,7 +156,7 @@ class _AlignButton extends StatelessWidget {
|
|||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
message: tooltips,
|
message: tooltips,
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
icon,
|
icon,
|
||||||
|
@ -225,11 +225,9 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
future: items,
|
future: items,
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,7 @@ class DocumentMoreButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
FlowySvgs.details_s,
|
FlowySvgs.details_s,
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'notification_filter_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class NotificationFilterBloc
|
||||||
|
extends Bloc<NotificationFilterEvent, NotificationFilterState> {
|
||||||
|
NotificationFilterBloc() : super(const NotificationFilterState()) {
|
||||||
|
on<NotificationFilterEvent>((event, emit) async {
|
||||||
|
event.when(
|
||||||
|
reset: () => emit(const NotificationFilterState()),
|
||||||
|
changeSortBy: (NotificationSortOption sortBy) => emit(
|
||||||
|
state.copyWith(sortBy: sortBy),
|
||||||
|
),
|
||||||
|
toggleGroupByDate: () => emit(
|
||||||
|
state.copyWith(groupByDate: !state.groupByDate),
|
||||||
|
),
|
||||||
|
toggleShowUnreadsOnly: () => emit(
|
||||||
|
state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationSortOption {
|
||||||
|
descending,
|
||||||
|
ascending,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class NotificationFilterEvent with _$NotificationFilterEvent {
|
||||||
|
const factory NotificationFilterEvent.toggleShowUnreadsOnly() =
|
||||||
|
_ToggleShowUnreadsOnly;
|
||||||
|
|
||||||
|
const factory NotificationFilterEvent.toggleGroupByDate() =
|
||||||
|
_ToggleGroupByDate;
|
||||||
|
|
||||||
|
const factory NotificationFilterEvent.changeSortBy(
|
||||||
|
NotificationSortOption sortBy,
|
||||||
|
) = _ChangeSortBy;
|
||||||
|
|
||||||
|
const factory NotificationFilterEvent.reset() = _Reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class NotificationFilterState extends Equatable with _$NotificationFilterState {
|
||||||
|
const NotificationFilterState._();
|
||||||
|
|
||||||
|
const factory NotificationFilterState({
|
||||||
|
@Default(false) bool showUnreadsOnly,
|
||||||
|
@Default(false) bool groupByDate,
|
||||||
|
@Default(NotificationSortOption.descending) NotificationSortOption sortBy,
|
||||||
|
}) = _NotificationFilterState;
|
||||||
|
|
||||||
|
// If state is not default values, then there are custom changes
|
||||||
|
bool get hasFilters =>
|
||||||
|
showUnreadsOnly != false ||
|
||||||
|
groupByDate != false ||
|
||||||
|
sortBy != NotificationSortOption.descending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [showUnreadsOnly, groupByDate, sortBy];
|
||||||
|
}
|
@ -34,19 +34,19 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
|
|
||||||
remindersOrFailure.fold(
|
remindersOrFailure.fold(
|
||||||
(error) => Log.error(error),
|
(error) => Log.error(error),
|
||||||
(reminders) => _updateState(emit, reminders),
|
(reminders) => emit(state.copyWith(reminders: reminders)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
remove: (reminderId) async {
|
remove: (reminder) async {
|
||||||
final unitOrFailure =
|
final unitOrFailure =
|
||||||
await reminderService.removeReminder(reminderId: reminderId);
|
await reminderService.removeReminder(reminderId: reminder.id);
|
||||||
|
|
||||||
unitOrFailure.fold(
|
unitOrFailure.fold(
|
||||||
(error) => Log.error(error),
|
(error) => Log.error(error),
|
||||||
(_) {
|
(_) {
|
||||||
final reminders = [...state.reminders];
|
final reminders = [...state.reminders];
|
||||||
reminders.removeWhere((e) => e.id == reminderId);
|
reminders.removeWhere((e) => e.id == reminder.id);
|
||||||
_updateState(emit, reminders);
|
emit(state.copyWith(reminders: reminders));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -57,8 +57,8 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
return unitOrFailure.fold(
|
return unitOrFailure.fold(
|
||||||
(error) => Log.error(error),
|
(error) => Log.error(error),
|
||||||
(_) {
|
(_) {
|
||||||
state.reminders.add(reminder);
|
final reminders = [...state.reminders, reminder];
|
||||||
_updateState(emit, state.reminders);
|
emit(state.copyWith(reminders: reminders));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -82,7 +82,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
state.reminders.indexWhere((r) => r.id == reminder.id);
|
state.reminders.indexWhere((r) => r.id == reminder.id);
|
||||||
final reminders = [...state.reminders];
|
final reminders = [...state.reminders];
|
||||||
reminders.replaceRange(index, index + 1, [newReminder]);
|
reminders.replaceRange(index, index + 1, [newReminder]);
|
||||||
_updateState(emit, reminders);
|
emit(state.copyWith(reminders: reminders));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -108,23 +108,13 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateState(Emitter emit, List<ReminderPB> reminders) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final hasUnreads = reminders.any(
|
|
||||||
(r) =>
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(r.scheduledAt.toInt() * 1000)
|
|
||||||
.isBefore(now) &&
|
|
||||||
!r.isRead,
|
|
||||||
);
|
|
||||||
emit(state.copyWith(reminders: reminders, hasUnreads: hasUnreads));
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer _periodicCheck() {
|
Timer _periodicCheck() {
|
||||||
return Timer.periodic(
|
return Timer.periodic(
|
||||||
const Duration(minutes: 1),
|
const Duration(minutes: 1),
|
||||||
(_) {
|
(_) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
for (final reminder in state.reminders) {
|
|
||||||
|
for (final reminder in state.upcomingReminders) {
|
||||||
if (reminder.isAck) {
|
if (reminder.isAck) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -163,7 +153,7 @@ class ReminderEvent with _$ReminderEvent {
|
|||||||
const factory ReminderEvent.started() = _Started;
|
const factory ReminderEvent.started() = _Started;
|
||||||
|
|
||||||
// Remove a reminder
|
// Remove a reminder
|
||||||
const factory ReminderEvent.remove({required String reminderId}) = _Remove;
|
const factory ReminderEvent.remove({required ReminderPB reminder}) = _Remove;
|
||||||
|
|
||||||
// Add a reminder
|
// Add a reminder
|
||||||
const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
|
const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
|
||||||
@ -212,21 +202,46 @@ class ReminderUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReminderState {
|
class ReminderState {
|
||||||
ReminderState({
|
ReminderState({List<ReminderPB>? reminders}) {
|
||||||
List<ReminderPB>? reminders,
|
_reminders = reminders ?? [];
|
||||||
bool? hasUnreads,
|
|
||||||
}) : reminders = reminders ?? [],
|
|
||||||
hasUnreads = hasUnreads ?? false;
|
|
||||||
|
|
||||||
final List<ReminderPB> reminders;
|
pastReminders = [];
|
||||||
final bool hasUnreads;
|
upcomingReminders = [];
|
||||||
|
|
||||||
ReminderState copyWith({
|
if (_reminders.isEmpty) {
|
||||||
List<ReminderPB>? reminders,
|
hasUnreads = false;
|
||||||
bool? hasUnreads,
|
return;
|
||||||
}) =>
|
}
|
||||||
ReminderState(
|
|
||||||
reminders: reminders ?? this.reminders,
|
final now = DateTime.now();
|
||||||
hasUnreads: hasUnreads ?? this.hasUnreads,
|
|
||||||
);
|
bool hasUnreadReminders = false;
|
||||||
|
for (final reminder in _reminders) {
|
||||||
|
final scheduledDate = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
reminder.scheduledAt.toInt() * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scheduledDate.isBefore(now)) {
|
||||||
|
pastReminders.add(reminder);
|
||||||
|
|
||||||
|
if (!hasUnreadReminders && !reminder.isRead) {
|
||||||
|
hasUnreadReminders = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
upcomingReminders.add(reminder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUnreads = hasUnreadReminders;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final List<ReminderPB> _reminders;
|
||||||
|
List<ReminderPB> get reminders => _reminders;
|
||||||
|
|
||||||
|
late final List<ReminderPB> pastReminders;
|
||||||
|
late final List<ReminderPB> upcomingReminders;
|
||||||
|
late final bool hasUnreads;
|
||||||
|
|
||||||
|
ReminderState copyWith({List<ReminderPB>? reminders}) =>
|
||||||
|
ReminderState(reminders: reminders ?? _reminders);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ class SidebarTopMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
richMessage: textSpan,
|
richMessage: textSpan,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
width: 28,
|
width: 28,
|
||||||
|
@ -66,7 +66,7 @@ class SidebarUser extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
|
Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
|
||||||
final userProfile = state.userProfile;
|
final userProfile = state.userProfile;
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.settings_menu_open.tr(),
|
message: LocaleKeys.settings_menu_open.tr(),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -339,7 +339,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
// + button
|
// + button
|
||||||
Widget _buildViewAddButton(BuildContext context) {
|
Widget _buildViewAddButton(BuildContext context) {
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
|
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
|
||||||
child: ViewAddButton(
|
child: ViewAddButton(
|
||||||
parentViewId: widget.view.id,
|
parentViewId: widget.view.id,
|
||||||
@ -379,7 +379,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
// ··· more action button
|
// ··· more action button
|
||||||
Widget _buildViewMoreActionButton(BuildContext context) {
|
Widget _buildViewMoreActionButton(BuildContext context) {
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
||||||
child: ViewMoreActionButton(
|
child: ViewMoreActionButton(
|
||||||
view: widget.view,
|
view: widget.view,
|
||||||
|
@ -66,7 +66,7 @@ class FlowyNavigation extends StatelessWidget {
|
|||||||
if (state.isMenuCollapsed) {
|
if (state.isMenuCollapsed) {
|
||||||
return RotationTransition(
|
return RotationTransition(
|
||||||
turns: const AlwaysStoppedAnimation(180 / 360),
|
turns: const AlwaysStoppedAnimation(180 / 360),
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
richMessage: sidebarTooltipTextSpan(
|
richMessage: sidebarTooltipTextSpan(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.sideBar_openSidebar.tr(),
|
LocaleKeys.sideBar_openSidebar.tr(),
|
||||||
|
@ -24,14 +24,14 @@ class NotificationButton extends StatelessWidget {
|
|||||||
return BlocProvider<ReminderBloc>.value(
|
return BlocProvider<ReminderBloc>.value(
|
||||||
value: getIt<ReminderBloc>(),
|
value: getIt<ReminderBloc>(),
|
||||||
child: BlocBuilder<ReminderBloc, ReminderState>(
|
child: BlocBuilder<ReminderBloc, ReminderState>(
|
||||||
builder: (context, state) => FlowyTooltip.delayed(
|
builder: (context, state) => FlowyTooltip(
|
||||||
message: LocaleKeys.notificationHub_title.tr(),
|
message: LocaleKeys.notificationHub_title.tr(),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: AppFlowyPopover(
|
child: AppFlowyPopover(
|
||||||
mutex: mutex,
|
mutex: mutex,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 300),
|
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 350),
|
||||||
popupBuilder: (_) =>
|
popupBuilder: (_) =>
|
||||||
NotificationDialog(views: views, mutex: mutex),
|
NotificationDialog(views: views, mutex: mutex),
|
||||||
child: _buildNotificationIcon(context, state.hasUnreads),
|
child: _buildNotificationIcon(context, state.hasUnreads),
|
||||||
|
@ -1,25 +1,35 @@
|
|||||||
|
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/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/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/notification_grouped_view.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/notification_view.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||||
import 'package:appflowy_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_ui/style_widget/text.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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
extension _ReminderReady on ReminderPB {
|
extension _ReminderSort on Iterable<ReminderPB> {
|
||||||
DateTime get scheduledDate =>
|
List<ReminderPB> sortByScheduledAt({
|
||||||
DateTime.fromMillisecondsSinceEpoch(scheduledAt.toInt() * 1000);
|
bool isDescending = true,
|
||||||
|
}) =>
|
||||||
bool isBefore(DateTime date) => scheduledDate.isBefore(date);
|
sorted(
|
||||||
|
(a, b) => isDescending
|
||||||
|
? b.scheduledAt.compareTo(a.scheduledAt)
|
||||||
|
: a.scheduledAt.compareTo(b.scheduledAt),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationDialog extends StatelessWidget {
|
class NotificationDialog extends StatefulWidget {
|
||||||
const NotificationDialog({
|
const NotificationDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.views,
|
required this.views,
|
||||||
@ -29,26 +39,62 @@ class NotificationDialog extends StatelessWidget {
|
|||||||
final List<ViewPB> views;
|
final List<ViewPB> views;
|
||||||
final PopoverMutex mutex;
|
final PopoverMutex mutex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotificationDialog> createState() => _NotificationDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationDialogState extends State<NotificationDialog>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _controller = TabController(length: 2, vsync: this);
|
||||||
|
final PopoverMutex _mutex = PopoverMutex();
|
||||||
|
final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(_updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateState() => setState(() {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mutex.close();
|
||||||
|
_controller.removeListener(_updateState);
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final reminderBloc = getIt<ReminderBloc>();
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
return BlocProvider<ReminderBloc>.value(
|
BlocProvider<ReminderBloc>.value(value: _reminderBloc),
|
||||||
value: reminderBloc,
|
BlocProvider<NotificationFilterBloc>(
|
||||||
child: BlocBuilder<ReminderBloc, ReminderState>(
|
create: (_) => NotificationFilterBloc(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
||||||
|
builder: (context, filterState) =>
|
||||||
|
BlocBuilder<ReminderBloc, ReminderState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final shownReminders = state.reminders
|
final sortDescending =
|
||||||
.where((reminder) => reminder.isBefore(DateTime.now()))
|
filterState.sortBy == NotificationSortOption.descending;
|
||||||
.sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
final List<ReminderPB> pastReminders = state.pastReminders
|
||||||
child: Column(
|
.where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
|
||||||
|
.sortByScheduledAt(isDescending: sortDescending);
|
||||||
|
|
||||||
|
final List<ReminderPB> upcomingReminders = state.upcomingReminders
|
||||||
|
.sortByScheduledAt(isDescending: sortDescending);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
DecoratedBox(
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@ -56,48 +102,101 @@ class NotificationDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(
|
width: 215,
|
||||||
vertical: 4,
|
child: TabBar(
|
||||||
horizontal: 10,
|
controller: _controller,
|
||||||
),
|
indicator: UnderlineTabIndicator(
|
||||||
child: FlowyText.semibold(
|
borderRadius: BorderRadius.circular(4),
|
||||||
LocaleKeys.notificationHub_title.tr(),
|
borderSide: BorderSide(
|
||||||
fontSize: 16,
|
width: 1,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
height: 26,
|
||||||
|
child: FlowyText.regular(
|
||||||
|
LocaleKeys.notificationHub_tabs_inbox.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
height: 26,
|
||||||
|
child: FlowyText.regular(
|
||||||
|
LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
NotificationViewFilters(),
|
||||||
|
],
|
||||||
|
),
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
if (shownReminders.isEmpty)
|
// TODO(Xazin): Resolve issue with taking up
|
||||||
Padding(
|
// max amount of vertical space
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
Expanded(
|
||||||
child: Center(
|
child: TabBarView(
|
||||||
child: FlowyText.regular(
|
controller: _controller,
|
||||||
LocaleKeys.notificationHub_empty.tr(),
|
children: [
|
||||||
|
if (!filterState.groupByDate) ...[
|
||||||
|
NotificationsView(
|
||||||
|
shownReminders: pastReminders,
|
||||||
|
reminderBloc: _reminderBloc,
|
||||||
|
views: widget.views,
|
||||||
|
onDelete: _onDelete,
|
||||||
|
onAction: _onAction,
|
||||||
|
onReadChanged: _onReadChanged,
|
||||||
|
),
|
||||||
|
NotificationsView(
|
||||||
|
shownReminders: upcomingReminders,
|
||||||
|
reminderBloc: _reminderBloc,
|
||||||
|
views: widget.views,
|
||||||
|
isUpcoming: true,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
else
|
);
|
||||||
...shownReminders.map((reminder) {
|
},
|
||||||
return NotificationItem(
|
|
||||||
reminderId: reminder.id,
|
|
||||||
key: ValueKey(reminder.id),
|
|
||||||
title: reminder.title,
|
|
||||||
scheduled: reminder.scheduledAt,
|
|
||||||
body: reminder.message,
|
|
||||||
isRead: reminder.isRead,
|
|
||||||
onReadChanged: (isRead) => reminderBloc.add(
|
|
||||||
ReminderEvent.update(
|
|
||||||
ReminderUpdate(id: reminder.id, isRead: isRead),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onDelete: () => reminderBloc
|
);
|
||||||
.add(ReminderEvent.remove(reminderId: reminder.id)),
|
}
|
||||||
onAction: () {
|
|
||||||
final view = views.firstWhereOrNull(
|
void _onAction(ReminderPB reminder) {
|
||||||
|
final view = widget.views.firstWhereOrNull(
|
||||||
(view) => view.id == reminder.objectId,
|
(view) => view.id == reminder.objectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -105,16 +204,238 @@ class NotificationDialog extends StatelessWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reminderBloc.add(
|
_reminderBloc.add(
|
||||||
ReminderEvent.pressReminder(reminderId: reminder.id),
|
ReminderEvent.pressReminder(reminderId: reminder.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
mutex.close();
|
widget.mutex.close();
|
||||||
},
|
}
|
||||||
|
|
||||||
|
void _onDelete(ReminderPB reminder) {
|
||||||
|
_reminderBloc.add(ReminderEvent.remove(reminder: reminder));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReadChanged(ReminderPB reminder, bool isRead) {
|
||||||
|
_reminderBloc.add(
|
||||||
|
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationViewFilters extends StatelessWidget {
|
||||||
|
NotificationViewFilters({super.key});
|
||||||
|
final PopoverMutex _mutex = PopoverMutex();
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_SortByOption(bloc: bloc),
|
||||||
|
_ShowUnreadsToggle(bloc: bloc),
|
||||||
|
_GroupByDateToggle(bloc: bloc),
|
||||||
|
BlocProvider<NotificationFilterBloc>.value(
|
||||||
|
value: bloc,
|
||||||
|
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
value: state.showUnreadsOnly,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GroupByDateToggle extends StatelessWidget {
|
||||||
|
const _GroupByDateToggle({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_groupByDate.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Toggle(
|
||||||
|
style: ToggleStyle.big,
|
||||||
|
onChanged: (value) =>
|
||||||
|
bloc.add(const NotificationFilterEvent.toggleGroupByDate()),
|
||||||
|
value: state.groupByDate,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortByOption extends StatefulWidget {
|
||||||
|
const _SortByOption({required this.bloc});
|
||||||
|
|
||||||
|
final NotificationFilterBloc bloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SortByOption> createState() => _SortByOptionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortByOptionState extends State<_SortByOption> {
|
||||||
|
bool _isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<NotificationFilterBloc>.value(
|
||||||
|
value: widget.bloc,
|
||||||
|
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(
|
||||||
|
resetHoverOnRebuild: false,
|
||||||
|
child: FlowyButton(
|
||||||
|
onHover: (isHovering) => isHovering != _isHovering
|
||||||
|
? setState(() => _isHovering = isHovering)
|
||||||
|
: null,
|
||||||
|
onTap: () => widget.bloc.add(
|
||||||
|
NotificationFilterEvent.changeSortBy(
|
||||||
|
isSortDescending
|
||||||
|
? NotificationSortOption.ascending
|
||||||
|
: NotificationSortOption.descending,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
isSortDescending
|
||||||
|
? FlowySvgs.sort_descending_s
|
||||||
|
: FlowySvgs.sort_ascending_s,
|
||||||
|
color: _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(
|
||||||
|
isSortDescending
|
||||||
|
? LocaleKeys.notificationHub_filters_descending.tr()
|
||||||
|
: LocaleKeys.notificationHub_filters_ascending.tr(),
|
||||||
|
color: _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: Theme.of(context).textTheme.bodyMedium?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/appearance.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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
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/application/appearance.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.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:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flowy_infra/theme_extension.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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
DateFormat _dateFormat(BuildContext context) => DateFormat('MMM d, y');
|
|
||||||
|
|
||||||
class NotificationItem extends StatefulWidget {
|
class NotificationItem extends StatefulWidget {
|
||||||
const NotificationItem({
|
const NotificationItem({
|
||||||
@ -17,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.readOnly = false,
|
||||||
this.onAction,
|
this.onAction,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onReadChanged,
|
this.onReadChanged,
|
||||||
@ -27,6 +29,7 @@ class NotificationItem extends StatefulWidget {
|
|||||||
final Int64 scheduled;
|
final Int64 scheduled;
|
||||||
final String body;
|
final String body;
|
||||||
final bool isRead;
|
final bool isRead;
|
||||||
|
final bool readOnly;
|
||||||
|
|
||||||
final VoidCallback? onAction;
|
final VoidCallback? onAction;
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
@ -53,7 +56,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: widget.onAction,
|
onTap: widget.onAction,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: widget.isRead ? 0.5 : 1,
|
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -68,7 +71,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
|
const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
|
||||||
if (!widget.isRead)
|
if (!widget.isRead && !widget.readOnly)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 1,
|
bottom: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
@ -89,11 +92,12 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
FlowyText.semibold(
|
||||||
child: FlowyText.semibold(widget.title),
|
widget.title,
|
||||||
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
|
const HSpace(8),
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
_scheduledString(widget.scheduled),
|
_scheduledString(widget.scheduled),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@ -110,7 +114,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isHovering)
|
if (_isHovering && !widget.readOnly)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 4,
|
right: 4,
|
||||||
top: 4,
|
top: 4,
|
||||||
@ -125,9 +129,13 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _scheduledString(Int64 secondsSinceEpoch) =>
|
String _scheduledString(Int64 secondsSinceEpoch) => context
|
||||||
_dateFormat(context).format(
|
.read<AppearanceSettingsCubit>()
|
||||||
|
.state
|
||||||
|
.dateFormat
|
||||||
|
.formatDate(
|
||||||
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
|
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/notifications_hub_empty.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NotificationsView extends StatelessWidget {
|
||||||
|
const NotificationsView({
|
||||||
|
super.key,
|
||||||
|
required this.shownReminders,
|
||||||
|
required this.reminderBloc,
|
||||||
|
required this.views,
|
||||||
|
this.isUpcoming = false,
|
||||||
|
this.onAction,
|
||||||
|
this.onDelete,
|
||||||
|
this.onReadChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<ReminderPB> shownReminders;
|
||||||
|
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 (shownReminders.isEmpty) {
|
||||||
|
return const Center(child: NotificationsHubEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...shownReminders.map(
|
||||||
|
(reminder) {
|
||||||
|
return 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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -164,7 +164,7 @@ class _ChangeStoragePathButton extends StatefulWidget {
|
|||||||
class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> {
|
class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyTooltip.delayed(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
message: LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
||||||
child: SecondaryTextButton(
|
child: SecondaryTextButton(
|
||||||
LocaleKeys.settings_files_change.tr(),
|
LocaleKeys.settings_files_change.tr(),
|
||||||
|
@ -239,7 +239,7 @@ class SettingsUserView extends StatelessWidget {
|
|||||||
required bool hasIcon,
|
required bool hasIcon,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) =>
|
}) =>
|
||||||
FlowyTooltip.delayed(
|
FlowyTooltip(
|
||||||
message: LocaleKeys.settings_user_tooltipSelectIcon.tr(),
|
message: LocaleKeys.settings_user_tooltipSelectIcon.tr(),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -106,7 +106,7 @@ class EnableEncrypt extends StatelessWidget {
|
|||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.settings_menu_clickToCopySecret.tr(),
|
message: LocaleKeys.settings_menu_clickToCopySecret.tr(),
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
disable: !(state.config.enableEncrypt),
|
disable: !(state.config.enableEncrypt),
|
||||||
|
@ -195,7 +195,7 @@ class FlowyTextButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (tooltip != null) {
|
if (tooltip != null) {
|
||||||
child = FlowyTooltip.delayed(
|
child = FlowyTooltip(
|
||||||
message: tooltip!,
|
message: tooltip!,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@ -284,7 +284,7 @@ class FlowyRichTextButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (tooltip != null) {
|
if (tooltip != null) {
|
||||||
child = FlowyTooltip.delayed(
|
child = FlowyTooltip(
|
||||||
message: tooltip!,
|
message: tooltip!,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
@ -83,7 +83,7 @@ class _FlowyHoverState extends State<FlowyHover> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget renderWidget() {
|
Widget renderWidget() {
|
||||||
var showHover = _onHover;
|
bool showHover = _onHover;
|
||||||
if (!showHover && widget.isSelected != null) {
|
if (!showHover && widget.isSelected != null) {
|
||||||
showHover = widget.isSelected!();
|
showHover = widget.isSelected!();
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class FlowyIconButton extends StatelessWidget {
|
|||||||
height: size.height,
|
height: size.height,
|
||||||
),
|
),
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
child: FlowyTooltip.delayed(
|
child: FlowyTooltip(
|
||||||
preferBelow: preferBelow,
|
preferBelow: preferBelow,
|
||||||
message: tooltipMessage,
|
message: tooltipMessage,
|
||||||
richMessage: richTooltipText,
|
richMessage: richTooltipText,
|
||||||
|
@ -2,15 +2,26 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
const _tooltipWaitDuration = Duration(milliseconds: 300);
|
const _tooltipWaitDuration = Duration(milliseconds: 300);
|
||||||
|
|
||||||
class FlowyTooltip {
|
class FlowyTooltip extends StatelessWidget {
|
||||||
static Tooltip delayed({
|
const FlowyTooltip({
|
||||||
String? message,
|
super.key,
|
||||||
InlineSpan? richMessage,
|
this.message,
|
||||||
bool? preferBelow,
|
this.richMessage,
|
||||||
Duration? showDuration,
|
this.preferBelow,
|
||||||
Widget? child,
|
this.showDuration,
|
||||||
EdgeInsetsGeometry? margin,
|
this.margin,
|
||||||
}) {
|
this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? message;
|
||||||
|
final InlineSpan? richMessage;
|
||||||
|
final bool? preferBelow;
|
||||||
|
final Duration? showDuration;
|
||||||
|
final EdgeInsetsGeometry? margin;
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
margin: margin,
|
margin: margin,
|
||||||
waitDuration: _tooltipWaitDuration,
|
waitDuration: _tooltipWaitDuration,
|
||||||
|
@ -814,7 +814,18 @@
|
|||||||
},
|
},
|
||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"empty": "Nothing to see here!"
|
"empty": "Nothing to see here!",
|
||||||
|
"tabs": {
|
||||||
|
"inbox": "Inbox",
|
||||||
|
"upcoming": "Upcoming"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"ascending": "Ascending",
|
||||||
|
"descending": "Descending",
|
||||||
|
"groupByDate": "Group by date",
|
||||||
|
"showUnreadsOnly": "Show unreads only",
|
||||||
|
"resetToDefault": "Reset to default"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"reminderNotification": {
|
"reminderNotification": {
|
||||||
"title": "Reminder",
|
"title": "Reminder",
|
||||||
|
Reference in New Issue
Block a user