refactor: New push

This commit is contained in:
Christian Pauly 2022-04-14 07:34:55 +02:00
commit aee89a9be0
8 changed files with 280 additions and 287 deletions

View file

@ -25,7 +25,6 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
@ -34,10 +33,10 @@ import 'package:unifiedpush/unifiedpush.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
import 'package:fluffychat/utils/push_helper.dart';
import '../config/app_config.dart';
import '../config/setting_keys.dart';
import 'famedlysdk_store.dart';
import 'matrix_sdk_extensions.dart/matrix_locals.dart';
import 'platform_infos.dart';
//import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
@ -66,6 +65,8 @@ class BackgroundPush {
final pendingTests = <String, Completer<void>>{};
final dynamic firebase = null; //FcmSharedIsolate();
DateTime? lastReceivedPush;
bool upAction = false;
@ -76,8 +77,15 @@ class BackgroundPush {
onRoomSync ??= client.onSync.stream
.where((s) => s.hasRoomUpdate)
.listen((s) => _onClearingPush(getFromServer: false));
_fcmSharedIsolate?.setListeners(
onMessage: _onFcmMessage,
firebase?.setListeners(
onMessage: (message) => pushHelper(
PushNotification.fromJson(
Map<String, dynamic>.from(message['data'] ?? message)),
client: client,
l10n: l10n,
activeRoomId: router?.currentState?.pathParameters['roomid'],
onSelectNotification: goToRoom,
),
onNewToken: _newFcmToken,
);
if (Platform.isAndroid) {
@ -112,16 +120,14 @@ class BackgroundPush {
void handleLoginStateChanged(_) => setupPush();
StreamSubscription<LoginState>? onLogin;
StreamSubscription<SyncUpdate>? onRoomSync;
void _newFcmToken(String token) {
_fcmToken = token;
setupPush();
}
final dynamic _fcmSharedIsolate = null; //FcmSharedIsolate();
StreamSubscription<LoginState>? onLogin;
StreamSubscription<SyncUpdate>? onRoomSync;
Future<void> setupPusher({
String? gatewayUrl,
String? token,
@ -129,7 +135,7 @@ class BackgroundPush {
bool useDeviceSpecificAppId = false,
}) async {
if (PlatformInfos.isIOS) {
await _fcmSharedIsolate?.requestPermission();
await firebase?.requestPermission();
}
final clientName = PlatformInfos.clientName;
oldTokens ??= <String>{};
@ -213,7 +219,6 @@ class BackgroundPush {
Future<void> setupPush() async {
Logs().d("SetupPush");
await setupLocalNotificationsPlugin();
if (client.loginState != LoginState.loggedIn ||
!PlatformInfos.isMobile ||
context == null) {
@ -272,7 +277,7 @@ class BackgroundPush {
Logs().v('Setup firebase');
if (_fcmToken?.isEmpty ?? true) {
try {
_fcmToken = await _fcmSharedIsolate?.getToken();
_fcmToken = await firebase?.getToken();
if (_fcmToken == null) throw ('PushToken is null');
} catch (e, s) {
Logs().w('[Push] cannot get token', e, e is String ? null : s);
@ -306,43 +311,10 @@ class BackgroundPush {
}
}
bool _notificationsPluginSetUp = false;
Future<void> setupLocalNotificationsPlugin() async {
if (_notificationsPluginSetUp) {
return;
}
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
const initializationSettingsAndroid =
AndroidInitializationSettings('notifications_icon');
final initializationSettingsIOS = IOSInitializationSettings(
onDidReceiveLocalNotification: (i, a, b, c) async => null,
);
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: goToRoom);
_notificationsPluginSetUp = true;
}
Future<void> setupUp() async {
await UnifiedPush.registerAppWithDialog(context!);
}
Future<void> _onFcmMessage(Map<dynamic, dynamic> message) async {
Logs().v('[Push] Foreground message received');
final data = Map<String, dynamic>.from(message['data'] ?? message);
try {
await _onMessage(data);
} catch (e, s) {
Logs().e('[Push] Error while processing notification', e, s);
await _showDefaultNotification(data);
}
}
Future<void> _newUpEndpoint(String newEndpoint, String i) async {
upAction = true;
if (newEndpoint.isEmpty) {
@ -374,7 +346,7 @@ class BackgroundPush {
Logs().i('[Push] UnifiedPush using endpoint ' + endpoint);
final oldTokens = <String?>{};
try {
final fcmToken = await _fcmSharedIsolate?.getToken();
final fcmToken = await firebase?.getToken();
oldTokens.add(fcmToken);
} catch (_) {}
await setupPusher(
@ -405,80 +377,12 @@ class BackgroundPush {
upAction = true;
final data = Map<String, dynamic>.from(
json.decode(utf8.decode(message))['notification']);
try {
await _onMessage(data);
} catch (e, s) {
Logs().e('[Push] Error while processing notification', e, s);
await _showDefaultNotification(data);
}
}
Future<void> _onMessage(Map<String, dynamic> data) async {
Logs().v('[Push] _onMessage');
lastReceivedPush = DateTime.now();
final roomId = data['room_id'];
final eventId = data['event_id'];
if (roomId == 'test') {
Logs().v('[Push] Test $eventId was successful!');
pendingTests.remove(eventId)?.complete();
return;
}
// For legacy reasons the counts map could be a String encoded JSON:
final countsMap =
data.tryGetMap<String, dynamic>('counts', TryGet.silent) ??
(jsonDecode(data.tryGet<String>('counts') ?? '{}')
as Map<String, dynamic>);
final unread = countsMap.tryGet<int>('unread');
if ((roomId?.isEmpty ?? true) ||
(eventId?.isEmpty ?? true) ||
unread == 0) {
await _onClearingPush();
return;
}
var giveUp = false;
var loaded = false;
final stopwatch = Stopwatch();
stopwatch.start();
final syncSubscription = client.onSync.stream.listen((r) {
if (stopwatch.elapsed.inSeconds >= 30) {
giveUp = true;
}
});
final eventSubscription = client.onEvent.stream.listen((e) {
if (e.content['event_id'] == eventId) {
loaded = true;
}
});
try {
if (!(await eventExists(roomId, eventId)) && !loaded) {
do {
Logs().v('[Push] getting ' + roomId + ', event ' + eventId);
await client
.oneShotSync()
.catchError((e) => Logs().v('[Push] Error one-shot syncing', e));
if (stopwatch.elapsed.inSeconds >= 60) {
giveUp = true;
}
} while (!loaded && !giveUp);
}
Logs().v('[Push] ' +
(giveUp ? 'gave up on ' : 'got ') +
roomId +
', event ' +
eventId);
} finally {
await syncSubscription.cancel();
await eventSubscription.cancel();
}
await _showNotification(roomId, eventId);
}
Future<bool> eventExists(String roomId, String? eventId) async {
final room = client.getRoomById(roomId);
if (room == null) return false;
return (await client.database!.getEventById(eventId!, room)) != null;
await pushHelper(
PushNotification.fromJson(data),
client: client,
l10n: l10n,
activeRoomId: router?.currentState?.pathParameters['roomid'],
);
}
/// Workaround for the problem that local notification IDs must be int but we
@ -584,139 +488,4 @@ class BackgroundPush {
_clearingPushLock = false;
}
}
Future<void> _showNotification(String roomId, String eventId) async {
await setupLocalNotificationsPlugin();
final room = client.getRoomById(roomId);
if (room == null) {
throw 'Room not found';
}
await room.postLoad();
final event = await client.database!.getEventById(eventId, room);
final activeRoomId = router!.currentState!.pathParameters['roomid'];
if (((activeRoomId?.isNotEmpty ?? false) &&
activeRoomId == room.id &&
client.syncPresence == null) ||
(event != null && (room.notificationCount == 0))) {
return;
}
// load the locale
await loadLocale();
// Calculate title
final title = l10n!.unreadMessages(room.notificationCount);
// Calculate the body
final body = event!.getLocalizedBody(
MatrixLocals(L10n.of(context!)!),
withSenderNamePrefix: !room.isDirectChat,
plaintextBody: true,
hideReply: true,
hideEdit: true,
);
// The person object for the android message style notification
final avatar = room.avatar == null
? null
: await DefaultCacheManager().getSingleFile(
event.room.avatar!
.getThumbnail(
client,
width: 126,
height: 126,
)
.toString(),
);
final person = Person(
name: room.getLocalizedDisplayname(MatrixLocals(l10n!)),
icon: avatar == null ? null : BitmapFilePathAndroidIcon(avatar.path),
);
// Show notification
final androidPlatformChannelSpecifics = _getAndroidNotificationDetails(
styleInformation: MessagingStyleInformation(
person,
conversationTitle: title,
messages: [
Message(
body,
event.originServerTs,
person,
)
],
),
ticker: l10n!.newMessageInFluffyChat,
);
const iOSPlatformChannelSpecifics = IOSNotificationDetails();
final platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
await _flutterLocalNotificationsPlugin.show(
await mapRoomIdToInt(room.id),
room.getLocalizedDisplayname(MatrixLocals(l10n!)),
body,
platformChannelSpecifics,
payload: roomId,
);
}
Future<dynamic> _showDefaultNotification(Map<String, dynamic> data) async {
try {
await setupLocalNotificationsPlugin();
await loadLocale();
final String? eventId = data['event_id'];
final String? roomId = data['room_id'];
// For legacy reasons the counts map could be a String encoded JSON:
final countsMap = data.tryGetMap<String, dynamic>('counts') ??
(jsonDecode(data.tryGet<String>('counts') ?? '{}')
as Map<String, dynamic>);
final unread = countsMap.tryGet<int>('unread') ?? 1;
if (unread == 0 || roomId == null || eventId == null) {
await _onClearingPush();
return;
}
// Display notification
final androidPlatformChannelSpecifics = _getAndroidNotificationDetails();
const iOSPlatformChannelSpecifics = IOSNotificationDetails();
final platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
final title = l10n!.unreadChats(unread);
await _flutterLocalNotificationsPlugin.show(
await mapRoomIdToInt(roomId),
title,
l10n!.openAppToReadMessages,
platformChannelSpecifics,
payload: roomId,
);
} catch (e, s) {
Logs().e('[Push] Error while processing background notification', e, s);
}
}
AndroidNotificationDetails _getAndroidNotificationDetails(
{MessagingStyleInformation? styleInformation, String? ticker}) {
final color = (context != null ? Theme.of(context!).primaryColor : null) ??
const Color(0xFF5625BA);
return AndroidNotificationDetails(
AppConfig.pushNotificationsChannelId,
AppConfig.pushNotificationsChannelName,
AppConfig.pushNotificationsChannelDescription,
styleInformation: styleInformation,
importance: Importance.max,
priority: Priority.high,
ticker: ticker,
color: color,
);
}
}

View file

@ -14,7 +14,7 @@ import 'matrix_sdk_extensions.dart/flutter_matrix_hive_database.dart';
abstract class ClientManager {
static const String clientNamespace = 'im.fluffychat.store.clients';
static Future<List<Client>> getClients() async {
static Future<List<Client>> getClients({bool initialize = true}) async {
if (PlatformInfos.isLinux) {
Hive.init((await getApplicationSupportDirectory()).path);
} else {
@ -37,12 +37,15 @@ abstract class ClientManager {
await Store().setItem(clientNamespace, jsonEncode(clientNames.toList()));
}
final clients = clientNames.map(createClient).toList();
await Future.wait(clients.map((client) => client
.init(
waitForFirstSync: false,
waitUntilLoadCompletedLoaded: false,
)
.catchError((e, s) => Logs().e('Unable to initialize client', e, s))));
if (initialize) {
await Future.wait(clients.map((client) => client
.init(
waitForFirstSync: false,
waitUntilLoadCompletedLoaded: false,
)
.catchError(
(e, s) => Logs().e('Unable to initialize client', e, s))));
}
if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
for (final client in loggedOutClients) {

View file

@ -265,4 +265,39 @@ class MatrixLocals extends MatrixLocalizations {
@override
String sentReaction(String senderName, String reactionKey) =>
l10n.reactedWith(senderName, reactionKey);
@override
// TODO: implement youAcceptedTheInvitation
String get youAcceptedTheInvitation => l10n.youAcceptedTheInvitation;
@override
String youBannedUser(String targetName) => l10n.youBannedUser(targetName);
@override
String youHaveWithdrawnTheInvitationFor(String targetName) =>
l10n.youHaveWithdrawnTheInvitationFor(targetName);
@override
String youInvitedBy(String senderName) => l10n.youInvitedBy(senderName);
@override
String youInvitedUser(String targetName) => l10n.youInvitedUser(targetName);
@override
// TODO: implement youJoinedTheChat
String get youJoinedTheChat => l10n.youJoinedTheChat;
@override
String youKicked(String targetName) => l10n.youKicked(targetName);
@override
String youKickedAndBanned(String targetName) =>
l10n.youKickedAndBanned(targetName);
@override
// TODO: implement youRejectedTheInvitation
String get youRejectedTheInvitation => l10n.youRejectedTheInvitation;
@override
String youUnbannedUser(String targetName) => l10n.youUnbannedUser(targetName);
}

146
lib/utils/push_helper.dart Normal file
View file

@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
Future<void> pushHelper(
PushNotification notification, {
Client? client,
L10n? l10n,
String? activeRoomId,
Future<dynamic> Function(String?)? onSelectNotification,
}) async {
final isBackgroundMessage = client == null;
Logs().v(
'Push helper has been started (background=$isBackgroundMessage).',
notification.toJson(),
);
if (!isBackgroundMessage &&
activeRoomId == notification.roomId &&
activeRoomId != null &&
client?.syncPresence == null) {
Logs().v('Room is in foreground. Stop push helper here.');
return;
}
// Initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await _flutterLocalNotificationsPlugin.initialize(
InitializationSettings(
android: const AndroidInitializationSettings('notifications_icon'),
iOS: IOSInitializationSettings(
onDidReceiveLocalNotification: (i, a, b, c) async => null,
),
),
onSelectNotification: onSelectNotification,
);
client ??= (await ClientManager.getClients(initialize: false)).first;
final event = await client.getEventByPushNotification(
notification,
storeInDatabase: isBackgroundMessage,
);
if (event == null) {
Logs().v('Notification is a clearing indicator.');
await _flutterLocalNotificationsPlugin.cancelAll();
final store = await SharedPreferences.getInstance();
await store.setString(SettingKeys.notificationCurrentIds, json.encode({}));
return;
}
Logs().v('Push helper got notification event.');
l10n ??= await L10n.delegate.load(window.locale);
final matrixLocals = MatrixLocals(l10n);
// Display notification// Calculate title
final title = l10n.unreadMessages(notification.counts?.unread ?? 1);
// Calculate the body
final body = event.getLocalizedBody(
matrixLocals,
plaintextBody: true,
hideReply: true,
hideEdit: true,
removeMarkdown: true,
);
// The person object for the android message style notification
if (isBackgroundMessage) WidgetsFlutterBinding.ensureInitialized();
final avatar = event.room.avatar?.toString();
final person = Person(
name: event.room.getLocalizedDisplayname(matrixLocals),
icon: avatar == null ? null : ContentUriAndroidIcon(avatar),
);
// Show notification
final androidPlatformChannelSpecifics = AndroidNotificationDetails(
AppConfig.pushNotificationsChannelId,
AppConfig.pushNotificationsChannelName,
AppConfig.pushNotificationsChannelDescription,
styleInformation: MessagingStyleInformation(
person,
conversationTitle: title,
groupConversation: !event.room.isDirectChat,
messages: [
Message(
body,
event.originServerTs,
person,
)
],
),
ticker: l10n.newMessageInFluffyChat,
importance: Importance.max,
priority: Priority.high,
);
const iOSPlatformChannelSpecifics = IOSNotificationDetails();
final platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iOSPlatformChannelSpecifics,
);
await _flutterLocalNotificationsPlugin.show(
await mapRoomIdToInt(event.room.id),
event.room.displayname,
body,
platformChannelSpecifics,
payload: event.roomId,
);
Logs().v('Push helper has been completed!');
}
/// Workaround for the problem that local notification IDs must be int but we
/// sort by [roomId] which is a String. To make sure that we don't have duplicated
/// IDs we map the [roomId] to a number and store this number.
Future<int> mapRoomIdToInt(String roomId) async {
final store = await SharedPreferences.getInstance();
final idMap = Map<String, int>.from(jsonDecode(
(store.getString(SettingKeys.notificationCurrentIds)) ?? '{}'));
int? currentInt;
try {
currentInt = idMap[roomId];
} catch (_) {
currentInt = null;
}
if (currentInt != null) {
return currentInt;
}
var nCurrentInt = 0;
while (idMap.values.contains(currentInt)) {
nCurrentInt++;
}
idMap[roomId] = nCurrentInt;
await store.setString(SettingKeys.notificationCurrentIds, json.encode(idMap));
return nCurrentInt;
}