diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 9c996308..69d947a8 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1424,6 +1424,12 @@ abstract class AppLocalizations { /// **'Delete for me'** String get deleteOkBtnForMe; + /// No description provided for @deleteOnlyForMe. + /// + /// In en, this message translates to: + /// **'Delete only for me'** + String get deleteOnlyForMe; + /// No description provided for @deleteImageTitle. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0cc3765e..0ef9b545 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -741,6 +741,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get deleteOkBtnForMe => 'Für mich löschen'; + @override + String get deleteOnlyForMe => 'Nur für mich löschen'; + @override String get deleteImageTitle => 'Bist du dir sicher?'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 77bd952c..a51297d4 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -736,6 +736,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteOkBtnForMe => 'Delete for me'; + @override + String get deleteOnlyForMe => 'Delete only for me'; + @override String get deleteImageTitle => 'Are you sure?'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 9d324ceb..ce51a6d0 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 9d324cebaeb127be6a1a40e732ce7c8b54104984 +Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5 diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index c318350f..e6b37675 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -392,49 +392,54 @@ Future insertMediaFileInMessagesTable( List groupIds, { AdditionalMessageData? additionalData, }) async { - await twonlyDB.mediaFilesDao.updateAllMediaFiles( - const MediaFilesCompanion( - isDraftMedia: Value(false), - ), - ); - for (final groupId in groupIds) { - final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId); - if (groupMembers.length == 1) { - if (groupMembers.first.accountDeleted) { - Log.warn( - 'Did not send media file to $groupId because the only account has deleted his account.', - ); - continue; - } - } - - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - groupId: Value(groupId), - mediaId: Value(mediaService.mediaFile.mediaId), - type: Value(MessageType.media.name), - additionalMessageData: Value.absentIfNull( - additionalData?.writeToBuffer(), - ), + await twonlyDB.transaction(() async { + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), ), ); - await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); - if (message != null) { - Log.info( - 'Created message ${message.messageId} for media ${message.mediaId}', - ); - // de-archive contact when sending a new message - await twonlyDB.groupsDao.updateGroup( - message.groupId, - const GroupsCompanion( - archived: Value(false), - deletedContent: Value(false), + for (final groupId in groupIds) { + final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId); + if (groupMembers.length == 1) { + if (groupMembers.first.accountDeleted) { + Log.warn( + 'Did not send media file to $groupId because the only account has deleted his account.', + ); + continue; + } + } + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + mediaId: Value(mediaService.mediaFile.mediaId), + type: Value(MessageType.media.name), + additionalMessageData: Value.absentIfNull( + additionalData?.writeToBuffer(), + ), ), ); - } else { - Log.error('Error inserting media upload message in database.'); + await twonlyDB.groupsDao.increaseLastMessageExchange( + groupId, + clock.now(), + ); + if (message != null) { + Log.info( + 'Created message ${message.messageId} for media ${message.mediaId}', + ); + // de-archive contact when sending a new message + await twonlyDB.groupsDao.updateGroup( + message.groupId, + const GroupsCompanion( + archived: Value(false), + deletedContent: Value(false), + ), + ); + } else { + Log.error('Error inserting media upload message in database.'); + } } - } + }); unawaited(startBackgroundMediaUpload(mediaService)); } diff --git a/lib/src/services/notifications/fcm.notifications.dart b/lib/src/services/notifications/fcm.notifications.dart index d6539d50..27ba1505 100644 --- a/lib/src/services/notifications/fcm.notifications.dart +++ b/lib/src/services/notifications/fcm.notifications.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io' show Platform; import 'package:firebase_app_installations/firebase_app_installations.dart'; @@ -9,14 +8,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.background.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; import '../../../firebase_options.dart'; @@ -176,53 +172,6 @@ class FcmNotificationService { if (await backgroundFetch()) { return; } - - try { - final pushDataBytes = base64Decode(pushDataString); - final encryptedPush = EncryptedPushNotification.fromBuffer( - pushDataBytes, - ); - final pushUsers = await getPushKeys( - SecureStorageKeys.receivingPushKeys, - ); - for (final pushUser in pushUsers) { - for (final pushKey in pushUser.pushKeys) { - final decrypted = await tryDecryptMessage( - pushKey.key, - encryptedPush, - ); - if (decrypted != null) { - if (isUUIDNewer(pushUser.lastMessageId, decrypted.messageId)) { - Log.info( - 'Skipping local push notification because message is older than lastMessageId', - ); - return; - } - Log.info( - 'Successfully decrypted push_data directly from FCM payload! Showing notification.', - ); - await showLocalPushNotification( - pushUser, - decrypted, - titleSuffix: - (userService.isUserCreated && - userService.currentUser.isDeveloper) - ? ' [d]' - : null, - ); - unawaited( - updateLastMessageId( - pushUser.userId.toInt(), - decrypted.messageId, - ), - ); - return; - } - } - } - } catch (e) { - Log.error('Error handling push_data: $e'); - } } } diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index d2f55c96..6a715e0b 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -37,6 +37,20 @@ Future _processSignalUserDataV2(Response_UserData userData) async { ), ); + final signalStore = await getSignalStore(); + if (signalStore != null) { + final existingIdentity = await signalStore.getIdentity( + SignalProtocolAddress(userData.userId.toString(), defaultDeviceId), + ); + + if (existingIdentity != null && existingIdentity != tempIdentityKey) { + Log.error( + 'Identity key mismatch for contact ${userData.userId}! Existing V1 key does not match the incoming V2 identity key.', + ); + return false; + } + } + int? tempEccPreKeyId; Uint8List? tempEccPreKeyPublic; if (userData.pqcBundle.hasPrekey()) { diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 7860eb78..2934f9e9 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -701,7 +701,9 @@ class _CameraPreviewViewState extends State { Widget build(BuildContext context) { if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) { - if (widget.isVisible && !mc.initCameraStarted) { + if (widget.isVisible && + !mc.initCameraStarted && + !mc.isSharePreviewIsShown) { WidgetsBinding.instance.addPostFrameCallback((_) { _checkAndInitCamera(); }); diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index dd890637..1cd2088f 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -310,10 +310,12 @@ class _ShareImageView extends State { }); // in case mediaStoreFutureReady is ready, the image is stored in the originalPath - await insertMediaFileInMessagesTable( - widget.mediaFileService, - widget.selectedGroupIds.toList(), - additionalData: widget.additionalData, + unawaited( + insertMediaFileInMessagesTable( + widget.mediaFileService, + widget.selectedGroupIds.toList(), + additionalData: widget.additionalData, + ), ); if (context.mounted) { diff --git a/lib/src/visual/views/camera/share_image_editor.view.dart b/lib/src/visual/views/camera/share_image_editor.view.dart index e2795911..f5ee5e06 100644 --- a/lib/src/visual/views/camera/share_image_editor.view.dart +++ b/lib/src/visual/views/camera/share_image_editor.view.dart @@ -359,11 +359,12 @@ class _ShareImageEditorView extends State { context.lang.dialogAskDeleteMediaFilePopTitle, ), actions: [ - FilledButton( - child: Text(context.lang.dialogAskDeleteMediaFilePopDelete), + MyButton( + variant: MyButtonVariant.primaryMiddle, onPressed: () { Navigator.pop(context, true); }, + child: Text(context.lang.dialogAskDeleteMediaFilePopDelete), ), TextButton( child: Text(context.lang.cancel), diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index 58febb11..fbd259c2 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -15,11 +15,12 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; import 'package:twonly/src/visual/views/chats/message_info.view.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class MessageContextMenu extends StatelessWidget { @@ -83,8 +84,9 @@ class MessageContextMenu extends StatelessWidget { return SynchronizedImageViewerScreen( galleryItems: galleryItems, initialIndex: 0, - activeMediaIdNotifier: - ValueNotifier(mediaFileService!.mediaFile.mediaId), + activeMediaIdNotifier: ValueNotifier( + mediaFileService!.mediaFile.mediaId, + ), ); }, transitionsBuilder: (context, animation, secondaryAnimation, child) { @@ -172,36 +174,31 @@ class MessageContextMenu extends StatelessWidget { ContextMenuItem( title: context.lang.delete, onTap: () async { - final delete = await showAlertDialog( + final action = await showDeleteMessageOptions( navigator.context, - navigator.context.lang.deleteTitle, - null, - customOk: - (message.senderId == null && !message.isDeletedFromSender) - ? navigator.context.lang.deleteOkBtnForAll - : navigator.context.lang.deleteOkBtnForMe, + message, + group, + galleryItems, ); - if (delete) { - if (message.senderId == null && !message.isDeletedFromSender) { - await twonlyDB.messagesDao.handleMessageDeletion( - null, - message.messageId, - clock.now(), - ); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( - messageUpdate: pb.EncryptedContent_MessageUpdate( - type: pb.EncryptedContent_MessageUpdate_Type.DELETE, - senderMessageId: message.messageId, - ), + if (action == 'delete_for_all') { + await twonlyDB.messagesDao.handleMessageDeletion( + null, + message.messageId, + clock.now(), + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.DELETE, + senderMessageId: message.messageId, ), - ); - } else { - await twonlyDB.messagesDao.deleteMessagesById( - message.messageId, - ); - } + ), + ); + } else if (action == 'delete_for_me') { + await twonlyDB.messagesDao.deleteMessagesById( + message.messageId, + ); } }, icon: FontAwesomeIcons.trash, @@ -230,81 +227,215 @@ class MessageContextMenu extends StatelessWidget { } } -Future editTextMessage(BuildContext context, Message message) async { - var newText = message.content; - final controller = TextEditingController(text: message.content); - await showDialog( +Future showDeleteMessageOptions( + BuildContext context, + Message message, + Group group, + List galleryItems, +) async { + return showModalBottomSheet( context: context, + backgroundColor: Colors.transparent, builder: (context) { - return AlertDialog( - content: StatefulBuilder( - builder: (context, setState) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: TextField( - controller: controller, - autofocus: true, - textCapitalization: TextCapitalization.sentences, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) => setState(() { - newText = value; - }), - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - ), - ], - ), - ); - }, + final isForAll = message.senderId == null && !message.isDeletedFromSender; + return Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 20, + right: 20, + top: 20, + bottom: 40, ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(context.lang.cancel), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), ), - TextButton( - onPressed: () async { - if (newText != null && - newText != message.content && - newText != '') { - final timestamp = clock.now(); - - await twonlyDB.messagesDao.handleTextEdit( - null, - message.messageId, - newText!, - timestamp, - ); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( - messageUpdate: pb.EncryptedContent_MessageUpdate( - type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT, - senderMessageId: message.messageId, - text: newText, - timestamp: Int64( - timestamp.millisecondsSinceEpoch, - ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Stack( + children: [ + ChatListEntry( + group: group, + message: message, + galleryItems: galleryItems, + ), + Positioned.fill( + child: GestureDetector( + onTap: () { + // Prevent clicks + }, + child: Container( + color: Colors.transparent, ), ), - ); - } - if (!context.mounted) return; - Navigator.of(context).pop(); - }, - child: Text(context.lang.ok), - ), - ], + ), + ], + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + if (isForAll) ...[ + Center( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () { + Navigator.pop(context, 'delete_for_all'); + }, + child: Text( + context.lang.deleteOkBtnForAll, + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: 12), + ], + Center( + child: MyButton( + variant: isForAll + ? MyButtonVariant.secondaryDense + : MyButtonVariant.errorMiddle, + onPressed: () { + Navigator.pop(context, 'delete_for_me'); + }, + child: Text( + isForAll + ? context.lang.deleteOnlyForMe + : context.lang.deleteOkBtnForMe, + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: 12), + Center( + child: MyButton( + variant: MyButtonVariant.text, + onPressed: () { + Navigator.pop(context, 'cancel'); + }, + child: Text(context.lang.cancel, textAlign: TextAlign.center), + ), + ), + ], + ), + ); + }, + ); +} + +Future editTextMessage(BuildContext context, Message message) async { + var newText = message.content; + final controller = TextEditingController(text: message.content); + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + padding: const EdgeInsets.only( + left: 20, + right: 20, + top: 20, + bottom: 40, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + autofocus: true, + textCapitalization: TextCapitalization.sentences, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) => setState(() { + newText = value; + }), + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.lang.cancel, + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.primaryMiddle, + onPressed: () async { + if (newText != null && + newText != message.content && + newText != '') { + final timestamp = clock.now(); + + await twonlyDB.messagesDao.handleTextEdit( + null, + message.messageId, + newText!, + timestamp, + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: + pb.EncryptedContent_MessageUpdate( + type: pb + .EncryptedContent_MessageUpdate_Type + .EDIT_TEXT, + senderMessageId: message.messageId, + text: newText, + timestamp: Int64( + timestamp.millisecondsSinceEpoch, + ), + ), + ), + ); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + child: Text( + context.lang.ok, + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, ); }, ); diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index fe71d894..b7b12373 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -58,6 +58,8 @@ class _MessageInputState extends State { Timer? _nextTypingIndicator; DateTime? _lastTextChangeTime; int? _contactId; + Timer? _recordingTimer; + DateTime? _recordingStartTime; Future _sendMessage() async { if (_textFieldController.text == '') return; @@ -104,6 +106,7 @@ class _MessageInputState extends State { _textFieldController.removeListener(_handleTextChange); widget.textFieldFocus.removeListener(_handleTextFocusChange); widget.textFieldFocus.dispose(); + _recordingTimer?.cancel(); recorderController.dispose(); _nextTypingIndicator?.cancel(); @@ -124,12 +127,6 @@ class _MessageInputState extends State { void _initializeControllers() { recorderController = RecorderController(); - recorderController.onCurrentDuration.listen((duration) { - if (!mounted) return; - setState(() { - _currentDuration = duration.inMilliseconds; - }); - }); } void _handleTextChange() { @@ -161,6 +158,21 @@ class _MessageInputState extends State { _recordingState = RecordingState.recording; _currentDuration = 0; }); + _recordingStartTime = clock.now(); + _recordingTimer?.cancel(); + _recordingTimer = Timer.periodic(const Duration(milliseconds: 100), ( + timer, + ) { + if (!mounted) return; + setState(() { + if (_recordingStartTime != null) { + _currentDuration = clock + .now() + .difference(_recordingStartTime!) + .inMilliseconds; + } + }); + }); await HapticFeedback.heavyImpact(); final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a'; unawaited( @@ -171,6 +183,8 @@ class _MessageInputState extends State { } Future _stopAudioRecording() async { + _recordingTimer?.cancel(); + _recordingTimer = null; await HapticFeedback.heavyImpact(); setState(() { _audioRecordingLock = false; @@ -200,6 +214,8 @@ class _MessageInputState extends State { } Future _cancelAudioRecording() async { + _recordingTimer?.cancel(); + _recordingTimer = null; setState(() { _audioRecordingLock = false; _cancelSlideOffset = 0; @@ -285,307 +301,301 @@ class _MessageInputState extends State { Padding( padding: const EdgeInsets.only(left: 10, bottom: 10), child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - if (_recordingState != RecordingState.recording) - GestureDetector( - onTap: () { - setState(() { - _emojiShowing = !_emojiShowing; - if (_emojiShowing) { - widget.textFieldFocus.unfocus(); - } else { - widget.textFieldFocus.requestFocus(); - } - }); - }, - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 12, - right: 8, - ), - child: FaIcon( - size: 20, - _emojiShowing - ? FontAwesomeIcons.keyboard - : FontAwesomeIcons.faceSmile, - ), + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + if (_recordingState != RecordingState.recording) + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, ), ), ), - Expanded( - child: Stack( - children: [ - TextField( - controller: _textFieldController, - focusNode: widget.textFieldFocus, - textCapitalization: - TextCapitalization.sentences, - keyboardType: TextInputType.multiline, - showCursor: - _recordingState != RecordingState.recording, - maxLines: 4, - minLines: 1, - onChanged: (value) { - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - style: const TextStyle(fontSize: 17), - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - ), + ), + Expanded( + child: Stack( + children: [ + TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + textCapitalization: TextCapitalization.sentences, + keyboardType: TextInputType.multiline, + showCursor: + _recordingState != RecordingState.recording, + maxLines: 4, + minLines: 1, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, ), - if (_recordingState == RecordingState.recording) - Container( - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only( - top: 14, - bottom: 14, - left: 12, - right: 8, - ), - child: FaIcon( - FontAwesomeIcons.microphone, - size: 20, - color: Colors.red, - ), + ), + if (_recordingState == RecordingState.recording) + Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only( + top: 14, + bottom: 14, + left: 12, + right: 8, + ), + child: FaIcon( + FontAwesomeIcons.microphone, + size: 20, + color: Colors.red, + ), + ), + const SizedBox(width: 10), + Text( + formatMsToMinSec( + _currentDuration, + ), + style: TextStyle( + color: isDarkMode(context) + ? Colors.white + : Colors.black, + fontSize: 12, + ), + ), + if (!_audioRecordingLock) ...[ + SizedBox( + width: (100 - _cancelSlideOffset) % 101, ), - const SizedBox(width: 10), Text( - formatMsToMinSec( - _currentDuration, - ), - style: TextStyle( - color: isDarkMode(context) - ? Colors.white - : Colors.black, - fontSize: 12, - ), + context.lang.voiceMessageSlideToCancel, ), - if (!_audioRecordingLock) ...[ - SizedBox( - width: - (100 - _cancelSlideOffset) % 101, - ), - Text( - context - .lang - .voiceMessageSlideToCancel, - ), - ] else ...[ - Expanded( - child: Container(), - ), - GestureDetector( - onTap: _cancelAudioRecording, - child: Text( - context.lang.voiceMessageCancel, - style: const TextStyle( - color: Colors.red, - ), + ] else ...[ + Expanded( + child: Container(), + ), + GestureDetector( + onTap: _cancelAudioRecording, + child: Text( + context.lang.voiceMessageCancel, + style: const TextStyle( + color: Colors.red, ), ), - const SizedBox(width: 20), - ], + ), + const SizedBox(width: 20), ], + ], + ), + ), + ], + ), + ), + if (_textFieldController.text == '') + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + if (_textFieldController.text == '') + GestureDetector( + onLongPressMoveUpdate: (details) { + if (_audioRecordingLock) return; + if (_recordingOffset.dy - + details.localPosition.dy >= + 100) { + HapticFeedback.heavyImpact(); + setState(() { + _audioRecordingLock = true; + }); + } + if (_recordingOffset.dx - + details.localPosition.dx >= + 90 && + _recordingState == RecordingState.recording) { + _recordingState = RecordingState.none; + HapticFeedback.heavyImpact(); + _cancelAudioRecording(); + } + + setState(() { + final a = + _recordingOffset.dx - + details.localPosition.dx; + if (a > 0 && a <= 90) { + _cancelSlideOffset = + _recordingOffset.dx - + details.localPosition.dx; + } + }); + }, + onLongPressStart: (a) { + _recordingOffset = a.localPosition; + _startAudioRecording(); + }, + onLongPressCancel: _cancelAudioRecording, + onLongPressEnd: (a) { + if (_recordingState != RecordingState.recording) { + return; + } + if (!_audioRecordingLock) { + _stopAudioRecording(); + } + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (_recordingState == RecordingState.recording && + !_audioRecordingLock) + Positioned.fill( + top: -120, + left: -5, + child: Align( + alignment: AlignmentGeometry.topCenter, + child: Container( + padding: const EdgeInsets.only(top: 13), + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 90, + ), + color: isDarkMode(context) + ? Colors.black + : Colors.white, + ), + child: const Center( + child: Column( + children: [ + FaIcon( + FontAwesomeIcons.lock, + size: 16, + ), + SizedBox(height: 5), + FaIcon( + FontAwesomeIcons.angleUp, + size: 16, + ), + ], + ), + ), + ), + ), + ), + if (_recordingState == RecordingState.recording && + !_audioRecordingLock) + Positioned.fill( + top: -20, + left: -25, + bottom: -20, + right: -20, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(90), + ), + width: 60, + height: 60, + ), + ), + if (!_audioRecordingLock) + ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 8, + right: 12, + ), + child: FaIcon( + size: 20, + color: + (_recordingState == + RecordingState.recording) + ? Colors.white + : null, + (_recordingState == RecordingState.none) + ? FontAwesomeIcons.microphone + : (_recordingState == + RecordingState.recording) + ? FontAwesomeIcons.stop + : FontAwesomeIcons.play, + ), ), ), ], ), ), - if (_textFieldController.text == '') - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, - ), - if (_textFieldController.text == '') - GestureDetector( - onLongPressMoveUpdate: (details) { - if (_audioRecordingLock) return; - if (_recordingOffset.dy - - details.localPosition.dy >= - 100) { - HapticFeedback.heavyImpact(); - setState(() { - _audioRecordingLock = true; - }); - } - if (_recordingOffset.dx - - details.localPosition.dx >= - 90 && - _recordingState == RecordingState.recording) { - _recordingState = RecordingState.none; - HapticFeedback.heavyImpact(); - _cancelAudioRecording(); - } - - setState(() { - final a = - _recordingOffset.dx - - details.localPosition.dx; - if (a > 0 && a <= 90) { - _cancelSlideOffset = - _recordingOffset.dx - - details.localPosition.dx; - } - }); - }, - onLongPressStart: (a) { - _recordingOffset = a.localPosition; - _startAudioRecording(); - }, - onLongPressCancel: _cancelAudioRecording, - onLongPressEnd: (a) { - if (_recordingState != RecordingState.recording) { - return; - } - if (!_audioRecordingLock) { - _stopAudioRecording(); - } - }, - child: Stack( - clipBehavior: Clip.none, - children: [ - if (_recordingState == - RecordingState.recording && - !_audioRecordingLock) - Positioned.fill( - top: -120, - left: -5, - child: Align( - alignment: AlignmentGeometry.topCenter, - child: Container( - padding: const EdgeInsets.only(top: 13), - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 90, - ), - color: isDarkMode(context) - ? Colors.black - : Colors.white, - ), - child: const Center( - child: Column( - children: [ - FaIcon( - FontAwesomeIcons.lock, - size: 16, - ), - SizedBox(height: 5), - FaIcon( - FontAwesomeIcons.angleUp, - size: 16, - ), - ], - ), - ), - ), - ), - ), - if (_recordingState == - RecordingState.recording && - !_audioRecordingLock) - Positioned.fill( - top: -20, - left: -25, - bottom: -20, - right: -20, - child: Container( - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(90), - ), - width: 60, - height: 60, - ), - ), - if (!_audioRecordingLock) - ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 8, - right: 12, - ), - child: FaIcon( - size: 20, - color: - (_recordingState == - RecordingState.recording) - ? Colors.white - : null, - (_recordingState == RecordingState.none) - ? FontAwesomeIcons.microphone - : (_recordingState == - RecordingState.recording) - ? FontAwesomeIcons.stop - : FontAwesomeIcons.play, - ), - ), - ), - ], - ), - ), - ], - ), + ], ), ), - if (_textFieldController.text != '' || _audioRecordingLock) - IconButton( - padding: const EdgeInsets.all(15), - icon: FaIcon( - color: context.color.primary, - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _audioRecordingLock - ? _stopAudioRecording - : _sendMessage, - ) - else - SparksWidget( - animate: _showSparks, - child: IconButton( - icon: const FaIcon(FontAwesomeIcons.plus), - padding: const EdgeInsets.all(15), - onPressed: () => _showAdditionalShareModal(context), - ), + ), + if (_textFieldController.text != '' || _audioRecordingLock) + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, ), - ], - ), + onPressed: _audioRecordingLock + ? _stopAudioRecording + : _sendMessage, + ) + else + SparksWidget( + animate: _showSparks, + child: IconButton( + icon: const FaIcon(FontAwesomeIcons.plus), + padding: const EdgeInsets.all(15), + onPressed: () => _showAdditionalShareModal(context), + ), + ), + ], ), + ), Offstage( offstage: !_emojiShowing, child: EmojiPicker( diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index b5dde0d7..fe28593f 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -9,6 +9,7 @@ import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -275,6 +276,33 @@ class _ContactViewState extends State { onTap: () => handleUserRemoveRequest(contact), ), if (userService.currentUser.isDeveloper) ...[ + if (contact.signalVersion != SignalVersion.v2) + BetterListTile( + icon: FontAwesomeIcons.arrowsRotate, + text: 'Update Connection to V2 (PQXDH)', + onTap: () async { + final userData = await apiService.getUserById(contact.userId); + if (userData != null) { + await processSignalUserData(userData); + final updatedContact = await twonlyDB.contactsDao + .getContactById(contact.userId); + final isV2 = + updatedContact?.signalVersion == SignalVersion.v2; + + if (context.mounted) { + showSnackbar( + context, + isV2 + ? 'Connection updated to V2 successfully' + : 'Failed to update connection to V2', + level: isV2 + ? SnackbarLevel.success + : SnackbarLevel.error, + ); + } + } + }, + ), const Divider(), Padding( padding: const EdgeInsets.symmetric(vertical: 12), diff --git a/pubspec.yaml b/pubspec.yaml index 1713105e..2f295d77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.5.0+167 +version: 0.5.0+168 environment: sdk: ^3.11.0