notification_dialog.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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/notification_grouped_view.dart';
  7. import 'package:appflowy/workspace/presentation/notifications/notification_view.dart';
  8. import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
  9. import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
  12. import 'package:appflowy_popover/appflowy_popover.dart';
  13. import 'package:calendar_view/calendar_view.dart';
  14. import 'package:collection/collection.dart';
  15. import 'package:easy_localization/easy_localization.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. bool isDescending = true,
  23. }) =>
  24. sorted(
  25. (a, b) => isDescending
  26. ? b.scheduledAt.compareTo(a.scheduledAt)
  27. : a.scheduledAt.compareTo(b.scheduledAt),
  28. );
  29. }
  30. class NotificationDialog extends StatefulWidget {
  31. const NotificationDialog({
  32. super.key,
  33. required this.views,
  34. required this.mutex,
  35. });
  36. final List<ViewPB> views;
  37. final PopoverMutex mutex;
  38. @override
  39. State<NotificationDialog> createState() => _NotificationDialogState();
  40. }
  41. class _NotificationDialogState extends State<NotificationDialog>
  42. with SingleTickerProviderStateMixin {
  43. late final TabController _controller = TabController(length: 2, vsync: this);
  44. final PopoverMutex _mutex = PopoverMutex();
  45. final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
  46. @override
  47. void initState() {
  48. super.initState();
  49. _controller.addListener(_updateState);
  50. }
  51. void _updateState() => setState(() {});
  52. @override
  53. void dispose() {
  54. _mutex.close();
  55. _controller.removeListener(_updateState);
  56. _controller.dispose();
  57. super.dispose();
  58. }
  59. @override
  60. Widget build(BuildContext context) {
  61. return MultiBlocProvider(
  62. providers: [
  63. BlocProvider<ReminderBloc>.value(value: _reminderBloc),
  64. BlocProvider<NotificationFilterBloc>(
  65. create: (_) => NotificationFilterBloc(),
  66. ),
  67. ],
  68. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  69. builder: (context, filterState) =>
  70. BlocBuilder<ReminderBloc, ReminderState>(
  71. builder: (context, state) {
  72. final sortDescending =
  73. filterState.sortBy == NotificationSortOption.descending;
  74. final List<ReminderPB> pastReminders = state.pastReminders
  75. .where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
  76. .sortByScheduledAt(isDescending: sortDescending);
  77. final List<ReminderPB> upcomingReminders = state.upcomingReminders
  78. .sortByScheduledAt(isDescending: sortDescending);
  79. return Column(
  80. mainAxisSize: MainAxisSize.min,
  81. crossAxisAlignment: CrossAxisAlignment.start,
  82. children: [
  83. Row(
  84. children: [
  85. DecoratedBox(
  86. decoration: BoxDecoration(
  87. border: Border(
  88. bottom: BorderSide(
  89. color: Theme.of(context).dividerColor,
  90. ),
  91. ),
  92. ),
  93. child: SizedBox(
  94. width: 215,
  95. child: TabBar(
  96. controller: _controller,
  97. indicator: UnderlineTabIndicator(
  98. borderRadius: BorderRadius.circular(4),
  99. borderSide: BorderSide(
  100. width: 1,
  101. color: Theme.of(context).colorScheme.primary,
  102. ),
  103. ),
  104. tabs: [
  105. Tab(
  106. height: 26,
  107. child: FlowyText.regular(
  108. LocaleKeys.notificationHub_tabs_inbox.tr(),
  109. ),
  110. ),
  111. Tab(
  112. height: 26,
  113. child: FlowyText.regular(
  114. LocaleKeys.notificationHub_tabs_upcoming.tr(),
  115. ),
  116. ),
  117. ],
  118. ),
  119. ),
  120. ),
  121. const Spacer(),
  122. NotificationViewFilters(),
  123. ],
  124. ),
  125. const VSpace(4),
  126. // TODO(Xazin): Resolve issue with taking up
  127. // max amount of vertical space
  128. Expanded(
  129. child: TabBarView(
  130. controller: _controller,
  131. children: [
  132. if (!filterState.groupByDate) ...[
  133. NotificationsView(
  134. shownReminders: pastReminders,
  135. reminderBloc: _reminderBloc,
  136. views: widget.views,
  137. onDelete: _onDelete,
  138. onAction: _onAction,
  139. onReadChanged: _onReadChanged,
  140. ),
  141. NotificationsView(
  142. shownReminders: upcomingReminders,
  143. reminderBloc: _reminderBloc,
  144. views: widget.views,
  145. isUpcoming: true,
  146. onAction: _onAction,
  147. ),
  148. ] else ...[
  149. NotificationsGroupView(
  150. groupedReminders: groupBy<ReminderPB, DateTime>(
  151. pastReminders,
  152. (r) => DateTime.fromMillisecondsSinceEpoch(
  153. r.scheduledAt.toInt() * 1000,
  154. ).withoutTime,
  155. ),
  156. reminderBloc: _reminderBloc,
  157. views: widget.views,
  158. onAction: _onAction,
  159. onDelete: _onDelete,
  160. onReadChanged: _onReadChanged,
  161. ),
  162. NotificationsGroupView(
  163. groupedReminders: groupBy<ReminderPB, DateTime>(
  164. upcomingReminders,
  165. (r) => DateTime.fromMillisecondsSinceEpoch(
  166. r.scheduledAt.toInt() * 1000,
  167. ).withoutTime,
  168. ),
  169. reminderBloc: _reminderBloc,
  170. views: widget.views,
  171. isUpcoming: true,
  172. onAction: _onAction,
  173. ),
  174. ],
  175. ],
  176. ),
  177. ),
  178. ],
  179. );
  180. },
  181. ),
  182. ),
  183. );
  184. }
  185. void _onAction(ReminderPB reminder) {
  186. final view = widget.views.firstWhereOrNull(
  187. (view) => view.id == reminder.objectId,
  188. );
  189. if (view == null) {
  190. return;
  191. }
  192. _reminderBloc.add(
  193. ReminderEvent.pressReminder(reminderId: reminder.id),
  194. );
  195. widget.mutex.close();
  196. }
  197. void _onDelete(ReminderPB reminder) {
  198. _reminderBloc.add(ReminderEvent.remove(reminder: reminder));
  199. }
  200. void _onReadChanged(ReminderPB reminder, bool isRead) {
  201. _reminderBloc.add(
  202. ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
  203. );
  204. }
  205. }
  206. class NotificationViewFilters extends StatelessWidget {
  207. NotificationViewFilters({super.key});
  208. final PopoverMutex _mutex = PopoverMutex();
  209. @override
  210. Widget build(BuildContext context) {
  211. return BlocProvider<NotificationFilterBloc>.value(
  212. value: context.read<NotificationFilterBloc>(),
  213. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  214. builder: (context, state) {
  215. return AppFlowyPopover(
  216. mutex: _mutex,
  217. offset: const Offset(0, 5),
  218. constraints: BoxConstraints.loose(const Size(225, 200)),
  219. direction: PopoverDirection.bottomWithLeftAligned,
  220. popupBuilder: (popoverContext) {
  221. // TODO(Xazin): This is a workaround until we have resolved
  222. // the issues with closing popovers on leave/outside-clicks
  223. return MouseRegion(
  224. onExit: (_) => _mutex.close(),
  225. child: NotificationFilterPopover(
  226. bloc: context.read<NotificationFilterBloc>(),
  227. ),
  228. );
  229. },
  230. child: FlowyIconButton(
  231. isSelected: state.hasFilters,
  232. iconColorOnHover: Theme.of(context).colorScheme.onSurface,
  233. icon: const FlowySvg(FlowySvgs.filter_s),
  234. ),
  235. );
  236. },
  237. ),
  238. );
  239. }
  240. }
  241. class NotificationFilterPopover extends StatelessWidget {
  242. const NotificationFilterPopover({
  243. super.key,
  244. required this.bloc,
  245. });
  246. final NotificationFilterBloc bloc;
  247. @override
  248. Widget build(BuildContext context) {
  249. return Column(
  250. mainAxisSize: MainAxisSize.min,
  251. children: [
  252. _SortByOption(bloc: bloc),
  253. _ShowUnreadsToggle(bloc: bloc),
  254. _GroupByDateToggle(bloc: bloc),
  255. BlocProvider<NotificationFilterBloc>.value(
  256. value: bloc,
  257. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  258. builder: (context, state) {
  259. return Row(
  260. mainAxisAlignment: MainAxisAlignment.end,
  261. children: [
  262. SizedBox(
  263. width: 115,
  264. child: FlowyButton(
  265. disable: !state.hasFilters,
  266. onTap: state.hasFilters
  267. ? () =>
  268. bloc.add(const NotificationFilterEvent.reset())
  269. : null,
  270. text: FlowyText(
  271. LocaleKeys.notificationHub_filters_resetToDefault.tr(),
  272. ),
  273. ),
  274. ),
  275. ],
  276. );
  277. },
  278. ),
  279. ),
  280. ],
  281. );
  282. }
  283. }
  284. class _ShowUnreadsToggle extends StatelessWidget {
  285. const _ShowUnreadsToggle({required this.bloc});
  286. final NotificationFilterBloc bloc;
  287. @override
  288. Widget build(BuildContext context) {
  289. return BlocProvider<NotificationFilterBloc>.value(
  290. value: bloc,
  291. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  292. builder: (context, state) {
  293. return Row(
  294. children: [
  295. const HSpace(4),
  296. Expanded(
  297. child: FlowyText(
  298. LocaleKeys.notificationHub_filters_showUnreadsOnly.tr(),
  299. ),
  300. ),
  301. Toggle(
  302. style: ToggleStyle.big,
  303. onChanged: (value) => bloc
  304. .add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
  305. value: state.showUnreadsOnly,
  306. ),
  307. ],
  308. );
  309. },
  310. ),
  311. );
  312. }
  313. }
  314. class _GroupByDateToggle extends StatelessWidget {
  315. const _GroupByDateToggle({required this.bloc});
  316. final NotificationFilterBloc bloc;
  317. @override
  318. Widget build(BuildContext context) {
  319. return BlocProvider<NotificationFilterBloc>.value(
  320. value: bloc,
  321. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  322. builder: (context, state) {
  323. return Row(
  324. children: [
  325. const HSpace(4),
  326. Expanded(
  327. child: FlowyText(
  328. LocaleKeys.notificationHub_filters_groupByDate.tr(),
  329. ),
  330. ),
  331. Toggle(
  332. style: ToggleStyle.big,
  333. onChanged: (value) =>
  334. bloc.add(const NotificationFilterEvent.toggleGroupByDate()),
  335. value: state.groupByDate,
  336. ),
  337. ],
  338. );
  339. },
  340. ),
  341. );
  342. }
  343. }
  344. class _SortByOption extends StatefulWidget {
  345. const _SortByOption({required this.bloc});
  346. final NotificationFilterBloc bloc;
  347. @override
  348. State<_SortByOption> createState() => _SortByOptionState();
  349. }
  350. class _SortByOptionState extends State<_SortByOption> {
  351. bool _isHovering = false;
  352. @override
  353. Widget build(BuildContext context) {
  354. return BlocProvider<NotificationFilterBloc>.value(
  355. value: widget.bloc,
  356. child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
  357. builder: (context, state) {
  358. final isSortDescending =
  359. state.sortBy == NotificationSortOption.descending;
  360. return Row(
  361. children: [
  362. const Expanded(
  363. child: Padding(
  364. padding: EdgeInsets.only(left: 4.0),
  365. child: FlowyText('Sort'),
  366. ),
  367. ),
  368. const Spacer(),
  369. SizedBox(
  370. width: 115,
  371. child: FlowyHover(
  372. resetHoverOnRebuild: false,
  373. child: FlowyButton(
  374. onHover: (isHovering) => isHovering != _isHovering
  375. ? setState(() => _isHovering = isHovering)
  376. : null,
  377. onTap: () => widget.bloc.add(
  378. NotificationFilterEvent.changeSortBy(
  379. isSortDescending
  380. ? NotificationSortOption.ascending
  381. : NotificationSortOption.descending,
  382. ),
  383. ),
  384. leftIcon: FlowySvg(
  385. isSortDescending
  386. ? FlowySvgs.sort_descending_s
  387. : FlowySvgs.sort_ascending_s,
  388. color: _isHovering
  389. ? Theme.of(context).colorScheme.onSurface
  390. : Theme.of(context).iconTheme.color,
  391. ),
  392. text: FlowyText.regular(
  393. isSortDescending
  394. ? LocaleKeys.notificationHub_filters_descending.tr()
  395. : LocaleKeys.notificationHub_filters_ascending.tr(),
  396. color: _isHovering
  397. ? Theme.of(context).colorScheme.onSurface
  398. : Theme.of(context).textTheme.bodyMedium?.color,
  399. ),
  400. ),
  401. ),
  402. ),
  403. ],
  404. );
  405. },
  406. ),
  407. );
  408. }
  409. }