129 lines
4.9 KiB
Dart
129 lines
4.9 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:eraser/eraser.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../push/chat_thread_store.dart';
|
|
import '../push/push_renderer.dart';
|
|
import '../push/push_target.dart';
|
|
import '../routing/app_routes.dart';
|
|
import '../session/session_manager.dart';
|
|
import '../state/app/modules/app_modules.dart';
|
|
import '../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
|
import '../state/app/modules/parent_letters/bloc/parent_letters_bloc.dart';
|
|
import 'notification_service.dart';
|
|
|
|
class NotificationTasks {
|
|
/// Recomputes the app badge from the notifications currently in the tray.
|
|
/// Deterministic — no server-provided counter to drift out of sync — so the
|
|
/// badge always matches what the user actually sees. Called after rendering,
|
|
/// cancelling, or opening the app.
|
|
static Future<void> refreshBadge() async {
|
|
try {
|
|
final plugin = NotificationService().flutterLocalNotificationsPlugin;
|
|
final actives = await plugin.getActiveNotifications();
|
|
await FlutterAppBadge.count(actives.length);
|
|
} on Object catch (e) {
|
|
log('Badge refresh failed: $e');
|
|
}
|
|
}
|
|
|
|
/// Per-chat tag scheme — canonical definition lives beside the stacked
|
|
/// notification id in chat_thread_store.dart.
|
|
static String chatTag(String chatToken) => chatNotificationTag(chatToken);
|
|
|
|
/// Removes tray notifications belonging to [chatToken]. Eraser handles
|
|
/// iOS (where the plugin's `getActiveNotifications` returns null ids
|
|
/// for FCM posts and can't cancel them); the local-notifications sweep
|
|
/// handles Android and acts as a fallback while Eraser's native side
|
|
/// isn't built in yet.
|
|
static Future<void> clearNotificationsForChat(String chatToken) async {
|
|
final tag = chatTag(chatToken);
|
|
try {
|
|
await Eraser.clearAppNotificationsByTag(tag);
|
|
} on MissingPluginException {
|
|
// Eraser native code not yet linked — needs flutter clean + run.
|
|
} on Object catch (e) {
|
|
log('Eraser($tag) failed: $e');
|
|
}
|
|
try {
|
|
final plugin = NotificationService().flutterLocalNotificationsPlugin;
|
|
final actives = await plugin.getActiveNotifications();
|
|
for (final n in actives) {
|
|
final id = n.id;
|
|
if (id == null) continue;
|
|
if (n.tag == tag) await plugin.cancel(id: id, tag: n.tag);
|
|
}
|
|
} on Object catch (e) {
|
|
log('Active-notification sweep failed: $e');
|
|
}
|
|
// Drop the stacked-notification history too — otherwise the next push
|
|
// would resurrect all already-read messages in the new notification.
|
|
try {
|
|
await ChatThreadStore().clearChat(chatToken);
|
|
} on Object catch (e) {
|
|
log('Chat thread cleanup failed: $e');
|
|
}
|
|
}
|
|
|
|
/// Refreshes the chat list. Deliberately does NOT touch [ChatBloc] —
|
|
/// the open chat view manages its own state via long-poll, and refreshing
|
|
/// it here would re-fetch the last-opened chat with setReadMarker=on
|
|
/// even if the user has already left.
|
|
static void updateProviders(BuildContext context) {
|
|
context.read<ChatListBloc>().refresh();
|
|
context.read<ParentLettersBloc>().refresh(silent: true);
|
|
}
|
|
|
|
/// [title] is the notification title, when the tap came with one.
|
|
static void openPushTarget(
|
|
BuildContext context,
|
|
PushTarget target, {
|
|
String? title,
|
|
}) {
|
|
switch (target) {
|
|
case ParentLetterTarget(:final letterId):
|
|
if (AppModule.isAvailableFor(
|
|
Modules.parentLetters,
|
|
SessionManager().current,
|
|
)) {
|
|
AppRoutes.openParentLetter(context, id: letterId);
|
|
}
|
|
case NewsletterTarget(:final newsletterId):
|
|
AppRoutes.openNewsletterById(context, id: newsletterId, title: title);
|
|
case ChatTarget(:final chatToken):
|
|
navigateToTalk(context, chatToken: chatToken);
|
|
}
|
|
}
|
|
|
|
/// Removes the letter's tray notification (Android; iOS alerts are posted by
|
|
/// the system and cannot be addressed by id).
|
|
static Future<void> clearParentLetterNotification(String letterId) async {
|
|
try {
|
|
await NotificationService().flutterLocalNotificationsPlugin.cancel(
|
|
id: PushRenderer.parentLetterNotificationId(letterId),
|
|
);
|
|
await refreshBadge();
|
|
} on Object catch (e) {
|
|
log('Parent letter notification cleanup failed: $e');
|
|
}
|
|
}
|
|
|
|
/// Switches to the Talk tab. If [chatToken] is provided, also schedules
|
|
/// the matching chat to be opened automatically once the chat list view
|
|
/// resolves the token (handled inside [ChatList]).
|
|
static void navigateToTalk(BuildContext context, {String? chatToken}) {
|
|
if (!AppModule.isAvailableFor(Modules.talk, SessionManager().current)) {
|
|
return;
|
|
}
|
|
if (chatToken != null && chatToken.isNotEmpty) {
|
|
AppRoutes.openChatByToken(context, chatToken);
|
|
} else {
|
|
AppRoutes.goToTalkTab(context);
|
|
}
|
|
}
|
|
}
|