import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations_de.dart'; import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future customLocalPushNotification(String title, String msg) async { final androidNotificationDetails = AndroidNotificationDetails( '1', 'System', channelDescription: 'System messages.', importance: Importance.high, priority: Priority.high, styleInformation: BigTextStyleInformation(msg), icon: 'ic_launcher_foreground', ); const darwinNotificationDetails = DarwinNotificationDetails(); final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); final id = Random.secure().nextInt(9999); await flutterLocalNotificationsPlugin.show( id, title, msg, notificationDetails, ); } Future showPushNotificationFromServerMessages( int fromUserId, EncryptedContent encryptedContent, ) async { final pushData = await getPushNotificationFromEncryptedContent( null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user. null, encryptedContent, ); if (pushData != null) { final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); for (final pushUser in pushUsers) { if (pushUser.userId.toInt() == fromUserId) { String? groupId; if (encryptedContent.hasGroupId()) { groupId = encryptedContent.groupId; } return showLocalPushNotification(pushUser, pushData, groupId: groupId); } } } } Future tryDecryptMessage( List key, EncryptedPushNotification push, ) async { try { final chacha20 = FlutterChacha20.poly1305Aead(); final secretKeyData = SecretKeyData(key); final secretBox = SecretBox( push.ciphertext, nonce: push.nonce, mac: Mac(push.mac), ); final plaintext = await chacha20.decrypt( secretBox, secretKey: secretKeyData, ); return PushNotification.fromBuffer(plaintext); } catch (e) { // this error is allowed to happen... return null; } } Future showLocalPushNotification( PushUser pushUser, PushNotification pushNotification, { String? groupId, }) async { String? title; String? body; // do not show notification for blocked users... if (pushUser.blocked) { Log.info('Blocked a message from a blocked user!'); return; } var targetGroupId = groupId; if (targetGroupId == null) { try { if (userService.isUserCreated) { targetGroupId = getUUIDforDirectChat( userService.currentUser.userId, pushUser.userId.toInt(), ); } } catch (_) {} } if (targetGroupId != null) { try { final currentUri = routerProvider.routerDelegate.currentConfiguration.uri; final isResumed = WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed; if (isResumed && currentUri.path.contains(targetGroupId)) { Log.info( 'Suppressing local push notification because chat with group $targetGroupId is currently open.', ); return; } } catch (e) { Log.error('Error checking current route: $e'); } } title = pushUser.displayName; body = getPushNotificationText(pushNotification); if (body == '') { Log.error('No push notification type defined!'); } FilePathAndroidBitmap? styleInformation; final avatarPath = await getAvatarIcon(pushUser.userId.toInt()); if (avatarPath != null) { styleInformation = FilePathAndroidBitmap(avatarPath); } final lang = getLocalizations(); final androidNotificationDetails = AndroidNotificationDetails( '0', lang.notificationCategoryMessageTitle, channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, ticker: 'You got a new message.', largeIcon: styleInformation, icon: 'ic_launcher_foreground', groupKey: 'com.twonly.messages', ); const darwinNotificationDetails = DarwinNotificationDetails(); final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); String? payload; if (groupId != null && (pushNotification.kind == PushKind.TEXT || pushNotification.kind == PushKind.RESPONSE || pushNotification.kind == PushKind.REACTION_TO_AUDIO || pushNotification.kind == PushKind.STORED_MEDIA_FILE || pushNotification.kind == PushKind.REACTION_TO_IMAGE || pushNotification.kind == PushKind.REACTION_TO_TEXT || pushNotification.kind == PushKind.REACTION_TO_VIDEO)) { payload = Routes.chatsMessages(groupId); } else { payload = Routes.chats; } await flutterLocalNotificationsPlugin.show( // Invalid argument (id): must fit within the size of a 32-bit integer pushUser.userId.toInt() % 2147483647, title, body, notificationDetails, payload: payload, ); if (Platform.isAndroid) { final summaryAndroidDetails = AndroidNotificationDetails( '0', lang.notificationCategoryMessageTitle, channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, groupKey: 'com.twonly.messages', setAsGroupSummary: true, icon: 'ic_launcher_foreground', ); final summaryNotificationDetails = NotificationDetails( android: summaryAndroidDetails, ); await flutterLocalNotificationsPlugin.show( 0, lang.notificationCategoryMessageTitle, '', summaryNotificationDetails, payload: Routes.chats, ); } } Future getAvatarIcon(int contactId) async { final avatarsDirectory = Directory('${AppEnvironment.cacheDir}/avatars'); final filePath = '${avatarsDirectory.path}/$contactId.png'; final file = File(filePath); if (file.existsSync()) { return filePath; } return null; } AppLocalizations getLocalizations() { final systemLanguage = Platform.localeName; if (systemLanguage.contains('de')) return AppLocalizationsDe(); return AppLocalizationsEn(); } String getPushNotificationText(PushNotification pushNotification) { final lang = getLocalizations(); var inGroup = ''; if (pushNotification.hasAdditionalContent()) { inGroup = ' ${lang.notificationFillerIn} ${pushNotification.additionalContent}'; } final pushNotificationText = { PushKind.TEXT.name: lang.notificationText(inGroup), PushKind.TWONLY.name: lang.notificationTwonly(inGroup), PushKind.VIDEO.name: lang.notificationVideo(inGroup), PushKind.IMAGE.name: lang.notificationImage(inGroup), PushKind.AUDIO.name: lang.notificationAudio(inGroup), PushKind.CONTACT_REQUEST.name: lang.notificationContactRequest, PushKind.ACCEPT_REQUEST.name: lang.notificationAcceptRequest, PushKind.STORED_MEDIA_FILE.name: lang.notificationStoredMediaFile, PushKind.REACTION.name: lang.notificationReaction, PushKind.REOPENED_MEDIA.name: lang.notificationReopenedMedia, PushKind.REACTION_TO_VIDEO.name: lang.notificationReactionToVideo( pushNotification.additionalContent, ), PushKind.REACTION_TO_AUDIO.name: lang.notificationReactionToAudio( pushNotification.additionalContent, ), PushKind.REACTION_TO_TEXT.name: lang.notificationReactionToText( pushNotification.additionalContent, ), PushKind.REACTION_TO_IMAGE.name: lang.notificationReactionToImage( pushNotification.additionalContent, ), PushKind.RESPONSE.name: lang.notificationResponse(inGroup), PushKind.ADDED_TO_GROUP.name: lang.notificationAddedToGroup( pushNotification.additionalContent, ), }; return pushNotificationText[pushNotification.kind.name] ?? ''; }