reminder_bloc.dart 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import 'dart:async';
  2. import 'package:appflowy/generated/locale_keys.g.dart';
  3. import 'package:appflowy/startup/startup.dart';
  4. import 'package:appflowy/user/application/reminder/reminder_extension.dart';
  5. import 'package:appflowy/user/application/reminder/reminder_service.dart';
  6. import 'package:appflowy/workspace/application/notifications/notification_action.dart';
  7. import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
  8. import 'package:appflowy/workspace/application/notifications/notification_service.dart';
  9. import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
  10. import 'package:appflowy_backend/log.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
  12. import 'package:bloc/bloc.dart';
  13. import 'package:collection/collection.dart';
  14. import 'package:easy_localization/easy_localization.dart';
  15. import 'package:fixnum/fixnum.dart';
  16. import 'package:flutter/foundation.dart';
  17. import 'package:freezed_annotation/freezed_annotation.dart';
  18. part 'reminder_bloc.freezed.dart';
  19. class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
  20. final NotificationSettingsCubit _notificationSettings;
  21. late final NotificationActionBloc actionBloc;
  22. late final ReminderService reminderService;
  23. late final Timer timer;
  24. ReminderBloc({
  25. required NotificationSettingsCubit notificationSettings,
  26. }) : _notificationSettings = notificationSettings,
  27. super(ReminderState()) {
  28. actionBloc = getIt<NotificationActionBloc>();
  29. reminderService = const ReminderService();
  30. timer = _periodicCheck();
  31. on<ReminderEvent>((event, emit) async {
  32. await event.when(
  33. markAllRead: () async {
  34. final unreadReminders =
  35. state.pastReminders.where((reminder) => !reminder.isRead);
  36. final reminders = [...state.reminders];
  37. final updatedReminders = <ReminderPB>[];
  38. for (final reminder in unreadReminders) {
  39. reminders.remove(reminder);
  40. reminder.isRead = true;
  41. await reminderService.updateReminder(reminder: reminder);
  42. updatedReminders.add(reminder);
  43. }
  44. reminders.addAll(updatedReminders);
  45. emit(state.copyWith(reminders: reminders));
  46. },
  47. started: () async {
  48. final remindersOrFailure = await reminderService.fetchReminders();
  49. remindersOrFailure.fold(
  50. (error) => Log.error(error),
  51. (reminders) => emit(state.copyWith(reminders: reminders)),
  52. );
  53. },
  54. remove: (reminder) async {
  55. final unitOrFailure =
  56. await reminderService.removeReminder(reminderId: reminder.id);
  57. unitOrFailure.fold(
  58. (error) => Log.error(error),
  59. (_) {
  60. final reminders = [...state.reminders];
  61. reminders.removeWhere((e) => e.id == reminder.id);
  62. emit(state.copyWith(reminders: reminders));
  63. },
  64. );
  65. },
  66. add: (reminder) async {
  67. final unitOrFailure =
  68. await reminderService.addReminder(reminder: reminder);
  69. return unitOrFailure.fold(
  70. (error) => Log.error(error),
  71. (_) {
  72. final reminders = [...state.reminders, reminder];
  73. emit(state.copyWith(reminders: reminders));
  74. },
  75. );
  76. },
  77. update: (updateObject) async {
  78. final reminder =
  79. state.reminders.firstWhereOrNull((r) => r.id == updateObject.id);
  80. if (reminder == null) {
  81. return;
  82. }
  83. final newReminder = updateObject.merge(a: reminder);
  84. final failureOrUnit = await reminderService.updateReminder(
  85. reminder: updateObject.merge(a: reminder),
  86. );
  87. failureOrUnit.fold(
  88. (error) => Log.error(error),
  89. (_) {
  90. final index =
  91. state.reminders.indexWhere((r) => r.id == reminder.id);
  92. final reminders = [...state.reminders];
  93. reminders.replaceRange(index, index + 1, [newReminder]);
  94. emit(state.copyWith(reminders: reminders));
  95. },
  96. );
  97. },
  98. pressReminder: (reminderId) {
  99. final reminder =
  100. state.reminders.firstWhereOrNull((r) => r.id == reminderId);
  101. if (reminder == null) {
  102. return;
  103. }
  104. add(
  105. ReminderEvent.update(ReminderUpdate(id: reminderId, isRead: true)),
  106. );
  107. actionBloc.add(
  108. NotificationActionEvent.performAction(
  109. action: NotificationAction(objectId: reminder.objectId),
  110. ),
  111. );
  112. },
  113. );
  114. });
  115. }
  116. Timer _periodicCheck() {
  117. return Timer.periodic(
  118. const Duration(minutes: 1),
  119. (_) {
  120. final now = DateTime.now();
  121. for (final reminder in state.upcomingReminders) {
  122. if (reminder.isAck) {
  123. continue;
  124. }
  125. final scheduledAt = DateTime.fromMillisecondsSinceEpoch(
  126. reminder.scheduledAt.toInt() * 1000,
  127. );
  128. if (scheduledAt.isBefore(now)) {
  129. if (_notificationSettings.state.isNotificationsEnabled) {
  130. NotificationMessage(
  131. identifier: reminder.id,
  132. title: LocaleKeys.reminderNotification_title.tr(),
  133. body: LocaleKeys.reminderNotification_message.tr(),
  134. onClick: () => actionBloc.add(
  135. NotificationActionEvent.performAction(
  136. action: NotificationAction(objectId: reminder.objectId),
  137. ),
  138. ),
  139. );
  140. }
  141. add(
  142. ReminderEvent.update(
  143. ReminderUpdate(id: reminder.id, isAck: true),
  144. ),
  145. );
  146. }
  147. }
  148. },
  149. );
  150. }
  151. }
  152. @freezed
  153. class ReminderEvent with _$ReminderEvent {
  154. // On startup we fetch all reminders and upcoming ones
  155. const factory ReminderEvent.started() = _Started;
  156. // Remove a reminder
  157. const factory ReminderEvent.remove({required ReminderPB reminder}) = _Remove;
  158. // Add a reminder
  159. const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
  160. // Update a reminder (eg. isAck, isRead, etc.)
  161. const factory ReminderEvent.update(ReminderUpdate update) = _Update;
  162. // Mark all unread reminders as read
  163. const factory ReminderEvent.markAllRead() = _MarkAllRead;
  164. const factory ReminderEvent.pressReminder({required String reminderId}) =
  165. _PressReminder;
  166. }
  167. /// Object used to merge updates with
  168. /// a [ReminderPB]
  169. ///
  170. class ReminderUpdate {
  171. final String id;
  172. final bool? isAck;
  173. final bool? isRead;
  174. final DateTime? scheduledAt;
  175. final bool? includeTime;
  176. ReminderUpdate({
  177. required this.id,
  178. this.isAck,
  179. this.isRead,
  180. this.scheduledAt,
  181. this.includeTime,
  182. });
  183. ReminderPB merge({required ReminderPB a}) {
  184. final isAcknowledged = isAck == null && scheduledAt != null
  185. ? scheduledAt!.isBefore(DateTime.now())
  186. : a.isAck;
  187. final meta = a.meta;
  188. if (includeTime != a.includeTime) {
  189. meta[ReminderMetaKeys.includeTime.name] = includeTime.toString();
  190. }
  191. return ReminderPB(
  192. id: a.id,
  193. objectId: a.objectId,
  194. scheduledAt: scheduledAt != null
  195. ? Int64(scheduledAt!.millisecondsSinceEpoch ~/ 1000)
  196. : a.scheduledAt,
  197. isAck: isAcknowledged,
  198. isRead: isRead ?? a.isRead,
  199. title: a.title,
  200. message: a.message,
  201. meta: meta,
  202. );
  203. }
  204. }
  205. class ReminderState {
  206. ReminderState({List<ReminderPB>? reminders}) {
  207. _reminders = reminders ?? [];
  208. pastReminders = [];
  209. upcomingReminders = [];
  210. if (_reminders.isEmpty) {
  211. hasUnreads = false;
  212. return;
  213. }
  214. final now = DateTime.now();
  215. bool hasUnreadReminders = false;
  216. for (final reminder in _reminders) {
  217. final scheduledDate = DateTime.fromMillisecondsSinceEpoch(
  218. reminder.scheduledAt.toInt() * 1000,
  219. );
  220. if (scheduledDate.isBefore(now)) {
  221. pastReminders.add(reminder);
  222. if (!hasUnreadReminders && !reminder.isRead) {
  223. hasUnreadReminders = true;
  224. }
  225. } else {
  226. upcomingReminders.add(reminder);
  227. }
  228. }
  229. hasUnreads = hasUnreadReminders;
  230. }
  231. late final List<ReminderPB> _reminders;
  232. List<ReminderPB> get reminders => _reminders;
  233. late final List<ReminderPB> pastReminders;
  234. late final List<ReminderPB> upcomingReminders;
  235. late final bool hasUnreads;
  236. ReminderState copyWith({List<ReminderPB>? reminders}) =>
  237. ReminderState(reminders: reminders ?? _reminders);
  238. }