import 'dart:async'; import 'dart:ui' show Color; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:matrix/matrix.dart'; /// System notifications for new messages: one per room, replaced as more /// messages arrive, opening the room when tapped. /// /// They come from two places. While the app runs, from the SDK's /// [Client.onNotification] (events the user's push rules say notify). While /// it doesn't, from a push ([Push]), which starts the app in the background /// just long enough to fetch the event and show it. class Notifications { Notifications._(); static final _plugin = FlutterLocalNotificationsPlugin(); static final _opened = StreamController.broadcast(); static bool _ready = false; /// Room IDs of notifications the user tapped while the app was running. static Stream get opened => _opened.stream; /// The room the user is looking at, whose messages don't notify. static String? activeRoomId; /// Whether the app is in the foreground. Messages in rooms other than /// [activeRoomId] still notify then. static bool foreground = true; static const _channel = AndroidNotificationChannel( 'messages', 'Messages', description: 'New messages and invites', importance: Importance.high, ); static Future init() async { if (_ready) return; await _plugin.initialize( settings: const InitializationSettings( android: AndroidInitializationSettings('ic_notification'), ), onDidReceiveNotificationResponse: (response) { final roomId = response.payload; if (roomId != null && roomId.isNotEmpty) _opened.add(roomId); }, ); await _plugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >() ?.createNotificationChannel(_channel); _ready = true; } /// The room of the notification that launched the app, if one did. static Future launchRoomId() async { final details = await _plugin.getNotificationAppLaunchDetails(); if (details?.didNotificationLaunchApp != true) return null; return details?.notificationResponse?.payload; } /// Asks for the Android 13+ notification permission. Returns whether /// notifications may be shown. static Future requestPermission() async => await _plugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >() ?.requestNotificationsPermission() ?? true; /// Shows [event] unless the user is looking at its room. static Future showEvent(Event event) async { final room = event.room; if (foreground && room.id == activeRoomId) return; final i18n = const MatrixDefaultLocalizations(); final body = room.membership == Membership.invite ? '${event.senderFromMemoryOrFallback.calcDisplayname()} invited you' : event.calcLocalizedBodyFallback( i18n, withSenderNamePrefix: !room.isDirectChat, hideReply: true, plaintextBody: false, removeMarkdown: true, ); await show( roomId: room.id, title: room.getLocalizedDisplayname(i18n), body: body, timestamp: event.originServerTs, ); } static Future show({ required String roomId, required String title, required String body, DateTime? timestamp, }) async { if (!_ready) await init(); try { await _plugin.show( id: _idFor(roomId), title: title, body: body, payload: roomId, notificationDetails: NotificationDetails( android: AndroidNotificationDetails( _channel.id, _channel.name, channelDescription: _channel.description, importance: Importance.high, priority: Priority.high, category: AndroidNotificationCategory.message, when: timestamp?.millisecondsSinceEpoch, showWhen: timestamp != null, color: const Color(0xFF00A389), groupKey: 'messages', ), ), ); } catch (e, s) { Logs().w('Could not show a notification', e, s); } } /// Removes the room's notification, when it has been read or opened. static Future clearRoom(String roomId) async { if (!_ready) return; await _plugin.cancel(id: _idFor(roomId)); } static Future clearAll() async { if (!_ready) return; await _plugin.cancelAll(); } // Notification IDs are 32-bit ints; a room's is a stable hash of its ID so // a new message replaces the room's previous notification. static int _idFor(String roomId) { var h = 0x811c9dc5; for (final unit in roomId.codeUnits) { h = ((h ^ unit) * 0x01000193) & 0x7fffffff; } return h; } /// Listens to [client] for events that notify while the app is running. static StreamSubscription watch(Client client) => client.onNotification.stream.listen((event) { if (kDebugMode) debugPrint('notify ${event.eventId}'); showEvent(event); }); }