From 124a5eedf84854f4dcbbf7b9dc2a4a5ea2387f5a Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sat, 23 Jan 2021 14:06:00 +0100 Subject: [PATCH] feat: Add unified push as push provider --- README.md | 41 +- .../chat/fluffy/fluffychat/Application.kt | 2 +- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- android/settings_aar.gradle | 1 + lib/components/matrix.dart | 7 +- lib/config/setting_keys.dart | 3 + lib/utils/famedlysdk_store.dart | 4 + lib/utils/firebase_controller.dart | 603 +++++++++++------- pubspec.lock | 19 +- pubspec.yaml | 12 +- 11 files changed, 458 insertions(+), 239 deletions(-) create mode 100644 android/settings_aar.gradle diff --git a/README.md b/README.md index 6c2074ce..0bca8cbe 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,45 @@ Example B: 3. For testing just run a regular build without extras +# Android push notifications without FCM +Fluffychat has the ability to receive push notifications on android without FCM via the +[UnifiedPush](https://github.com/UnifiedPush) project, e.g. using gotify as push backend. As the project is still pretty new +there might still be some bugs, overall it seems to be working, though. + +While UnifiedPush also supports p2p push via [NoProvider2Push](https://github.com/NoProvider2Push/android) +here the gotify setup will be outlined. Adapt re-write proxies accordingly, if you want to use a different push provider. + +For self-hosted push with gotify you have to install and configure [gotify-server](https://github.com/gotify/server) +with [UnifiedPush](https://github.com/UnifiedPush/contrib/blob/main/providers/gotify.md) support. + +Next, you add the `repo.unifiedpush.org` repository to fdroid and install the gotify client via it. Log into your gotify account and push notifications should work! + +## Matrix-specific re-write proxy +Until [MSC2970](https://github.com/matrix-org/matrix-doc/pull/2970) is figured out we unfortunately +need another simple re-write proxy. By default the one at https://matrix.gateway.unifiedpush.org +is used, however you can easily self-host it. For that, add to your nginx config on the same domain you serve gotify the following: +``` +resolver 8.8.8.8; + +location /_matrix/push/v1/notify { + set $target ''; + if ($request_method = GET ) { + return 200 '{"gateway":"matrix"}'; + } + access_by_lua_block { + local cjson = require("cjson") + ngx.req.read_body() + local body = ngx.req.get_body_data() + local parsedBody = cjson.decode(body) + ngx.var.target = parsedBody["notification"]["devices"][1]["pushkey"] + ngx.req.set_body_data(body) + } + proxy_set_header Content-Type application/json; + proxy_set_header Host $host; + proxy_pass $target; +} +``` + # Special thanks to * Fabiyamada is a graphics designer from Brasil and has made the fluffychat logo and the banner. Big thanks for her great designs. @@ -158,4 +197,4 @@ Example B: * Also thanks to all translators and testers! With your help, fluffychat is now available in more than 12 languages. -* Noto Emoji Font for the awesome emojis. \ No newline at end of file +* Noto Emoji Font for the awesome emojis. diff --git a/android/app/src/main/kotlin/chat/fluffy/fluffychat/Application.kt b/android/app/src/main/kotlin/chat/fluffy/fluffychat/Application.kt index 2df7fe77..249803ab 100644 --- a/android/app/src/main/kotlin/chat/fluffy/fluffychat/Application.kt +++ b/android/app/src/main/kotlin/chat/fluffy/fluffychat/Application.kt @@ -28,4 +28,4 @@ class Application : FlutterApplication(), PluginRegistrantCallback { FlutterSecureStoragePlugin.registerWith(registry.registrarFor("com.it_nomads.fluttersecurestorage")); } } -} \ No newline at end of file +} diff --git a/android/build.gradle b/android/build.gradle index 70b3637d..abd75862 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.2' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..3c46198f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 62786b96..45e63dd6 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -340,10 +340,9 @@ class MatrixState extends State { widget.apl.currentState.pushNamedAndRemoveAllOthers('/'); if (loginState == LoginState.logged) { FirebaseController.context = context; - FirebaseController.setupFirebase( - this, - clientName, - ).catchError(SentryController.captureException); + FirebaseController.matrix = this; + FirebaseController.setupFirebase(clientName) + .catchError(SentryController.captureException); } } }); diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 27abb602..ca067f0c 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -12,4 +12,7 @@ abstract class SettingKeys { static const String showNoPid = 'chat.fluffy.show_no_pid'; static const String databasePassword = 'database-password'; static const String appLockKey = 'chat.fluffy.app_lock'; + static const String unifiedPushRegistered = + 'chat.fluffy.unifiedpush.registered'; + static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint'; } diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 0ce242d8..e59c621e 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -92,6 +92,10 @@ class Store { return await secureStorage.write(key: key, value: value); } + Future setItemBool(String key, bool value) async { + await setItem(key, value.toString()); + } + Future deleteItem(String key) async { if (!PlatformInfos.isMobile) { await _setupLocalStorage(); diff --git a/lib/utils/firebase_controller.dart b/lib/utils/firebase_controller.dart index 347fd751..e882db80 100644 --- a/lib/utils/firebase_controller.dart +++ b/lib/utils/firebase_controller.dart @@ -13,6 +13,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n_en.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:unifiedpush/unifiedpush.dart'; +import 'package:http/http.dart' as http; import '../components/matrix.dart'; import '../config/setting_keys.dart'; @@ -26,91 +28,22 @@ abstract class FirebaseController { static BuildContext context; static MatrixState matrix; - static Future setupFirebase( - MatrixState matrix, String clientName) async { - FirebaseController.matrix = matrix; + static Future setupFirebase(String clientName) async { if (!PlatformInfos.isMobile) return; - final client = matrix.client; if (Platform.isIOS) iOS_Permission(); - String token; - try { - token = await _firebaseMessaging.getToken(); - if (token?.isEmpty ?? true) { - throw '_firebaseMessaging.getToken() has not thrown an exception but returned no token'; - } - } catch (e, s) { - Logs().w('Unable to get firebase token', e, s); - final storeItem = await matrix.store.getItem(SettingKeys.showNoGoogle); - final configOptionMissing = storeItem == null || storeItem.isEmpty; - if (configOptionMissing || (!configOptionMissing && storeItem == '1')) { - await FlushbarHelper.createError( - message: L10n.of(context).noGoogleServicesWarning, - duration: Duration(seconds: 15), - ).show(context); - if (configOptionMissing) { - await matrix.store.setItem(SettingKeys.showNoGoogle, '0'); - } - } - return; - } - final pushers = await client.requestPushers().catchError((e) { - Logs().w('[Push] Unable to request pushers', e); - return []; - }); - final currentPushers = pushers.where((pusher) => pusher.pushkey == token); - if (currentPushers.length == 1 && - currentPushers.first.kind == 'http' && - currentPushers.first.appId == AppConfig.pushNotificationsAppId && - currentPushers.first.appDisplayName == clientName && - currentPushers.first.deviceDisplayName == client.deviceName && - currentPushers.first.lang == 'en' && - currentPushers.first.data.url.toString() == - AppConfig.pushNotificationsGatewayUrl && - currentPushers.first.data.format == - AppConfig.pushNotificationsPusherFormat) { - Logs().i('[Push] Pusher already set'); - } else { - if (currentPushers.isNotEmpty) { - for (final currentPusher in currentPushers) { - currentPusher.pushkey = token; - currentPusher.kind = null; - await client.setPusher( - currentPusher, - append: true, - ); - Logs().i('[Push] Remove legacy pusher for this device'); - } - } - await client - .setPusher( - Pusher( - token, - AppConfig.pushNotificationsAppId, - clientName, - client.deviceName, - 'en', - PusherData( - url: Uri.parse(AppConfig.pushNotificationsGatewayUrl), - format: AppConfig.pushNotificationsPusherFormat, - ), - kind: 'http', - ), - append: false, - ) - .catchError((e, s) { - Logs().e('[Push] Unable to set pushers', e, s); - return []; - }); - } - Function goToRoom = (dynamic message) async { try { String roomId; + if (message is String && message[0] == '{') { + message = json.decode(message); + } if (message is String) { roomId = message; } else if (message is Map) { - roomId = (message['data'] ?? message)['room_id']; + roomId = (message['data'] ?? + message['notification'] ?? + message)['room_id']; } if (roomId?.isEmpty ?? true) throw ('Bad roomId'); await matrix.widget.apl.currentState @@ -136,162 +69,371 @@ abstract class FirebaseController { await _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: goToRoom); + // ignore: unawaited_futures + _flutterLocalNotificationsPlugin + .getNotificationAppLaunchDetails() + .then((details) { + if (details == null || !details.didNotificationLaunchApp) { + return; + } + goToRoom(details.payload); + }); + + if ((await UnifiedPush.getDistributors()).isNotEmpty) { + await setupUnifiedPush(clientName); + return; + } + + String fcmToken; + try { + fcmToken = await _firebaseMessaging.getToken(); + if (fcmToken?.isEmpty ?? true) { + throw '_firebaseMessaging.getToken() has not thrown an exception but returned no token'; + } + } catch (e, s) { + Logs().w('Unable to get firebase token', e, s); + fcmToken = null; + } + if (fcmToken?.isEmpty ?? true) { + // no google services warning + if (await matrix.store.getItemBool(SettingKeys.showNoGoogle, true)) { + await FlushbarHelper.createError( + message: L10n.of(context).noGoogleServicesWarning, + duration: Duration(seconds: 15), + ).show(context); + if (null == await matrix.store.getItemBool(SettingKeys.showNoGoogle)) { + await matrix.store.setItemBool(SettingKeys.showNoGoogle, false); + } + } + return; + } + await setupPusher( + clientName: clientName, + gatewayUrl: AppConfig.pushNotificationsGatewayUrl, + token: fcmToken, + ); + _firebaseMessaging.configure( - onMessage: _onMessage, - onBackgroundMessage: _onMessage, + onMessage: _onFcmMessage, + onBackgroundMessage: _onFcmMessage, onResume: goToRoom, onLaunch: goToRoom, ); Logs().i('[Push] Firebase initialized'); - return; } - static Future _onMessage(Map message) async { - try { - final data = message['data'] ?? message; - final String roomId = data['room_id']; - final String eventId = data['event_id']; - final int unread = json.decode(data['counts'])['unread']; - if ((roomId?.isEmpty ?? true) || - (eventId?.isEmpty ?? true) || - unread == 0) { - await _flutterLocalNotificationsPlugin.cancelAll(); - return null; + static Future setupPusher({ + String clientName, + String gatewayUrl, + String token, + Set oldTokens, + }) async { + oldTokens ??= {}; + final client = matrix.client; + final pushers = await client.requestPushers().catchError((e) { + Logs().w('[Push] Unable to request pushers', e); + return []; + }); + var setNewPusher = false; + if (gatewayUrl != null && token != null && clientName != null) { + final currentPushers = pushers.where((pusher) => pusher.pushkey == token); + if (currentPushers.length == 1 && + currentPushers.first.kind == 'http' && + currentPushers.first.appId == AppConfig.pushNotificationsAppId && + currentPushers.first.appDisplayName == clientName && + currentPushers.first.deviceDisplayName == client.deviceName && + currentPushers.first.lang == 'en' && + currentPushers.first.data.url.toString() == gatewayUrl && + currentPushers.first.data.format == + AppConfig.pushNotificationsPusherFormat) { + Logs().i('[Push] Pusher already set'); + } else { + oldTokens.add(token); + setNewPusher = true; } - if (context != null && matrix.activeRoomId == roomId) { - Logs().i('[Push] New clearing push'); - return null; + } + for (final pusher in pushers) { + if (oldTokens.contains(pusher.pushkey)) { + pusher.kind = null; + try { + await client.setPusher( + pusher, + append: true, + ); + Logs().i('[Push] Removed legacy pusher for this device'); + } catch (err) { + Logs().w('[Push] Failed to remove old pusher', err); + } } - Logs().i('[Push] New message received'); - // FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092 - // Locked on EN until issue resolved - final i18n = context == null ? L10nEn() : L10n.of(context); - - // Get the client - Client client; - var tempClient = false; + } + if (setNewPusher) { try { - client = matrix.client; - } catch (_) { - client = null; - } - if (client == null) { - tempClient = true; - final platform = kIsWeb ? 'Web' : Platform.operatingSystem; - final clientName = 'FluffyChat $platform'; - client = Client(clientName, databaseBuilder: getDatabase)..init(); - Logs().i('[Push] Use a temp client'); - await client.onLoginStateChanged.stream - .firstWhere((l) => l == LoginState.logged) - .timeout( - Duration(seconds: 5), - ); - } - - // Get the room - var room = client.getRoomById(roomId); - if (room == null) { - Logs().i('[Push] Wait for the room'); - await client.onRoomUpdate.stream - .where((u) => u.id == roomId) - .first - .timeout(Duration(seconds: 5)); - Logs().i('[Push] Room found'); - room = client.getRoomById(roomId); - if (room == null) return null; - } - - // Get the event - var event = await client.database.getEventById(client.id, eventId, room); - if (event == null) { - Logs().i('[Push] Wait for the event'); - final eventUpdate = await client.onEvent.stream - .where((u) => u.content['event_id'] == eventId) - .first - .timeout(Duration(seconds: 5)); - Logs().i('[Push] Event found'); - event = Event.fromJson(eventUpdate.content, room); - if (room == null) return null; - } - - // Count all unread events - var unreadEvents = 0; - client.rooms - .forEach((Room room) => unreadEvents += room.notificationCount); - - // Calculate title - final title = unread > 1 - ? i18n.unreadMessagesInChats( - unreadEvents.toString(), unread.toString()) - : i18n.unreadMessages(unreadEvents.toString()); - - // Calculate the body - final body = event.getLocalizedBody( - MatrixLocals(i18n), - withSenderNamePrefix: true, - hideReply: true, - ); - - // The person object for the android message style notification - final person = Person( - name: room.getLocalizedDisplayname(MatrixLocals(i18n)), - icon: room.avatar == null - ? null - : BitmapFilePathAndroidIcon( - await downloadAndSaveAvatar( - room.avatar, - client, - width: 126, - height: 126, - ), - ), - ); - - // Show notification - var androidPlatformChannelSpecifics = AndroidNotificationDetails( - AppConfig.pushNotificationsChannelId, - AppConfig.pushNotificationsChannelName, - AppConfig.pushNotificationsChannelDescription, - styleInformation: MessagingStyleInformation( - person, - conversationTitle: title, - messages: [ - Message( - body, - event.originServerTs, - person, - ) - ], + await client.setPusher( + Pusher( + token, + AppConfig.pushNotificationsAppId, + clientName, + client.deviceName, + 'en', + PusherData( + url: Uri.parse(gatewayUrl), + format: AppConfig.pushNotificationsPusherFormat, + ), + kind: 'http', ), - importance: Importance.max, - priority: Priority.high, - ticker: i18n.newMessageInFluffyChat); - var iOSPlatformChannelSpecifics = IOSNotificationDetails(); - var platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics, - ); - await _flutterLocalNotificationsPlugin.show( - 0, - room.getLocalizedDisplayname(MatrixLocals(i18n)), - body, - platformChannelSpecifics, - payload: roomId); - - if (tempClient) { - await client.dispose(); - client = null; - Logs().i('[Push] Temp client disposed'); + append: false, + ); + } catch (e, s) { + Logs().e('[Push] Unable to set pushers', e, s); } + } + } + + static Future onUnifiedPushMessage(String payload) async { + Map data; + try { + data = Map.from(json.decode(payload)['notification']); + await _onMessage(data); + } catch (e, s) { + Logs().e('[Push] Failed to display message', e, s); + await _showDefaultNotification(data); + } + } + + static Future onRemoveEndpoint() async { + Logs().i('[Push] Removing UnifiedPush endpoint...'); + // we need our own store object as it is likely that we don't have a context here + final store = Store(); + final oldEndpoint = await store.getItem(SettingKeys.unifiedPushEndpoint); + await store.setItemBool(SettingKeys.unifiedPushRegistered, false); + await store.deleteItem(SettingKeys.unifiedPushEndpoint); + if (matrix != null && (oldEndpoint?.isNotEmpty ?? false)) { + // remove the old pusher + await setupPusher( + oldTokens: {oldEndpoint}, + ); + } + } + + static Future onBackgroundNewEndpoint(String endpoint) async { + // just remove the old endpoint. we'll deal with this when the app next starts up + await onRemoveEndpoint(); + } + + static Future setupUnifiedPush(String clientName) async { + final onNewEndpoint = (String newEndpoint) async { + if (newEndpoint?.isEmpty ?? true) { + await onRemoveEndpoint(); + return; + } + var endpoint = + 'https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify'; + try { + final url = Uri.parse(newEndpoint) + .replace( + path: '/_matrix/push/v1/notify', + query: '', + ) + .toString() + .split('?') + .first; + final res = json.decode(utf8.decode((await http.get(url)).bodyBytes)); + if (res['gateway'] == 'matrix') { + endpoint = url; + } + } catch (e) { + Logs().i('[Push] No self-hosted unified push gateway present: ' + + newEndpoint); + } + Logs().i('[Push] UnifiedPush using endpoint ' + endpoint); + final oldTokens = {}; + try { + final fcmToken = await _firebaseMessaging.getToken(); + oldTokens.add(fcmToken); + } catch (_) {} + await setupPusher( + clientName: clientName, + gatewayUrl: endpoint, + token: newEndpoint, + oldTokens: oldTokens, + ); + await matrix.store.setItem(SettingKeys.unifiedPushEndpoint, newEndpoint); + await matrix.store.setItemBool(SettingKeys.unifiedPushRegistered, true); + }; + await UnifiedPush.initialize( + onNewEndpoint, // new endpoint + onRemoveEndpoint, // registration failed + onRemoveEndpoint, // registration removed + onRemoveEndpoint, // unregistered + onUnifiedPushMessage, // foreground message + onBackgroundNewEndpoint, // background new endpoint (be static) + onRemoveEndpoint, // background unregistered (be static) + onUnifiedPushMessage, // background push message (be static) + ); + if (!(await matrix.store + .getItemBool(SettingKeys.unifiedPushRegistered, false))) { + Logs().i('[Push] UnifiedPush not registered, attempting to do so...'); + await UnifiedPush.registerAppWithDialog(); + } else { + // make sure the endpoint is up-to-date etc. + await onNewEndpoint( + await matrix.store.getItem(SettingKeys.unifiedPushEndpoint)); + } + } + + static Future _onFcmMessage(Map message) async { + Map data; + try { + data = Map.from(message['data'] ?? message); + await _onMessage(data); } catch (e, s) { Logs().e('[Push] Error while processing notification', e, s); - await _showDefaultNotification(message); + await _showDefaultNotification(data); + } + return null; + } + + static Future _onMessage(Map data) async { + final String roomId = data['room_id']; + final String eventId = data['event_id']; + final unread = ((data['counts'] is String + ? json.decode(data.tryGet('counts', '{}')) + : data.tryGet>( + 'counts', {})) as Map) + .tryGet('unread'); + if ((roomId?.isEmpty ?? true) || + (eventId?.isEmpty ?? true) || + unread == 0) { + Logs().i('[Push] New clearing push'); + await _flutterLocalNotificationsPlugin.cancelAll(); + return null; + } + if (matrix != null && matrix?.activeRoomId == roomId) { + return null; + } + Logs().i('[Push] New message received'); + // FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092 + // Locked on EN until issue resolved + final i18n = context == null ? L10nEn() : L10n.of(context); + + // Get the client + var client = matrix?.client; + var tempClient = false; + if (client == null) { + tempClient = true; + final platform = kIsWeb ? 'Web' : Platform.operatingSystem; + final clientName = 'FluffyChat $platform'; + client = Client(clientName, databaseBuilder: getDatabase)..init(); + Logs().i('[Push] Use a temp client'); + await client.onLoginStateChanged.stream + .firstWhere((l) => l == LoginState.logged) + .timeout( + Duration(seconds: 20), + ); + } + + final roomFuture = + client.onRoomUpdate.stream.where((u) => u.id == roomId).first; + final eventFuture = client.onEvent.stream + .where((u) => u.content['event_id'] == eventId) + .first; + + // Get the room + var room = client.getRoomById(roomId); + if (room == null) { + Logs().i('[Push] Wait for the room'); + await roomFuture.timeout(Duration(seconds: 5)); + Logs().i('[Push] Room found'); + room = client.getRoomById(roomId); + if (room == null) return null; + } else { + // cancel the future + // ignore: unawaited_futures + roomFuture.timeout(Duration(seconds: 0)).catchError((_) => null); + } + + // Get the event + var event = await client.database.getEventById(client.id, eventId, room); + if (event == null) { + Logs().i('[Push] Wait for the event'); + final eventUpdate = await eventFuture.timeout(Duration(seconds: 5)); + Logs().i('[Push] Event found'); + event = Event.fromJson(eventUpdate.content, room); + if (room == null) return null; + } else { + // cancel the future + // ignore: unawaited_futures + eventFuture.timeout(Duration(seconds: 0)).catchError((_) => null); + } + + // Count all unread events + var unreadEvents = 0; + client.rooms.forEach((Room room) => unreadEvents += room.notificationCount); + + // Calculate title + final title = unread > 1 + ? i18n.unreadMessagesInChats(unreadEvents.toString(), unread.toString()) + : i18n.unreadMessages(unreadEvents.toString()); + + // Calculate the body + final body = event.getLocalizedBody( + MatrixLocals(i18n), + withSenderNamePrefix: true, + hideReply: true, + ); + + // The person object for the android message style notification + final person = Person( + name: room.getLocalizedDisplayname(MatrixLocals(i18n)), + icon: room.avatar == null + ? null + : BitmapFilePathAndroidIcon( + await downloadAndSaveAvatar( + room.avatar, + client, + width: 126, + height: 126, + ), + ), + ); + + // Show notification + var androidPlatformChannelSpecifics = _getAndroidNotificationDetails( + styleInformation: MessagingStyleInformation( + person, + conversationTitle: title, + messages: [ + Message( + body, + event.originServerTs, + person, + ) + ], + ), + ticker: i18n.newMessageInFluffyChat, + ); + var iOSPlatformChannelSpecifics = IOSNotificationDetails(); + var platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); + await _flutterLocalNotificationsPlugin.show( + 0, + room.getLocalizedDisplayname(MatrixLocals(i18n)), + body, + platformChannelSpecifics, + payload: roomId); + + if (tempClient) { + await client.dispose(); + client = null; + Logs().i('[Push] Temp client disposed'); } return null; } static Future _showDefaultNotification( - Map message) async { + Map data) async { try { var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // Init notifications framework @@ -308,26 +450,20 @@ abstract class FirebaseController { // Locked on en for now //final l10n = L10n(Platform.localeName); final l10n = L10nEn(); - - // Notification data and matrix data - Map data = message['data'] ?? message; String eventID = data['event_id']; String roomID = data['room_id']; - final int unread = data.containsKey('counts') - ? json.decode(data['counts'])['unread'] - : 1; + final unread = ((data['counts'] is String + ? json.decode(data.tryGet('counts', '{}')) + : data.tryGet>( + 'counts', {})) as Map) + .tryGet('unread', 1); await flutterLocalNotificationsPlugin.cancelAll(); if (unread == 0 || roomID == null || eventID == null) { return; } // Display notification - var androidPlatformChannelSpecifics = AndroidNotificationDetails( - AppConfig.pushNotificationsChannelId, - AppConfig.pushNotificationsChannelName, - AppConfig.pushNotificationsChannelDescription, - importance: Importance.max, - priority: Priority.high); + var androidPlatformChannelSpecifics = _getAndroidNotificationDetails(); var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, @@ -372,4 +508,21 @@ abstract class FirebaseController { Logs().i('Settings registered: $settings'); }); } + + static AndroidNotificationDetails _getAndroidNotificationDetails( + {MessagingStyleInformation styleInformation, String ticker}) { + final color = + context != null ? Theme.of(context).primaryColor : Color(0xFF5625BA); + + return AndroidNotificationDetails( + AppConfig.pushNotificationsChannelId, + AppConfig.pushNotificationsChannelName, + AppConfig.pushNotificationsChannelDescription, + styleInformation: styleInformation, + importance: Importance.max, + priority: Priority.high, + ticker: ticker, + color: color, + ); + } } diff --git a/pubspec.lock b/pubspec.lock index a092e5a1..cef0e02f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -910,10 +910,12 @@ packages: receive_sharing_intent: dependency: "direct main" description: - name: receive_sharing_intent - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.3" + path: "." + ref: "107ea4ae3c3da15be4e6d3337623b69cc2e04c68" + resolved-ref: "107ea4ae3c3da15be4e6d3337623b69cc2e04c68" + url: "https://github.com/radvansky-tomas/receive_sharing_intent.git" + source: git + version: "1.4.2" rxdart: dependency: transitive description: @@ -1150,6 +1152,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0-nullsafety.3" + unifiedpush: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "7092bcc846f6f919c7dfb58f2c30bb45ca7231c0" + url: "https://github.com/UnifiedPush/flutter-connector.git" + source: git + version: "0.0.1" universal_html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0eaf5d15..922efed7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,12 @@ dependencies: url: https://gitlab.com/famedly/famedlysdk.git ref: main + unifiedpush: +# path: /home/sorunome/repos/gotify/flutter_unified_push + git: + url: https://github.com/UnifiedPush/flutter-connector.git + ref: main + localstorage: ^3.0.6+9 file_picker_cross: 4.2.2 image_picker: ^0.6.7+21 @@ -35,7 +41,11 @@ dependencies: flutter_secure_storage: ^3.3.5 http: ^0.12.2 universal_html: ^1.2.4 - receive_sharing_intent: ^1.4.3 + receive_sharing_intent: + # see https://github.com/KasemJaffer/receive_sharing_intent/pull/115 + git: + url: https://github.com/radvansky-tomas/receive_sharing_intent.git + ref: 107ea4ae3c3da15be4e6d3337623b69cc2e04c68 flutter_slidable: ^0.5.7 flutter_sound_lite: ^7.5.3+1 open_file: ^3.0.3