538 lines
16 KiB
Dart
Raw Normal View History

2023-10-02 09:12:24 +02:00
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/list_extension.dart';
2023-10-02 09:12:24 +02:00
import 'package:appflowy/startup/startup.dart';
2023-10-17 08:48:58 +02:00
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
2023-10-02 09:12:24 +02:00
import 'package:appflowy/user/application/reminder/reminder_service.dart';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/util/int64_extension.dart';
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/notification/notification_service.dart';
2023-10-02 09:12:24 +02:00
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
2023-10-02 09:12:24 +02:00
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
2023-10-02 09:12:24 +02:00
part 'reminder_bloc.freezed.dart';
class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
ReminderBloc() : super(ReminderState()) {
Log.info('ReminderBloc created');
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
_actionBloc = getIt<ActionNavigationBloc>();
_reminderService = const ReminderService();
2023-10-02 09:12:24 +02:00
timer = _periodicCheck();
_dispatch();
}
2023-10-17 08:48:58 +02:00
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
late final ActionNavigationBloc _actionBloc;
late final ReminderService _reminderService;
late final Timer timer;
2023-10-17 08:48:58 +02:00
void _dispatch() {
on<ReminderEvent>(
(event, emit) async {
await event.when(
started: () async {
Log.info('Start fetching reminders');
final result = await _reminderService.fetchReminders();
2023-10-02 09:12:24 +02:00
result.fold(
(reminders) {
Log.info('Fetched reminders on startup: ${reminders.length}');
emit(state.copyWith(reminders: reminders));
},
(error) => Log.error('Failed to fetch reminders: $error'),
);
},
remove: (reminderId) async {
final result = await _reminderService.removeReminder(
reminderId: reminderId,
);
result.fold(
(_) {
Log.info('Removed reminder: $reminderId');
final reminders = [...state.reminders];
reminders.removeWhere((e) => e.id == reminderId);
emit(state.copyWith(reminders: reminders));
},
(error) => Log.error(
'Failed to remove reminder($reminderId): $error',
),
);
},
add: (reminder) async {
// check the timestamp in the reminder
if (reminder.createdAt == null) {
reminder.freeze();
reminder = reminder.rebuild((update) {
update.meta[ReminderMetaKeys.createdAt] =
DateTime.now().millisecondsSinceEpoch.toString();
});
}
final result = await _reminderService.addReminder(
reminder: reminder,
);
return result.fold(
(_) {
Log.info('Added reminder: ${reminder.id}');
Log.info('Before adding reminder: ${state.reminders.length}');
final reminders = [...state.reminders, reminder];
Log.info('After adding reminder: ${reminders.length}');
emit(state.copyWith(reminders: reminders));
},
(error) => Log.error('Failed to add reminder: $error'),
);
},
addById: (reminderId, objectId, scheduledAt, meta) async => add(
ReminderEvent.add(
reminder: ReminderPB(
id: reminderId,
objectId: objectId,
title: LocaleKeys.reminderNotification_title.tr(),
message: LocaleKeys.reminderNotification_message.tr(),
scheduledAt: scheduledAt,
isAck: scheduledAt.toDateTime().isBefore(DateTime.now()),
meta: meta,
),
),
),
update: (updateObject) async {
final reminder = state.reminders.firstWhereOrNull(
(r) => r.id == updateObject.id,
);
2023-10-02 09:12:24 +02:00
if (reminder == null) {
return;
}
final newReminder = updateObject.merge(a: reminder);
final failureOrUnit = await _reminderService.updateReminder(
reminder: newReminder,
);
Log.info('Updating reminder: ${reminder.id}');
failureOrUnit.fold(
(_) {
Log.info('Updated reminder: ${reminder.id}');
final index =
state.reminders.indexWhere((r) => r.id == reminder.id);
final reminders = [...state.reminders];
reminders.replaceRange(index, index + 1, [newReminder]);
emit(state.copyWith(reminders: reminders));
},
(error) => Log.error(
'Failed to update reminder(${reminder.id}): $error',
),
);
},
pressReminder: (reminderId, path, view) {
final reminder =
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
if (reminder == null) {
return;
}
add(
ReminderEvent.update(
ReminderUpdate(
id: reminderId,
isRead: state.pastReminders.contains(reminder),
),
),
);
String? rowId;
if (view?.layout != ViewLayoutPB.Document) {
rowId = reminder.meta[ReminderMetaKeys.rowId];
}
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
final action = NavigationAction(
objectId: reminder.objectId,
arguments: {
ActionArgumentKeys.view: view,
ActionArgumentKeys.nodePath: path,
ActionArgumentKeys.rowId: rowId,
},
);
if (!isClosed) {
_actionBloc.add(
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
ActionNavigationEvent.performAction(
action: action,
nextActions: [
action.copyWith(
type: rowId != null
? ActionType.openRow
: ActionType.jumpToBlock,
),
],
),
);
}
},
markAsRead: (reminderIds) async {
final reminders = await _onMarkAsRead(reminderIds: reminderIds);
Log.info('Marked reminders as read: $reminderIds');
emit(
state.copyWith(
reminders: reminders,
),
);
},
archive: (reminderIds) async {
final reminders = await _onArchived(
isArchived: true,
reminderIds: reminderIds,
);
Log.info('Archived reminders: $reminderIds');
emit(
state.copyWith(
reminders: reminders,
),
);
},
markAllRead: () async {
final reminders = await _onMarkAsRead();
Log.info('Marked all reminders as read');
emit(
state.copyWith(
reminders: reminders,
),
);
},
archiveAll: () async {
final reminders = await _onArchived(isArchived: true);
Log.info('Archived all reminders');
emit(
state.copyWith(
reminders: reminders,
),
);
},
unarchiveAll: () async {
final reminders = await _onArchived(isArchived: false);
emit(
state.copyWith(
reminders: reminders,
),
);
},
refresh: () async {
final result = await _reminderService.fetchReminders();
result.fold(
(reminders) {
Log.info('Fetched reminders on refresh: ${reminders.length}');
emit(state.copyWith(reminders: reminders));
},
(error) => Log.error('Failed to fetch reminders: $error'),
);
},
);
},
);
2023-10-02 09:12:24 +02:00
}
/// Mark the reminder as read
///
/// If the [reminderIds] is null, all unread reminders will be marked as read
/// Otherwise, only the reminders with the given IDs will be marked as read
Future<List<ReminderPB>> _onMarkAsRead({
List<String>? reminderIds,
}) async {
final Iterable<ReminderPB> remindersToUpdate;
if (reminderIds != null) {
remindersToUpdate = state.reminders.where(
(reminder) => reminderIds.contains(reminder.id) && !reminder.isRead,
);
} else {
// Get all reminders that are not matching the isArchived flag
remindersToUpdate = state.reminders.where(
(reminder) => !reminder.isRead,
);
}
for (final reminder in remindersToUpdate) {
reminder.isRead = true;
await _reminderService.updateReminder(reminder: reminder);
Log.info('Mark reminder ${reminder.id} as read');
}
return state.reminders.map((e) {
if (reminderIds != null && !reminderIds.contains(e.id)) {
return e;
}
if (e.isRead) {
return e;
}
e.freeze();
return e.rebuild((update) {
update.isRead = true;
});
}).toList();
}
/// Archive or unarchive reminders
///
/// If the [reminderIds] is null, all reminders will be archived
/// Otherwise, only the reminders with the given IDs will be archived or unarchived
Future<List<ReminderPB>> _onArchived({
required bool isArchived,
List<String>? reminderIds,
}) async {
final Iterable<ReminderPB> remindersToUpdate;
if (reminderIds != null) {
remindersToUpdate = state.reminders.where(
(reminder) =>
reminderIds.contains(reminder.id) &&
reminder.isArchived != isArchived,
);
} else {
// Get all reminders that are not matching the isArchived flag
remindersToUpdate = state.reminders.where(
(reminder) => reminder.isArchived != isArchived,
);
}
for (final reminder in remindersToUpdate) {
reminder.isRead = isArchived;
reminder.meta[ReminderMetaKeys.isArchived] = isArchived.toString();
await _reminderService.updateReminder(reminder: reminder);
Log.info('Reminder ${reminder.id} is archived: $isArchived');
}
return state.reminders.map((e) {
if (reminderIds != null && !reminderIds.contains(e.id)) {
return e;
}
if (e.isArchived == isArchived) {
return e;
}
e.freeze();
return e.rebuild((update) {
update.isRead = isArchived;
update.meta[ReminderMetaKeys.isArchived] = isArchived.toString();
});
}).toList();
}
2023-10-02 09:12:24 +02:00
Timer _periodicCheck() {
return Timer.periodic(
const Duration(minutes: 1),
(_) async {
2023-10-02 09:12:24 +02:00
final now = DateTime.now();
2023-10-12 04:19:36 +02:00
for (final reminder in state.upcomingReminders) {
2023-10-02 09:12:24 +02:00
if (reminder.isAck) {
continue;
}
final scheduledAt = reminder.scheduledAt.toDateTime();
2023-10-02 09:12:24 +02:00
if (scheduledAt.isBefore(now)) {
final notificationSettings =
await UserSettingsBackendService().getNotificationSettings();
if (notificationSettings.notificationsEnabled) {
NotificationMessage(
identifier: reminder.id,
title: LocaleKeys.reminderNotification_title.tr(),
body: LocaleKeys.reminderNotification_message.tr(),
onClick: () => _actionBloc.add(
feat: search mvp (#5064) * feat: implement folder indexer * feat: sqlite search views using fts5 * feat: add view indexing to user manager * feat: implement folder indexer * feat: add sqlite search documents * feat: add document indexing to user manager * feat: add document indexing to folder indexer * chore: update collab rev * feat: search frontend integration * refactor: search index * test: add event test * chore: fix ci * feat: initial command palette overlay impl (#4619) * chore: test search engine * chore: initial structure * chore: replace old search request * chore: enable log for lib-dispatch * chore: move search manager to core * feat: move traits and responsibility to search crate * feat: move search to search crate * feat: replace sqlite with tantivy * feat: deserialize tantivy documents * chore: fixes after rebase * chore: clean code * feat: fetch and sort results * fix: code review + cleaning * feat: support custom icons * feat: support view layout icons * feat: rename bloc and fix indexing * fix: prettify dialog * feat: score results * chore: update collab rev * feat: add recent view history to command palette * test: add integration_tests * fix: clippy changes * fix: focus traversal in cmd palette * fix: remove file after merging main * chore: code review and panic-safe * feat: index all views if index does not exist * chore: improve logic with conditional * chore: add is_empty check * chore: abstract logic from folder manager init * chore: update collab rev * chore: code review * chore: fixes after merge + update lock file * chore: revert cargo lock * fix: set icon type when removing icon * fix: code review + dependency inversion * fix: remove icon fix for not persisting icon type * test: simple tests manipulating views * test: create 100 views * fix: tauri build * chore: create 1000 views * chore: create util methods * chore: test * chore: test * chore: remove logs * chore: fix build.rs * chore: export models * chore: enable clear cache on Rust-CI * fix: navigate to newly created views * fix: force disable setting workspace listener on rebuilds * fix: remove late final * fix: missing returns * fix: localization and minor fixes * test: add index assert to large test * fix: missing section param after merging main * chore: try fix unzip file error * chore: lower the test * feat: show hint when result is in trash * feat: one index_writer per index * fix: minor changes after merge * fix: make create_log_filter public after merge * chore: fix test * chore: fix test * chore: flutter analyze * chore: flutter analyze * chore: fix tauri build --------- Co-authored-by: nathan <nathan@appflowy.io> Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-04-12 10:21:41 +02:00
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: reminder.objectId),
),
2023-10-02 09:12:24 +02:00
),
);
}
2023-10-02 09:12:24 +02:00
add(
ReminderEvent.update(
ReminderUpdate(id: reminder.id, isAck: true),
),
);
}
}
},
);
}
}
@freezed
class ReminderEvent with _$ReminderEvent {
// On startup we fetch all reminders and upcoming ones
const factory ReminderEvent.started() = _Started;
// Remove a reminder
const factory ReminderEvent.remove({required String reminderId}) = _Remove;
2023-10-02 09:12:24 +02:00
// Add a reminder
const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
// Add a reminder
const factory ReminderEvent.addById({
required String reminderId,
required String objectId,
required Int64 scheduledAt,
@Default(null) Map<String, String>? meta,
}) = _AddById;
2023-10-02 09:12:24 +02:00
// Update a reminder (eg. isAck, isRead, etc.)
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
// Event to mark specific reminders as read, takes a list of reminder IDs
const factory ReminderEvent.markAsRead(List<String> reminderIds) =
_MarkAsRead;
// Event to mark all unread reminders as read
2023-10-17 08:48:58 +02:00
const factory ReminderEvent.markAllRead() = _MarkAllRead;
// Event to archive specific reminders, takes a list of reminder IDs
const factory ReminderEvent.archive(List<String> reminderIds) = _Archive;
// Event to archive all reminders
const factory ReminderEvent.archiveAll() = _ArchiveAll;
// Event to unarchive all reminders
const factory ReminderEvent.unarchiveAll() = _UnarchiveAll;
// Event to handle reminder press action
const factory ReminderEvent.pressReminder({
required String reminderId,
@Default(null) int? path,
@Default(null) ViewPB? view,
}) = _PressReminder;
// Event to refresh reminders
const factory ReminderEvent.refresh() = _Refresh;
2023-10-02 09:12:24 +02:00
}
/// Object used to merge updates with
/// a [ReminderPB]
///
class ReminderUpdate {
ReminderUpdate({
required this.id,
this.isAck,
this.isRead,
this.scheduledAt,
2023-10-17 08:48:58 +02:00
this.includeTime,
this.isArchived,
this.date,
2023-10-02 09:12:24 +02:00
});
final String id;
final bool? isAck;
final bool? isRead;
final DateTime? scheduledAt;
final bool? includeTime;
final bool? isArchived;
final DateTime? date;
2023-10-02 09:12:24 +02:00
ReminderPB merge({required ReminderPB a}) {
final isAcknowledged = isAck == null && scheduledAt != null
? scheduledAt!.isBefore(DateTime.now())
: a.isAck;
2023-10-17 08:48:58 +02:00
final meta = a.meta;
if (includeTime != a.includeTime) {
meta[ReminderMetaKeys.includeTime] = includeTime.toString();
2023-10-17 08:48:58 +02:00
}
if (isArchived != a.isArchived) {
meta[ReminderMetaKeys.isArchived] = isArchived.toString();
}
if (date != a.date && date != null) {
meta[ReminderMetaKeys.date] = date!.millisecondsSinceEpoch.toString();
}
2023-10-02 09:12:24 +02:00
return ReminderPB(
id: a.id,
objectId: a.objectId,
scheduledAt: scheduledAt != null
? Int64(scheduledAt!.millisecondsSinceEpoch ~/ 1000)
: a.scheduledAt,
isAck: isAcknowledged,
isRead: isRead ?? a.isRead,
title: a.title,
message: a.message,
2023-10-17 08:48:58 +02:00
meta: meta,
2023-10-02 09:12:24 +02:00
);
}
}
class ReminderState {
2023-10-12 04:19:36 +02:00
ReminderState({List<ReminderPB>? reminders}) {
_reminders = reminders ?? [];
pastReminders = [];
upcomingReminders = [];
if (_reminders.isEmpty) {
hasUnreads = false;
return;
}
final now = DateTime.now();
bool hasUnreadReminders = false;
for (final reminder in _reminders) {
final scheduledDate = DateTime.fromMillisecondsSinceEpoch(
reminder.scheduledAt.toInt() * 1000,
2023-10-02 09:12:24 +02:00
);
2023-10-12 04:19:36 +02:00
if (scheduledDate.isBefore(now)) {
pastReminders.add(reminder);
if (!hasUnreadReminders && !reminder.isRead) {
hasUnreadReminders = true;
}
} else {
upcomingReminders.add(reminder);
}
}
hasUnreads = hasUnreadReminders;
}
late final List<ReminderPB> _reminders;
List<ReminderPB> get reminders => _reminders.unique((e) => e.id);
2023-10-12 04:19:36 +02:00
late final List<ReminderPB> pastReminders;
late final List<ReminderPB> upcomingReminders;
late final bool hasUnreads;
ReminderState copyWith({List<ReminderPB>? reminders}) =>
ReminderState(reminders: reminders ?? _reminders);
2023-10-02 09:12:24 +02:00
}