opening chat if clicked on the notification

This commit is contained in:
otsmr 2026-04-06 00:02:55 +02:00
parent aa26766bdf
commit 267e2bd376
12 changed files with 200 additions and 94 deletions

View file

@ -3,6 +3,7 @@
## 0.1.2 ## 0.1.2
- New: Developer settings to reduce flames - New: Developer settings to reduce flames
- New: Clicking on “Text Notifications” will now open the chat directly (Android only)
- Improve: Improved troubleshooting for issues with push notifications - Improve: Improved troubleshooting for issues with push notifications
- Fix: Flash not activated when starting a video recording - Fix: Flash not activated when starting a video recording
- Fix: Problem sending media when a recipient has deleted their account. - Fix: Problem sending media when a recipient has deleted their account.

View file

@ -56,13 +56,20 @@ PODS:
- FirebaseAnalytics (~> 12.9.0) - FirebaseAnalytics (~> 12.9.0)
- Firebase/CoreOnly (12.9.0): - Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.9.0)
- Firebase/Installations (12.9.0):
- Firebase/CoreOnly
- FirebaseInstallations (~> 12.9.0)
- Firebase/Messaging (12.9.0): - Firebase/Messaging (12.9.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.9.0) - FirebaseMessaging (~> 12.9.0)
- firebase_core (4.5.0): - firebase_app_installations (0.4.1):
- Firebase/Installations (= 12.9.0)
- firebase_core
- Flutter
- firebase_core (4.6.0):
- Firebase/CoreOnly (= 12.9.0) - Firebase/CoreOnly (= 12.9.0)
- Flutter - Flutter
- firebase_messaging (16.1.2): - firebase_messaging (16.1.3):
- Firebase/Messaging (= 12.9.0) - Firebase/Messaging (= 12.9.0)
- firebase_core - firebase_core
- Flutter - Flutter
@ -278,17 +285,17 @@ PODS:
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- restart_app (1.7.3): - restart_app (1.7.3):
- Flutter - Flutter
- SDWebImage (5.21.6): - SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.6) - SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.6) - SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0): - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2) - Sentry/HybridSDK (8.58.0)
- sentry_flutter (9.14.0): - sentry_flutter (9.16.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.56.2) - Sentry/HybridSDK (= 8.58.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -297,32 +304,32 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.51.1): - sqlite3 (3.52.0):
- sqlite3/common (= 3.51.1) - sqlite3/common (= 3.52.0)
- sqlite3/common (3.51.1) - sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.51.1): - sqlite3/dbstatvtab (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.51.1): - sqlite3/fts5 (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.51.1): - sqlite3/math (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.51.1): - sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.51.1): - sqlite3/rtree (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.51.1): - sqlite3/session (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.51.1) - sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
- sqlite3/session - sqlite3/session
- SwiftProtobuf (1.34.1) - SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
@ -343,6 +350,7 @@ DEPENDENCIES:
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase - Firebase
- firebase_app_installations (from `.symlinks/plugins/firebase_app_installations/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- FirebaseCore - FirebaseCore
@ -430,6 +438,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/emoji_picker_flutter/ios" :path: ".symlinks/plugins/emoji_picker_flutter/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
firebase_app_installations:
:path: ".symlinks/plugins/firebase_app_installations/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging: firebase_messaging:
@ -493,7 +503,7 @@ SPEC CHECKSUMS:
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@ -502,8 +512,9 @@ SPEC CHECKSUMS:
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56 Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_core: afac1aac13c931e0401c7e74ed1276112030efab firebase_app_installations: 1abd8d071ea2022d7888f7a9713710c37136ff91
firebase_messaging: 7cb2727feb789751fc6936bcc8e08408970e2820 firebase_core: 8e6f58412ca227827c366b92e7cee047a2148c60
firebase_messaging: c3aa897e0d40109cfb7927c40dc0dea799863f3b
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352 FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8 FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72 FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
@ -544,16 +555,16 @@ SPEC CHECKSUMS:
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2 restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
sentry_flutter: 841fa2fe08dc72eb95e2320b76e3f751f3400cf5 sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a

View file

@ -133,9 +133,7 @@ class ApiService {
return; return;
} }
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
Log.info('Starting reconnection timer with $_reconnectionDelay s delay');
reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async { reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async {
Log.info('Reconnection timer triggered');
reconnectionTimer = null; reconnectionTimer = null;
// only try to reconnect in case the app is in the foreground // only try to reconnect in case the app is in the foreground
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {

View file

@ -111,8 +111,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
); );
Uint8List? pushData; Uint8List? pushData;
if (pushNotification != null && receipt.retryCount <= 3) { if (pushNotification != null && receipt.retryCount <= 1) {
/// In case the message has to be resend more than three times, do not show a notification again... // Only show the push notification the first two time.
pushData = await encryptPushNotification( pushData = await encryptPushNotification(
receipt.contactId, receipt.contactId,
pushNotification, pushNotification,

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
@ -25,6 +26,7 @@ import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -164,7 +166,7 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
final ( final (
encryptedContent, encryptedContent,
plainTextContent, plainTextContent,
) = await handleEncryptedMessage( ) = await handleEncryptedMessageRaw(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
message.type, message.type,
@ -182,6 +184,9 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
encryptedContent: encryptedContent.writeToBuffer(), encryptedContent: encryptedContent.writeToBuffer(),
); );
receiptIdDB = const Value.absent(); receiptIdDB = const Value.absent();
} else {
// Message was successful processed
//
} }
} }
@ -206,19 +211,19 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
} }
} }
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
int fromUserId, int fromUserId,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
Message_Type messageType, Message_Type messageType,
String receiptId, String receiptId,
) async { ) async {
final (content, decryptionErrorType) = await signalDecryptMessage( final (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
messageType.value, messageType.value,
); );
if (content == null) { if (encryptedContent == null) {
return ( return (
null, null,
PlaintextContent() PlaintextContent()
@ -227,6 +232,27 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
final (a, b) = await handleEncryptedMessage(
fromUserId,
encryptedContent,
messageType,
receiptId,
);
if (Platform.isAndroid && a == null && b == null) {
// Message was handled without any error -> Show push notification to the user.
await showPushNotificationFromServerMessages(fromUserId, encryptedContent);
}
return (a, b);
}
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
int fromUserId,
EncryptedContent content,
Message_Type messageType,
String receiptId,
) async {
// We got a valid message fromUserId, so mark all messages which where // We got a valid message fromUserId, so mark all messages which where
// send to the user but not yet ACK for retransmission. All marked messages // send to the user but not yet ACK for retransmission. All marked messages
// will be either transmitted again after a new server connection (minimum 20 seconds). // will be either transmitted again after a new server connection (minimum 20 seconds).

View file

@ -176,6 +176,6 @@ bool isItPossibleToRestoreFlames(Group group) {
return group.maxFlameCounter > 2 && return group.maxFlameCounter > 2 &&
flameCounter < group.maxFlameCounter && flameCounter < group.maxFlameCounter &&
group.maxFlameCounterFrom!.isAfter( group.maxFlameCounterFrom!.isAfter(
clock.now().subtract(const Duration(days: 5)), clock.now().subtract(const Duration(days: 7)),
); );
} }

View file

@ -72,13 +72,19 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
try { try {
final task = VideoRenderData( final task = VideoRenderData(
video: EditorVideo.file(media.originalPath), videoSegments: [
imageBytes: media.overlayImagePath.readAsBytesSync(), VideoSegment(video: EditorVideo.file(media.originalPath)),
],
imageLayers: [
ImageLayer(image: EditorLayerImage.file(media.overlayImagePath)),
],
enableAudio: !media.removeAudio, enableAudio: !media.removeAudio,
); );
await ProVideoEditor.instance await ProVideoEditor.instance.renderVideoToFile(
.renderVideoToFile(media.ffmpegOutputPath.path, task); media.ffmpegOutputPath.path,
task,
);
if (Platform.isIOS || if (Platform.isIOS ||
media.ffmpegOutputPath.statSync().size >= 10_000_000 || media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
@ -115,8 +121,8 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024) final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
.toStringAsFixed(2); .toStringAsFixed(2);
final sizeTo = final sizeTo = (media.tempPath.statSync().size / 1024 / 1024)
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2); .toStringAsFixed(2);
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.', 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.',

View file

@ -6,10 +6,12 @@ import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.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:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage_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.dart';
import 'package:twonly/src/localization/generated/app_localizations_de.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/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/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -45,10 +47,34 @@ Future<void> customLocalPushNotification(String title, String msg) async {
); );
} }
Future<void> 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<void> handlePushData(String pushDataB64) async { Future<void> handlePushData(String pushDataB64) async {
try { try {
final pushData = final pushData = EncryptedPushNotification.fromBuffer(
EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64)); base64.decode(pushDataB64),
);
PushNotification? pushNotification; PushNotification? pushNotification;
PushUser? foundPushUser; PushUser? foundPushUser;
@ -121,8 +147,10 @@ Future<PushNotification?> tryDecryptMessage(
mac: Mac(push.mac), mac: Mac(push.mac),
); );
final plaintext = final plaintext = await chacha20.decrypt(
await chacha20.decrypt(secretBox, secretKey: secretKeyData); secretBox,
secretKey: secretKeyData,
);
return PushNotification.fromBuffer(plaintext); return PushNotification.fromBuffer(plaintext);
} catch (e) { } catch (e) {
// this error is allowed to happen... // this error is allowed to happen...
@ -132,8 +160,9 @@ Future<PushNotification?> tryDecryptMessage(
Future<void> showLocalPushNotification( Future<void> showLocalPushNotification(
PushUser pushUser, PushUser pushUser,
PushNotification pushNotification, PushNotification pushNotification, {
) async { String? groupId,
}) async {
String? title; String? title;
String? body; String? body;
@ -174,13 +203,25 @@ Future<void> showLocalPushNotification(
iOS: darwinNotificationDetails, iOS: darwinNotificationDetails,
); );
String? payload;
if (groupId != null &&
(pushNotification.kind == PushKind.text ||
pushNotification.kind == PushKind.response ||
pushNotification.kind == PushKind.reactionToAudio ||
pushNotification.kind == PushKind.reactionToImage ||
pushNotification.kind == PushKind.reactionToText ||
pushNotification.kind == PushKind.reactionToAudio)) {
payload = Routes.chatsMessages(groupId);
}
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
pushUser.userId.toInt() % // Invalid argument (id): must fit within the size of a 32-bit integer
2147483647, // Invalid argument (id): must fit within the size of a 32-bit integer pushUser.userId.toInt() % 2147483647,
title, title,
body, body,
notificationDetails, notificationDetails,
// payload: pushNotification.kind.name, payload: payload,
); );
} }
@ -259,17 +300,22 @@ String getPushNotificationText(PushNotification pushNotification) {
PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile,
PushKind.reaction.name: lang.notificationReaction, PushKind.reaction.name: lang.notificationReaction,
PushKind.reopenedMedia.name: lang.notificationReopenedMedia, PushKind.reopenedMedia.name: lang.notificationReopenedMedia,
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name: lang.notificationReactionToVideo(
lang.notificationReactionToVideo(pushNotification.additionalContent), pushNotification.additionalContent,
PushKind.reactionToAudio.name: ),
lang.notificationReactionToAudio(pushNotification.additionalContent), PushKind.reactionToAudio.name: lang.notificationReactionToAudio(
PushKind.reactionToText.name: pushNotification.additionalContent,
lang.notificationReactionToText(pushNotification.additionalContent), ),
PushKind.reactionToImage.name: PushKind.reactionToText.name: lang.notificationReactionToText(
lang.notificationReactionToImage(pushNotification.additionalContent), pushNotification.additionalContent,
),
PushKind.reactionToImage.name: lang.notificationReactionToImage(
pushNotification.additionalContent,
),
PushKind.response.name: lang.notificationResponse(inGroup), PushKind.response.name: lang.notificationResponse(inGroup),
PushKind.addedToGroup.name: PushKind.addedToGroup.name: lang.notificationAddedToGroup(
lang.notificationAddedToGroup(pushNotification.additionalContent), pushNotification.additionalContent,
),
}; };
return pushNotificationText[pushNotification.kind.name] ?? ''; return pushNotificationText[pushNotification.kind.name] ?? '';

View file

@ -110,35 +110,23 @@ Future<void> initFCMService() async {
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
unawaited(checkForTokenUpdates()); await checkForTokenUpdates();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// You may set the permission requests to "provisional" which allows the user to choose what type
// of notifications they would like to receive once the user receives a notification.
// final notificationSettings =
// await FirebaseMessaging.instance.requestPermission(provisional: true);
await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.requestPermission();
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
// if (Platform.isIOS) {
// final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
// if (apnsToken == null) {
// return;
// }
// }
FirebaseMessaging.onMessage.listen(handleRemoteMessage); FirebaseMessaging.onMessage.listen(handleRemoteMessage);
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
initLogger(); final isInitialized = await initBackgroundExecution();
// Log.info('Handling a background message: ${message.messageId}'); Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message); await handleRemoteMessage(message);
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (await initBackgroundExecution()) { if (isInitialized) {
await handlePeriodicTask(); await handlePeriodicTask();
} }
} else { } else {
@ -164,7 +152,11 @@ Future<void> handleRemoteMessage(RemoteMessage message) async {
final body = final body =
message.notification?.body ?? message.data['body'] as String? ?? ''; message.notification?.body ?? message.data['body'] as String? ?? '';
await customLocalPushNotification(title, body); await customLocalPushNotification(title, body);
} else if (message.data['push_data'] != null) {
await handlePushData(message.data['push_data'] as String);
} }
// On Android the push notification is now shown in the server_message.dart. This ensures
// that the messages was successfully decrypted before showing the push notification
// else if (message.data['push_data'] != null) {
// await handlePushData(message.data['push_data'] as String);
// }
} }

View file

@ -49,13 +49,15 @@ Future<void> setupNotificationWithUsers({
final contacts = await twonlyDB.contactsDao.getAllContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
final pushUser = final pushUser = pushUsers.firstWhereOrNull(
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); (x) => x.userId == contact.userId,
);
if (pushUser != null && pushUser.pushKeys.isNotEmpty) { if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
// make it harder to predict the change of the key // make it harder to predict the change of the key
final timeBefore = final timeBefore = clock.now().subtract(
clock.now().subtract(Duration(days: 10 + random.nextInt(5))); Duration(days: 10 + random.nextInt(5)),
);
final lastKey = pushUser.pushKeys.last; final lastKey = pushUser.pushKeys.last;
final createdAt = DateTime.fromMillisecondsSinceEpoch( final createdAt = DateTime.fromMillisecondsSinceEpoch(
lastKey.createdAtUnixTimestamp.toInt(), lastKey.createdAtUnixTimestamp.toInt(),
@ -197,7 +199,7 @@ Future<void> updateLastMessageId(int fromUserId, String messageId) async {
} }
Future<PushNotification?> getPushNotificationFromEncryptedContent( Future<PushNotification?> getPushNotificationFromEncryptedContent(
int toUserId, int? toUserId,
String? messageId, String? messageId,
EncryptedContent content, EncryptedContent content,
) async { ) async {
@ -210,7 +212,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageById(content.reaction.targetMessageId) .getMessageById(content.reaction.targetMessageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg == null || msg.senderId == null || msg.senderId != toUserId) { if (msg == null || msg.senderId != toUserId) {
return null; return null;
} }
if (msg.content != null) { if (msg.content != null) {
@ -285,7 +287,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
.getMessageById(content.reaction.targetMessageId) .getMessageById(content.reaction.targetMessageId)
.getSingleOrNull(); .getSingleOrNull();
// These notifications should only be send to the original sender. // These notifications should only be send to the original sender.
if (msg == null || msg.senderId == null || msg.senderId != toUserId) { if (msg == null || msg.senderId != toUserId) {
return null; return null;
} }
switch (content.mediaUpdate.type) { switch (content.mediaUpdate.type) {

View file

@ -8,8 +8,11 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.dart'; import 'package:twonly/src/utils/exclusive_access.dart';
bool _isInitialized = false;
void initLogger() { void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; if (_isInitialized) return;
_isInitialized = true;
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) async { Logger.root.onRecord.listen((record) async {
unawaited(_writeLogToFile(record)); unawaited(_writeLogToFile(record));

View file

@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/intent/links.intent.dart'; import 'package:twonly/src/services/intent/links.intent.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
@ -106,7 +108,12 @@ class HomeViewState extends State<HomeView> {
}); });
}; };
selectNotificationStream.stream.listen((response) async { selectNotificationStream.stream.listen((response) async {
if (response.payload != null &&
response.payload!.startsWith(Routes.chats)) {
await routerProvider.push(response.payload!);
} else {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
}
}); });
unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
unawaited(initAsync()); unawaited(initAsync());
@ -140,14 +147,27 @@ class HomeViewState extends State<HomeView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final notificationAppLaunchDetails = final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); .getNotificationAppLaunchDetails();
if (widget.initialPage == 0 || if (widget.initialPage == 0 ||
(notificationAppLaunchDetails != null && (notificationAppLaunchDetails != null &&
notificationAppLaunchDetails.didNotificationLaunchApp)) { notificationAppLaunchDetails.didNotificationLaunchApp)) {
var pushed = false;
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
final payload =
notificationAppLaunchDetails?.notificationResponse?.payload;
if (payload != null && payload.startsWith(Routes.chats)) {
await routerProvider.push(payload);
pushed = true;
}
}
if (!pushed) {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
} }
}
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
if (draftMedia != null) { if (draftMedia != null) {
@ -168,8 +188,9 @@ class HomeViewState extends State<HomeView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: onDoubleTap: offsetRatio == 0
offsetRatio == 0 ? _mainCameraController.onDoubleTap : null, ? _mainCameraController.onDoubleTap
: null,
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null, onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[