reminder_bloc.dart 7.6 KB

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