Merge branch 'soru/unified-push' into 'main'

feat: Add unified push as push provider

See merge request famedly/fluffychat!341
This commit is contained in:
Krille Fear 2021-01-28 14:32:07 +00:00
commit fd08a6a4db
11 changed files with 458 additions and 239 deletions

View File

@ -146,6 +146,45 @@ Example B:
3. For testing just run a regular build without extras 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 # Special thanks to
* <a href="https://github.com/fabiyamada">Fabiyamada</a> is a graphics designer from Brasil and has made the fluffychat logo and the banner. Big thanks for her great designs. * <a href="https://github.com/fabiyamada">Fabiyamada</a> 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. * Also thanks to all translators and testers! With your help, fluffychat is now available in more than 12 languages.
* <a href="https://github.com/googlefonts/noto-emoji/">Noto Emoji Font</a> for the awesome emojis. * <a href="https://github.com/googlefonts/noto-emoji/">Noto Emoji Font</a> for the awesome emojis.

View File

@ -28,4 +28,4 @@ class Application : FlutterApplication(), PluginRegistrantCallback {
FlutterSecureStoragePlugin.registerWith(registry.registrarFor("com.it_nomads.fluttersecurestorage")); FlutterSecureStoragePlugin.registerWith(registry.registrarFor("com.it_nomads.fluttersecurestorage"));
} }
} }
} }

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { 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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
} }

View File

@ -1,6 +1,5 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@ -0,0 +1 @@
include ':app'

View File

@ -340,10 +340,9 @@ class MatrixState extends State<Matrix> {
widget.apl.currentState.pushNamedAndRemoveAllOthers('/'); widget.apl.currentState.pushNamedAndRemoveAllOthers('/');
if (loginState == LoginState.logged) { if (loginState == LoginState.logged) {
FirebaseController.context = context; FirebaseController.context = context;
FirebaseController.setupFirebase( FirebaseController.matrix = this;
this, FirebaseController.setupFirebase(clientName)
clientName, .catchError(SentryController.captureException);
).catchError(SentryController.captureException);
} }
} }
}); });

View File

@ -12,4 +12,7 @@ abstract class SettingKeys {
static const String showNoPid = 'chat.fluffy.show_no_pid'; static const String showNoPid = 'chat.fluffy.show_no_pid';
static const String databasePassword = 'database-password'; static const String databasePassword = 'database-password';
static const String appLockKey = 'chat.fluffy.app_lock'; static const String appLockKey = 'chat.fluffy.app_lock';
static const String unifiedPushRegistered =
'chat.fluffy.unifiedpush.registered';
static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint';
} }

View File

@ -92,6 +92,10 @@ class Store {
return await secureStorage.write(key: key, value: value); return await secureStorage.write(key: key, value: value);
} }
Future<void> setItemBool(String key, bool value) async {
await setItem(key, value.toString());
}
Future<void> deleteItem(String key) async { Future<void> deleteItem(String key) async {
if (!PlatformInfos.isMobile) { if (!PlatformInfos.isMobile) {
await _setupLocalStorage(); await _setupLocalStorage();

View File

@ -13,6 +13,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_gen/gen_l10n/l10n_en.dart'; import 'package:flutter_gen/gen_l10n/l10n_en.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.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 '../components/matrix.dart';
import '../config/setting_keys.dart'; import '../config/setting_keys.dart';
@ -26,91 +28,22 @@ abstract class FirebaseController {
static BuildContext context; static BuildContext context;
static MatrixState matrix; static MatrixState matrix;
static Future<void> setupFirebase( static Future<void> setupFirebase(String clientName) async {
MatrixState matrix, String clientName) async {
FirebaseController.matrix = matrix;
if (!PlatformInfos.isMobile) return; if (!PlatformInfos.isMobile) return;
final client = matrix.client;
if (Platform.isIOS) iOS_Permission(); 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 <Pusher>[];
});
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 { Function goToRoom = (dynamic message) async {
try { try {
String roomId; String roomId;
if (message is String && message[0] == '{') {
message = json.decode(message);
}
if (message is String) { if (message is String) {
roomId = message; roomId = message;
} else if (message is Map) { } 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'); if (roomId?.isEmpty ?? true) throw ('Bad roomId');
await matrix.widget.apl.currentState await matrix.widget.apl.currentState
@ -136,162 +69,371 @@ abstract class FirebaseController {
await _flutterLocalNotificationsPlugin.initialize(initializationSettings, await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: goToRoom); 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( _firebaseMessaging.configure(
onMessage: _onMessage, onMessage: _onFcmMessage,
onBackgroundMessage: _onMessage, onBackgroundMessage: _onFcmMessage,
onResume: goToRoom, onResume: goToRoom,
onLaunch: goToRoom, onLaunch: goToRoom,
); );
Logs().i('[Push] Firebase initialized'); Logs().i('[Push] Firebase initialized');
return;
} }
static Future<dynamic> _onMessage(Map<String, dynamic> message) async { static Future<void> setupPusher({
try { String clientName,
final data = message['data'] ?? message; String gatewayUrl,
final String roomId = data['room_id']; String token,
final String eventId = data['event_id']; Set<String> oldTokens,
final int unread = json.decode(data['counts'])['unread']; }) async {
if ((roomId?.isEmpty ?? true) || oldTokens ??= <String>{};
(eventId?.isEmpty ?? true) || final client = matrix.client;
unread == 0) { final pushers = await client.requestPushers().catchError((e) {
await _flutterLocalNotificationsPlugin.cancelAll(); Logs().w('[Push] Unable to request pushers', e);
return null; return <Pusher>[];
});
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'); for (final pusher in pushers) {
return null; 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 if (setNewPusher) {
// Locked on EN until issue resolved
final i18n = context == null ? L10nEn() : L10n.of(context);
// Get the client
Client client;
var tempClient = false;
try { try {
client = matrix.client; await client.setPusher(
} catch (_) { Pusher(
client = null; token,
} AppConfig.pushNotificationsAppId,
if (client == null) { clientName,
tempClient = true; client.deviceName,
final platform = kIsWeb ? 'Web' : Platform.operatingSystem; 'en',
final clientName = 'FluffyChat $platform'; PusherData(
client = Client(clientName, databaseBuilder: getDatabase)..init(); url: Uri.parse(gatewayUrl),
Logs().i('[Push] Use a temp client'); format: AppConfig.pushNotificationsPusherFormat,
await client.onLoginStateChanged.stream ),
.firstWhere((l) => l == LoginState.logged) kind: 'http',
.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,
)
],
), ),
importance: Importance.max, append: false,
priority: Priority.high, );
ticker: i18n.newMessageInFluffyChat); } catch (e, s) {
var iOSPlatformChannelSpecifics = IOSNotificationDetails(); Logs().e('[Push] Unable to set pushers', e, s);
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');
} }
}
}
static Future<void> onUnifiedPushMessage(String payload) async {
Map<String, dynamic> data;
try {
data = Map<String, dynamic>.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<void> 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<void> onBackgroundNewEndpoint(String endpoint) async {
// just remove the old endpoint. we'll deal with this when the app next starts up
await onRemoveEndpoint();
}
static Future<void> 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 = <String>{};
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<dynamic> _onFcmMessage(Map<String, dynamic> message) async {
Map<String, dynamic> data;
try {
data = Map<String, dynamic>.from(message['data'] ?? message);
await _onMessage(data);
} catch (e, s) { } catch (e, s) {
Logs().e('[Push] Error while processing notification', e, s); Logs().e('[Push] Error while processing notification', e, s);
await _showDefaultNotification(message); await _showDefaultNotification(data);
}
return null;
}
static Future<dynamic> _onMessage(Map<String, dynamic> data) async {
final String roomId = data['room_id'];
final String eventId = data['event_id'];
final unread = ((data['counts'] is String
? json.decode(data.tryGet<String>('counts', '{}'))
: data.tryGet<Map<String, dynamic>>(
'counts', <String, dynamic>{})) as Map<String, dynamic>)
.tryGet<int>('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; return null;
} }
static Future<dynamic> _showDefaultNotification( static Future<dynamic> _showDefaultNotification(
Map<String, dynamic> message) async { Map<String, dynamic> data) async {
try { try {
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Init notifications framework // Init notifications framework
@ -308,26 +450,20 @@ abstract class FirebaseController {
// Locked on en for now // Locked on en for now
//final l10n = L10n(Platform.localeName); //final l10n = L10n(Platform.localeName);
final l10n = L10nEn(); final l10n = L10nEn();
// Notification data and matrix data
Map<dynamic, dynamic> data = message['data'] ?? message;
String eventID = data['event_id']; String eventID = data['event_id'];
String roomID = data['room_id']; String roomID = data['room_id'];
final int unread = data.containsKey('counts') final unread = ((data['counts'] is String
? json.decode(data['counts'])['unread'] ? json.decode(data.tryGet<String>('counts', '{}'))
: 1; : data.tryGet<Map<String, dynamic>>(
'counts', <String, dynamic>{})) as Map<String, dynamic>)
.tryGet<int>('unread', 1);
await flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
if (unread == 0 || roomID == null || eventID == null) { if (unread == 0 || roomID == null || eventID == null) {
return; return;
} }
// Display notification // Display notification
var androidPlatformChannelSpecifics = AndroidNotificationDetails( var androidPlatformChannelSpecifics = _getAndroidNotificationDetails();
AppConfig.pushNotificationsChannelId,
AppConfig.pushNotificationsChannelName,
AppConfig.pushNotificationsChannelDescription,
importance: Importance.max,
priority: Priority.high);
var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails( var platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics, android: androidPlatformChannelSpecifics,
@ -372,4 +508,21 @@ abstract class FirebaseController {
Logs().i('Settings registered: $settings'); 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,
);
}
} }

View File

@ -910,10 +910,12 @@ packages:
receive_sharing_intent: receive_sharing_intent:
dependency: "direct main" dependency: "direct main"
description: description:
name: receive_sharing_intent path: "."
url: "https://pub.dartlang.org" ref: "107ea4ae3c3da15be4e6d3337623b69cc2e04c68"
source: hosted resolved-ref: "107ea4ae3c3da15be4e6d3337623b69cc2e04c68"
version: "1.4.3" url: "https://github.com/radvansky-tomas/receive_sharing_intent.git"
source: git
version: "1.4.2"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1150,6 +1152,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0-nullsafety.3" 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: universal_html:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -15,6 +15,12 @@ dependencies:
url: https://gitlab.com/famedly/famedlysdk.git url: https://gitlab.com/famedly/famedlysdk.git
ref: main 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 localstorage: ^3.0.6+9
file_picker_cross: 4.2.2 file_picker_cross: 4.2.2
image_picker: ^0.6.7+21 image_picker: ^0.6.7+21
@ -35,7 +41,11 @@ dependencies:
flutter_secure_storage: ^3.3.5 flutter_secure_storage: ^3.3.5
http: ^0.12.2 http: ^0.12.2
universal_html: ^1.2.4 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_slidable: ^0.5.7
flutter_sound_lite: ^7.5.3+1 flutter_sound_lite: ^7.5.3+1
open_file: ^3.0.3 open_file: ^3.0.3