Files
Warp/apps/mobile/lib/matrix/notifications.dart
T
tajniak81andClaude Opus 5.5 3356089c13 Android app (apps/mobile) with Flutter
apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK,
with vodozemac (compiled from Rust during the build) for end-to-end
encryption. It speaks the plain Client-Server API and uses the web
client's design tokens, fonts and symbol.

- Sign in by server name (.well-known), homeserver address or full user ID;
  sign up with the dummy and registration token stages. Sessions survive
  restarts: SQLite in private storage, credentials also in the Keystore,
  both excluded from backups and device transfers.
- Room list with unread and mention counts, invites, search and filters.
  Timeline with grouping, day separators, notices, history on scroll,
  links, images and files (authenticated media). Sending text, photos,
  camera shots and files, with Sending/Sent/Read/Not sent and Retry.
  Typing, receipts, deleting. New rooms and DMs, encrypted by default;
  joining by address; room info with members, invite, rename, topic,
  notification level, leave.
- Encryption: one-step setup behind a recovery key, verifying a new session
  with the recovery key (restoring the backup) or another session, emoji
  verification of people and sessions, reset. Undecryptable messages say
  why. Session list with verify and sign out.
- Notifications while running, and push through UnifiedPush (no Firebase):
  the distributor's endpoint becomes an event_id_only pusher, the gateway
  is discovered, and a push starts the app headless to fetch, decrypt and
  show the event.

Make targets: mobile (release APKs per ABI into bin/), mobile-run,
mobile-test. Kotlin incremental compilation is off (Windows, project and
pub cache on different drives), and Tink's JVM artifact is excluded in
favour of tink-android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:45:06 +02:00

158 lines
5.1 KiB
Dart

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<String>.broadcast();
static bool _ready = false;
/// Room IDs of notifications the user tapped while the app was running.
static Stream<String> 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<void> 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<String?> 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<bool> requestPermission() async =>
await _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission() ??
true;
/// Shows [event] unless the user is looking at its room.
static Future<void> 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<void> 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<void> clearRoom(String roomId) async {
if (!_ready) return;
await _plugin.cancel(id: _idFor(roomId));
}
static Future<void> 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<Event> watch(Client client) =>
client.onNotification.stream.listen((event) {
if (kDebugMode) debugPrint('notify ${event.eventId}');
showEvent(event);
});
}