refactor: New push

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

View File

@ -2779,5 +2779,50 @@
"widgetName": "Name",
"widgetUrlError": "This is not a valid URL.",
"widgetNameError": "Please provide a display name.",
"errorAddingWidget": "Error adding the widget."
"errorAddingWidget": "Error adding the widget.",
"youRejectedTheInvitation": "You rejected the invitation",
"youJoinedTheChat": "You joined the chat",
"youAcceptedTheInvitation": "You accepted the invitation",
"youBannedUser": "You banned {user}",
"@youBannedUser": {
"placeholders": {
"user": {}
}
},
"youHaveWithdrawnTheInvitationFor": "You have withdrawn the invitation for {user}",
"@youHaveWithdrawnTheInvitationFor": {
"placeholders": {
"user": {}
}
},
"youInvitedBy": "You have been invited by {user}",
"@youInvitedBy": {
"placeholders": {
"user": {}
}
},
"youInvitedUser": "You invited {user}",
"@youInvitedUser": {
"placeholders": {
"user": {}
}
},
"youKicked": "You kicked {user}",
"@youKicked": {
"placeholders": {
"user": {}
}
},
"youKickedAndBanned": "You kicked and banned {user}",
"@youKickedAndBanned": {
"placeholders": {
"user": {}
}
},
"youUnbannedUser": "You unbanned {user}",
"@youUnbannedUser": {
"placeholders": {
"user": {}
}
}
}

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;
}

View File

@ -983,7 +983,7 @@ packages:
name: matrix
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.18"
version: "0.8.19"
matrix_api_lite:
dependency: transitive
description:

View File

@ -24,8 +24,7 @@ dependencies:
email_validator: ^2.0.1
emoji_picker_flutter: ^1.1.2
encrypt: ^5.0.1
#fcm_shared_isolate:
# git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
#fcm_shared_isolate: ^0.1.0
file_picker_cross: ^4.5.0
flutter:
sdk: flutter
@ -58,7 +57,7 @@ dependencies:
keyboard_shortcuts: ^0.1.4
localstorage: ^4.0.0+1
lottie: ^1.2.2
matrix: ^0.8.18
matrix: ^0.8.19
matrix_link_text: ^1.0.2
open_noti_settings: ^0.4.0
package_info_plus: ^1.3.0

View File

@ -1,5 +1,5 @@
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4e018b38..eebf7582 100644
index ad9ffb87..37baafb1 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -44,7 +44,7 @@ android {
@ -11,7 +11,7 @@ index 4e018b38..eebf7582 100644
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -81,7 +81,7 @@ flutter {
@@ -82,11 +82,11 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -20,7 +20,6 @@ index 4e018b38..eebf7582 100644
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
@@ -89,4 +89,4 @@ dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}
@ -56,11 +55,11 @@ index 85aa8647..3b7e09e7 100644
}
diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart
index 3347f176..e304abb4 100644
index 00ca2aaa..8bb8a156 100644
--- a/lib/utils/background_push.dart
+++ b/lib/utils/background_push.dart
@@ -39,7 +39,7 @@ import 'famedlysdk_store.dart';
import 'matrix_sdk_extensions.dart/matrix_locals.dart';
@@ -39,7 +39,7 @@ import '../config/setting_keys.dart';
import 'famedlysdk_store.dart';
import 'platform_infos.dart';
-//import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
@ -68,28 +67,25 @@ index 3347f176..e304abb4 100644
class NoTokenException implements Exception {
String get cause => 'Cannot get firebase token';
@@ -117,7 +117,7 @@ class BackgroundPush {
setupPush();
}
@@ -65,7 +65,7 @@ class BackgroundPush {
- final dynamic _fcmSharedIsolate = null; //FcmSharedIsolate();
+ final dynamic _fcmSharedIsolate = FcmSharedIsolate();
final pendingTests = <String, Completer<void>>{};
- final dynamic firebase = null; //FcmSharedIsolate();
+ final dynamic firebase = FcmSharedIsolate();
DateTime? lastReceivedPush;
StreamSubscription<LoginState>? onLogin;
StreamSubscription<SyncUpdate>? onRoomSync;
diff --git a/pubspec.yaml b/pubspec.yaml
index 73b9eca2..5e6f9f16 100644
index c6295788..8dd17ce4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -25,8 +25,8 @@ dependencies:
@@ -24,7 +24,7 @@ dependencies:
email_validator: ^2.0.1
emoji_picker_flutter: ^1.1.2
encrypt: ^5.0.1
- #fcm_shared_isolate:
- # git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
+ fcm_shared_isolate:
+ git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
- #fcm_shared_isolate: ^0.1.0
+ fcm_shared_isolate: ^0.1.0
file_picker_cross: ^4.5.0
flutter:
sdk: flutter