reminder_bloc.dart 7.2 KB

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