reminder_bloc.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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) => _updateState(emit, reminders),
  32. );
  33. },
  34. remove: (reminderId) async {
  35. final unitOrFailure =
  36. await reminderService.removeReminder(reminderId: reminderId);
  37. unitOrFailure.fold(
  38. (error) => Log.error(error),
  39. (_) {
  40. final reminders = [...state.reminders];
  41. reminders.removeWhere((e) => e.id == reminderId);
  42. _updateState(emit, 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. state.reminders.add(reminder);
  53. _updateState(emit, state.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. _updateState(emit, 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. void _updateState(Emitter emit, List<ReminderPB> reminders) {
  97. final now = DateTime.now();
  98. final hasUnreads = reminders.any(
  99. (r) =>
  100. DateTime.fromMillisecondsSinceEpoch(r.scheduledAt.toInt() * 1000)
  101. .isBefore(now) &&
  102. !r.isRead,
  103. );
  104. emit(state.copyWith(reminders: reminders, hasUnreads: hasUnreads));
  105. }
  106. Timer _periodicCheck() {
  107. return Timer.periodic(
  108. const Duration(minutes: 1),
  109. (_) {
  110. final now = DateTime.now();
  111. for (final reminder in state.reminders) {
  112. if (reminder.isAck) {
  113. continue;
  114. }
  115. final scheduledAt = DateTime.fromMillisecondsSinceEpoch(
  116. reminder.scheduledAt.toInt() * 1000,
  117. );
  118. if (scheduledAt.isBefore(now)) {
  119. NotificationMessage(
  120. identifier: reminder.id,
  121. title: LocaleKeys.reminderNotification_title.tr(),
  122. body: LocaleKeys.reminderNotification_message.tr(),
  123. onClick: () => actionBloc.add(
  124. NotificationActionEvent.performAction(
  125. action: NotificationAction(objectId: reminder.objectId),
  126. ),
  127. ),
  128. );
  129. add(
  130. ReminderEvent.update(
  131. ReminderUpdate(id: reminder.id, isAck: true),
  132. ),
  133. );
  134. }
  135. }
  136. },
  137. );
  138. }
  139. }
  140. @freezed
  141. class ReminderEvent with _$ReminderEvent {
  142. // On startup we fetch all reminders and upcoming ones
  143. const factory ReminderEvent.started() = _Started;
  144. // Remove a reminder
  145. const factory ReminderEvent.remove({required String reminderId}) = _Remove;
  146. // Add a reminder
  147. const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
  148. // Update a reminder (eg. isAck, isRead, etc.)
  149. const factory ReminderEvent.update(ReminderUpdate update) = _Update;
  150. const factory ReminderEvent.pressReminder({required String reminderId}) =
  151. _PressReminder;
  152. }
  153. /// Object used to merge updates with
  154. /// a [ReminderPB]
  155. ///
  156. class ReminderUpdate {
  157. final String id;
  158. final bool? isAck;
  159. final bool? isRead;
  160. final DateTime? scheduledAt;
  161. ReminderUpdate({
  162. required this.id,
  163. this.isAck,
  164. this.isRead,
  165. this.scheduledAt,
  166. });
  167. ReminderPB merge({required ReminderPB a}) {
  168. final isAcknowledged = isAck == null && scheduledAt != null
  169. ? scheduledAt!.isBefore(DateTime.now())
  170. : a.isAck;
  171. return ReminderPB(
  172. id: a.id,
  173. objectId: a.objectId,
  174. scheduledAt: scheduledAt != null
  175. ? Int64(scheduledAt!.millisecondsSinceEpoch ~/ 1000)
  176. : a.scheduledAt,
  177. isAck: isAcknowledged,
  178. isRead: isRead ?? a.isRead,
  179. title: a.title,
  180. message: a.message,
  181. meta: a.meta,
  182. );
  183. }
  184. }
  185. class ReminderState {
  186. ReminderState({
  187. List<ReminderPB>? reminders,
  188. bool? hasUnreads,
  189. }) : reminders = reminders ?? [],
  190. hasUnreads = hasUnreads ?? false;
  191. final List<ReminderPB> reminders;
  192. final bool hasUnreads;
  193. ReminderState copyWith({
  194. List<ReminderPB>? reminders,
  195. bool? hasUnreads,
  196. }) =>
  197. ReminderState(
  198. reminders: reminders ?? this.reminders,
  199. hasUnreads: hasUnreads ?? this.hasUnreads,
  200. );
  201. }