notification_dialog.dart 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import 'package:appflowy/generated/flowy_svgs.g.dart';
  2. import 'package:appflowy/generated/locale_keys.g.dart';
  3. import 'package:appflowy/startup/startup.dart';
  4. import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
  5. import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
  6. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
  7. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
  8. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
  9. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
  11. import 'package:appflowy_popover/appflowy_popover.dart';
  12. import 'package:collection/collection.dart';
  13. import 'package:easy_localization/easy_localization.dart';
  14. import 'package:flowy_infra/size.dart';
  15. import 'package:flowy_infra/theme_extension.dart';
  16. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  17. import 'package:flowy_infra_ui/style_widget/hover.dart';
  18. import 'package:flutter/material.dart';
  19. import 'package:flutter_bloc/flutter_bloc.dart';
  20. extension _ReminderSort on Iterable<ReminderPB> {
  21. List<ReminderPB> sortByScheduledAt() =>
  22. sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
  23. }
  24. class NotificationDialog extends StatefulWidget {
  25. const NotificationDialog({
  26. super.key,
  27. required this.views,
  28. required this.mutex,
  29. });
  30. final List<ViewPB> views;
  31. final PopoverMutex mutex;
  32. @override
  33. State<NotificationDialog> createState() => _NotificationDialogState();
  34. }
  35. class _NotificationDialogState extends State<NotificationDialog>
  36. with SingleTickerProviderStateMixin {
  37. late final TabController _controller = TabController(length: 2, vsync: this);
  38. final PopoverMutex _mutex = PopoverMutex();
  39. final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
  40. @override
  41. void initState() {
  42. super.initState();
  43. _controller.addListener(_updateState);
  44. }
  45. void _updateState() => setState(() {});
  46. @override
  47. void dispose() {
  48. _mutex.close();
  49. _controller.removeListener(_updateState);
  50. _controller.dispose();
  51. super.dispose();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. return MultiBlocProvider(
  56. providers: [
  57. BlocProvider<ReminderBloc>.value(value: _reminderBloc),
  58. BlocProvider<NotificationFilterBloc>(
  59. create: (_) => NotificationFilterBloc(),
  60. ),
  61. ],
  62. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  63. builder: (context, filterState) =>
  64. BlocBuilder<ReminderBloc, ReminderState>(
  65. builder: (context, state) {
  66. final List<ReminderPB> pastReminders = state.pastReminders
  67. .where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
  68. .sortByScheduledAt();
  69. final List<ReminderPB> upcomingReminders =
  70. state.upcomingReminders.sortByScheduledAt();
  71. return Column(
  72. mainAxisSize: MainAxisSize.min,
  73. crossAxisAlignment: CrossAxisAlignment.start,
  74. children: [
  75. const NotificationHubTitle(),
  76. NotificationTabBar(tabController: _controller),
  77. // TODO(Xazin): Resolve issue with taking up
  78. // max amount of vertical space
  79. Expanded(
  80. child: TabBarView(
  81. controller: _controller,
  82. children: [
  83. NotificationsView(
  84. shownReminders: pastReminders,
  85. reminderBloc: _reminderBloc,
  86. views: widget.views,
  87. onDelete: _onDelete,
  88. onAction: _onAction,
  89. onReadChanged: _onReadChanged,
  90. actionBar: _InboxActionBar(
  91. hasUnreads: state.hasUnreads,
  92. showUnreadsOnly: filterState.showUnreadsOnly,
  93. ),
  94. ),
  95. NotificationsView(
  96. shownReminders: upcomingReminders,
  97. reminderBloc: _reminderBloc,
  98. views: widget.views,
  99. isUpcoming: true,
  100. onAction: _onAction,
  101. ),
  102. ],
  103. ),
  104. ),
  105. ],
  106. );
  107. },
  108. ),
  109. ),
  110. );
  111. }
  112. void _onAction(ReminderPB reminder) {
  113. final view = widget.views.firstWhereOrNull(
  114. (view) => view.id == reminder.objectId,
  115. );
  116. if (view == null) {
  117. return;
  118. }
  119. _reminderBloc.add(
  120. ReminderEvent.pressReminder(reminderId: reminder.id),
  121. );
  122. widget.mutex.close();
  123. }
  124. void _onDelete(ReminderPB reminder) {
  125. _reminderBloc.add(ReminderEvent.remove(reminder: reminder));
  126. }
  127. void _onReadChanged(ReminderPB reminder, bool isRead) {
  128. _reminderBloc.add(
  129. ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
  130. );
  131. }
  132. }
  133. class _InboxActionBar extends StatelessWidget {
  134. const _InboxActionBar({
  135. required this.hasUnreads,
  136. required this.showUnreadsOnly,
  137. });
  138. final bool hasUnreads;
  139. final bool showUnreadsOnly;
  140. @override
  141. Widget build(BuildContext context) {
  142. return DecoratedBox(
  143. decoration: BoxDecoration(
  144. border: Border(
  145. bottom: BorderSide(
  146. color: Theme.of(context).dividerColor,
  147. ),
  148. ),
  149. ),
  150. child: Padding(
  151. padding: const EdgeInsets.symmetric(
  152. horizontal: 16,
  153. vertical: 8,
  154. ),
  155. child: Row(
  156. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  157. children: [
  158. _MarkAsReadButton(
  159. onMarkAllRead: !hasUnreads
  160. ? null
  161. : () => context
  162. .read<ReminderBloc>()
  163. .add(const ReminderEvent.markAllRead()),
  164. ),
  165. _ToggleUnreadsButton(
  166. showUnreadsOnly: showUnreadsOnly,
  167. onToggled: (showUnreadsOnly) => context
  168. .read<NotificationFilterBloc>()
  169. .add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
  170. ),
  171. ],
  172. ),
  173. ),
  174. );
  175. }
  176. }
  177. class _ToggleUnreadsButton extends StatefulWidget {
  178. const _ToggleUnreadsButton({
  179. required this.onToggled,
  180. this.showUnreadsOnly = false,
  181. });
  182. final Function(bool) onToggled;
  183. final bool showUnreadsOnly;
  184. @override
  185. State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
  186. }
  187. class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
  188. late bool showUnreadsOnly = widget.showUnreadsOnly;
  189. @override
  190. Widget build(BuildContext context) {
  191. return SegmentedButton<bool>(
  192. onSelectionChanged: (Set<bool> newSelection) {
  193. setState(() => showUnreadsOnly = newSelection.first);
  194. widget.onToggled(showUnreadsOnly);
  195. },
  196. showSelectedIcon: false,
  197. style: ButtonStyle(
  198. side: MaterialStatePropertyAll(
  199. BorderSide(color: Theme.of(context).dividerColor),
  200. ),
  201. shape: const MaterialStatePropertyAll(
  202. RoundedRectangleBorder(borderRadius: Corners.s6Border),
  203. ),
  204. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  205. (state) {
  206. if (state.contains(MaterialState.hovered) ||
  207. state.contains(MaterialState.selected) ||
  208. state.contains(MaterialState.pressed)) {
  209. return Theme.of(context).colorScheme.onSurface;
  210. }
  211. return AFThemeExtension.of(context).textColor;
  212. },
  213. ),
  214. backgroundColor: MaterialStateProperty.resolveWith<Color>(
  215. (state) {
  216. if (state.contains(MaterialState.hovered) ||
  217. state.contains(MaterialState.selected) ||
  218. state.contains(MaterialState.pressed)) {
  219. return Theme.of(context).colorScheme.primary;
  220. }
  221. return Theme.of(context).cardColor;
  222. },
  223. ),
  224. ),
  225. segments: [
  226. ButtonSegment<bool>(
  227. value: false,
  228. label: Text(
  229. LocaleKeys.notificationHub_actions_showAll.tr(),
  230. style: const TextStyle(fontSize: 12),
  231. ),
  232. ),
  233. ButtonSegment<bool>(
  234. value: true,
  235. label: Text(
  236. LocaleKeys.notificationHub_actions_showUnreads.tr(),
  237. style: const TextStyle(fontSize: 12),
  238. ),
  239. ),
  240. ],
  241. selected: <bool>{showUnreadsOnly},
  242. );
  243. }
  244. }
  245. class _MarkAsReadButton extends StatefulWidget {
  246. final VoidCallback? onMarkAllRead;
  247. const _MarkAsReadButton({this.onMarkAllRead});
  248. @override
  249. State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
  250. }
  251. class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
  252. bool _isHovering = false;
  253. @override
  254. Widget build(BuildContext context) {
  255. return Opacity(
  256. opacity: widget.onMarkAllRead != null ? 1 : 0.5,
  257. child: FlowyHover(
  258. onHover: (isHovering) => setState(() => _isHovering = isHovering),
  259. resetHoverOnRebuild: false,
  260. child: FlowyTextButton(
  261. LocaleKeys.notificationHub_actions_markAllRead.tr(),
  262. fontColor: widget.onMarkAllRead != null && _isHovering
  263. ? Theme.of(context).colorScheme.onSurface
  264. : AFThemeExtension.of(context).textColor,
  265. heading: FlowySvg(
  266. FlowySvgs.checklist_s,
  267. color: widget.onMarkAllRead != null && _isHovering
  268. ? Theme.of(context).colorScheme.onSurface
  269. : AFThemeExtension.of(context).textColor,
  270. ),
  271. hoverColor: widget.onMarkAllRead != null && _isHovering
  272. ? Theme.of(context).colorScheme.primary
  273. : null,
  274. onPressed: widget.onMarkAllRead,
  275. ),
  276. ),
  277. );
  278. }
  279. }