fix multiple issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-08-05 21:39:01 +02:00
parent 1f269ae5b3
commit 8094949c76
14 changed files with 639 additions and 485 deletions

View file

@ -1424,6 +1424,12 @@ abstract class AppLocalizations {
/// **'Delete for me'** /// **'Delete for me'**
String get deleteOkBtnForMe; 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. /// No description provided for @deleteImageTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -741,6 +741,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteOkBtnForMe => 'Für mich löschen'; String get deleteOkBtnForMe => 'Für mich löschen';
@override
String get deleteOnlyForMe => 'Nur für mich löschen';
@override @override
String get deleteImageTitle => 'Bist du dir sicher?'; String get deleteImageTitle => 'Bist du dir sicher?';

View file

@ -736,6 +736,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteOkBtnForMe => 'Delete for me'; String get deleteOkBtnForMe => 'Delete for me';
@override
String get deleteOnlyForMe => 'Delete only for me';
@override @override
String get deleteImageTitle => 'Are you sure?'; String get deleteImageTitle => 'Are you sure?';

@ -1 +1 @@
Subproject commit 9d324cebaeb127be6a1a40e732ce7c8b54104984 Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5

View file

@ -392,49 +392,54 @@ Future<void> insertMediaFileInMessagesTable(
List<String> groupIds, { List<String> groupIds, {
AdditionalMessageData? additionalData, AdditionalMessageData? additionalData,
}) async { }) async {
await twonlyDB.mediaFilesDao.updateAllMediaFiles( await twonlyDB.transaction(() async {
const MediaFilesCompanion( await twonlyDB.mediaFilesDao.updateAllMediaFiles(
isDraftMedia: Value(false), 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.groupsDao.increaseLastMessageExchange(groupId, clock.now()); for (final groupId in groupIds) {
if (message != null) { final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId);
Log.info( if (groupMembers.length == 1) {
'Created message ${message.messageId} for media ${message.mediaId}', if (groupMembers.first.accountDeleted) {
); Log.warn(
// de-archive contact when sending a new message 'Did not send media file to $groupId because the only account has deleted his account.',
await twonlyDB.groupsDao.updateGroup( );
message.groupId, continue;
const GroupsCompanion( }
archived: Value(false), }
deletedContent: Value(false),
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 { await twonlyDB.groupsDao.increaseLastMessageExchange(
Log.error('Error inserting media upload message in database.'); 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)); unawaited(startBackgroundMediaUpload(mediaService));
} }

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:firebase_app_installations/firebase_app_installations.dart'; 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/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.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/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.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/fcm.background.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import '../../../firebase_options.dart'; import '../../../firebase_options.dart';
@ -176,53 +172,6 @@ class FcmNotificationService {
if (await backgroundFetch()) { if (await backgroundFetch()) {
return; 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');
}
} }
} }

View file

@ -37,6 +37,20 @@ Future<bool> _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; int? tempEccPreKeyId;
Uint8List? tempEccPreKeyPublic; Uint8List? tempEccPreKeyPublic;
if (userData.pqcBundle.hasPrekey()) { if (userData.pqcBundle.hasPrekey()) {

View file

@ -701,7 +701,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length ||
mc.cameraController == null) { mc.cameraController == null) {
if (widget.isVisible && !mc.initCameraStarted) { if (widget.isVisible &&
!mc.initCameraStarted &&
!mc.isSharePreviewIsShown) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAndInitCamera(); _checkAndInitCamera();
}); });

View file

@ -310,10 +310,12 @@ class _ShareImageView extends State<ShareImageView> {
}); });
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath // in case mediaStoreFutureReady is ready, the image is stored in the originalPath
await insertMediaFileInMessagesTable( unawaited(
widget.mediaFileService, insertMediaFileInMessagesTable(
widget.selectedGroupIds.toList(), widget.mediaFileService,
additionalData: widget.additionalData, widget.selectedGroupIds.toList(),
additionalData: widget.additionalData,
),
); );
if (context.mounted) { if (context.mounted) {

View file

@ -359,11 +359,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
context.lang.dialogAskDeleteMediaFilePopTitle, context.lang.dialogAskDeleteMediaFilePopTitle,
), ),
actions: [ actions: [
FilledButton( MyButton(
child: Text(context.lang.dialogAskDeleteMediaFilePopDelete), variant: MyButtonVariant.primaryMiddle,
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.pop(context, true);
}, },
child: Text(context.lang.dialogAskDeleteMediaFilePopDelete),
), ),
TextButton( TextButton(
child: Text(context.lang.cancel), child: Text(context.lang.cancel),

View file

@ -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/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.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/components/emoji_picker.bottom.dart';
import 'package:twonly/src/visual/context_menu/context_menu.helper.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/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/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'; import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
class MessageContextMenu extends StatelessWidget { class MessageContextMenu extends StatelessWidget {
@ -83,8 +84,9 @@ class MessageContextMenu extends StatelessWidget {
return SynchronizedImageViewerScreen( return SynchronizedImageViewerScreen(
galleryItems: galleryItems, galleryItems: galleryItems,
initialIndex: 0, initialIndex: 0,
activeMediaIdNotifier: activeMediaIdNotifier: ValueNotifier(
ValueNotifier(mediaFileService!.mediaFile.mediaId), mediaFileService!.mediaFile.mediaId,
),
); );
}, },
transitionsBuilder: (context, animation, secondaryAnimation, child) { transitionsBuilder: (context, animation, secondaryAnimation, child) {
@ -172,36 +174,31 @@ class MessageContextMenu extends StatelessWidget {
ContextMenuItem( ContextMenuItem(
title: context.lang.delete, title: context.lang.delete,
onTap: () async { onTap: () async {
final delete = await showAlertDialog( final action = await showDeleteMessageOptions(
navigator.context, navigator.context,
navigator.context.lang.deleteTitle, message,
null, group,
customOk: galleryItems,
(message.senderId == null && !message.isDeletedFromSender)
? navigator.context.lang.deleteOkBtnForAll
: navigator.context.lang.deleteOkBtnForMe,
); );
if (delete) { if (action == 'delete_for_all') {
if (message.senderId == null && !message.isDeletedFromSender) { await twonlyDB.messagesDao.handleMessageDeletion(
await twonlyDB.messagesDao.handleMessageDeletion( null,
null, message.messageId,
message.messageId, clock.now(),
clock.now(), );
); await sendCipherTextToGroup(
await sendCipherTextToGroup( message.groupId,
message.groupId, pb.EncryptedContent(
pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate(
messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb.EncryptedContent_MessageUpdate_Type.DELETE,
type: pb.EncryptedContent_MessageUpdate_Type.DELETE, senderMessageId: message.messageId,
senderMessageId: message.messageId,
),
), ),
); ),
} else { );
await twonlyDB.messagesDao.deleteMessagesById( } else if (action == 'delete_for_me') {
message.messageId, await twonlyDB.messagesDao.deleteMessagesById(
); message.messageId,
} );
} }
}, },
icon: FontAwesomeIcons.trash, icon: FontAwesomeIcons.trash,
@ -230,81 +227,215 @@ class MessageContextMenu extends StatelessWidget {
} }
} }
Future<void> editTextMessage(BuildContext context, Message message) async { Future<String?> showDeleteMessageOptions(
var newText = message.content; BuildContext context,
final controller = TextEditingController(text: message.content); Message message,
await showDialog( Group group,
List<MemoryItem> galleryItems,
) async {
return showModalBottomSheet<String>(
context: context, context: context,
backgroundColor: Colors.transparent,
builder: (context) { builder: (context) {
return AlertDialog( final isForAll = message.senderId == null && !message.isDeletedFromSender;
content: StatefulBuilder( return Container(
builder: (context, setState) { width: double.infinity,
return SingleChildScrollView( padding: const EdgeInsets.only(
child: Column( left: 20,
mainAxisSize: MainAxisSize.min, right: 20,
children: [ top: 20,
Padding( bottom: 40,
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(),
),
),
),
],
),
);
},
), ),
actions: [ decoration: BoxDecoration(
TextButton( color: context.color.surfaceContainer,
onPressed: () { borderRadius: const BorderRadius.only(
Navigator.of(context).pop(); topLeft: Radius.circular(24),
}, topRight: Radius.circular(24),
child: Text(context.lang.cancel),
), ),
TextButton( ),
onPressed: () async { child: Column(
if (newText != null && mainAxisSize: MainAxisSize.min,
newText != message.content && crossAxisAlignment: CrossAxisAlignment.stretch,
newText != '') { children: [
final timestamp = clock.now(); Stack(
children: [
await twonlyDB.messagesDao.handleTextEdit( ChatListEntry(
null, group: group,
message.messageId, message: message,
newText!, galleryItems: galleryItems,
timestamp, ),
); Positioned.fill(
await sendCipherTextToGroup( child: GestureDetector(
message.groupId, onTap: () {
pb.EncryptedContent( // Prevent clicks
messageUpdate: pb.EncryptedContent_MessageUpdate( },
type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT, child: Container(
senderMessageId: message.messageId, color: Colors.transparent,
text: newText,
timestamp: Int64(
timestamp.millisecondsSinceEpoch,
),
), ),
), ),
); ),
} ],
if (!context.mounted) return; ),
Navigator.of(context).pop(); const SizedBox(height: 24),
}, const Divider(),
child: Text(context.lang.ok), 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<void> editTextMessage(BuildContext context, Message message) async {
var newText = message.content;
final controller = TextEditingController(text: message.content);
await showModalBottomSheet<void>(
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,
),
),
),
],
),
],
),
),
);
},
); );
}, },
); );

View file

@ -58,6 +58,8 @@ class _MessageInputState extends State<MessageInput> {
Timer? _nextTypingIndicator; Timer? _nextTypingIndicator;
DateTime? _lastTextChangeTime; DateTime? _lastTextChangeTime;
int? _contactId; int? _contactId;
Timer? _recordingTimer;
DateTime? _recordingStartTime;
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_textFieldController.text == '') return; if (_textFieldController.text == '') return;
@ -104,6 +106,7 @@ class _MessageInputState extends State<MessageInput> {
_textFieldController.removeListener(_handleTextChange); _textFieldController.removeListener(_handleTextChange);
widget.textFieldFocus.removeListener(_handleTextFocusChange); widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose(); widget.textFieldFocus.dispose();
_recordingTimer?.cancel();
recorderController.dispose(); recorderController.dispose();
_nextTypingIndicator?.cancel(); _nextTypingIndicator?.cancel();
@ -124,12 +127,6 @@ class _MessageInputState extends State<MessageInput> {
void _initializeControllers() { void _initializeControllers() {
recorderController = RecorderController(); recorderController = RecorderController();
recorderController.onCurrentDuration.listen((duration) {
if (!mounted) return;
setState(() {
_currentDuration = duration.inMilliseconds;
});
});
} }
void _handleTextChange() { void _handleTextChange() {
@ -161,6 +158,21 @@ class _MessageInputState extends State<MessageInput> {
_recordingState = RecordingState.recording; _recordingState = RecordingState.recording;
_currentDuration = 0; _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(); await HapticFeedback.heavyImpact();
final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a'; final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
unawaited( unawaited(
@ -171,6 +183,8 @@ class _MessageInputState extends State<MessageInput> {
} }
Future<void> _stopAudioRecording() async { Future<void> _stopAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
setState(() { setState(() {
_audioRecordingLock = false; _audioRecordingLock = false;
@ -200,6 +214,8 @@ class _MessageInputState extends State<MessageInput> {
} }
Future<void> _cancelAudioRecording() async { Future<void> _cancelAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
setState(() { setState(() {
_audioRecordingLock = false; _audioRecordingLock = false;
_cancelSlideOffset = 0; _cancelSlideOffset = 0;
@ -285,307 +301,301 @@ class _MessageInputState extends State<MessageInput> {
Padding( Padding(
padding: const EdgeInsets.only(left: 10, bottom: 10), padding: const EdgeInsets.only(left: 10, bottom: 10),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.color.surfaceContainer, color: context.color.surfaceContainer,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Row( child: Row(
children: [ children: [
if (_recordingState != RecordingState.recording) if (_recordingState != RecordingState.recording)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
_emojiShowing = !_emojiShowing; _emojiShowing = !_emojiShowing;
if (_emojiShowing) { if (_emojiShowing) {
widget.textFieldFocus.unfocus(); widget.textFieldFocus.unfocus();
} else { } else {
widget.textFieldFocus.requestFocus(); widget.textFieldFocus.requestFocus();
} }
}); });
}, },
child: ColoredBox( child: ColoredBox(
color: Colors.transparent, color: Colors.transparent,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 8, top: 8,
bottom: 8, bottom: 8,
left: 12, left: 12,
right: 8, right: 8,
), ),
child: FaIcon( child: FaIcon(
size: 20, size: 20,
_emojiShowing _emojiShowing
? FontAwesomeIcons.keyboard ? FontAwesomeIcons.keyboard
: FontAwesomeIcons.faceSmile, : FontAwesomeIcons.faceSmile,
),
), ),
), ),
), ),
Expanded( ),
child: Stack( Expanded(
children: [ child: Stack(
TextField( children: [
controller: _textFieldController, TextField(
focusNode: widget.textFieldFocus, controller: _textFieldController,
textCapitalization: focusNode: widget.textFieldFocus,
TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
showCursor: showCursor:
_recordingState != RecordingState.recording, _recordingState != RecordingState.recording,
maxLines: 4, maxLines: 4,
minLines: 1, minLines: 1,
onChanged: (value) { onChanged: (value) {
setState(() {}); setState(() {});
}, },
onSubmitted: (_) { onSubmitted: (_) {
_sendMessage(); _sendMessage();
}, },
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.lang.chatListDetailInput, hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: InputBorder.none, border: InputBorder.none,
),
), ),
if (_recordingState == RecordingState.recording) ),
Container( if (_recordingState == RecordingState.recording)
decoration: BoxDecoration( Container(
color: context.color.surfaceContainer, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), color: context.color.surfaceContainer,
), borderRadius: BorderRadius.circular(20),
child: Row( ),
children: [ child: Row(
const Padding( children: [
padding: EdgeInsets.only( const Padding(
top: 14, padding: EdgeInsets.only(
bottom: 14, top: 14,
left: 12, bottom: 14,
right: 8, left: 12,
), right: 8,
child: FaIcon( ),
FontAwesomeIcons.microphone, child: FaIcon(
size: 20, FontAwesomeIcons.microphone,
color: Colors.red, 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( Text(
formatMsToMinSec( context.lang.voiceMessageSlideToCancel,
_currentDuration,
),
style: TextStyle(
color: isDarkMode(context)
? Colors.white
: Colors.black,
fontSize: 12,
),
), ),
if (!_audioRecordingLock) ...[ ] else ...[
SizedBox( Expanded(
width: child: Container(),
(100 - _cancelSlideOffset) % 101, ),
), GestureDetector(
Text( onTap: _cancelAudioRecording,
context child: Text(
.lang context.lang.voiceMessageCancel,
.voiceMessageSlideToCancel, 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( if (_textFieldController.text != '' || _audioRecordingLock)
padding: const EdgeInsets.all(15), IconButton(
icon: FaIcon( padding: const EdgeInsets.all(15),
color: context.color.primary, icon: FaIcon(
FontAwesomeIcons.solidPaperPlane, 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),
),
), ),
], 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(
offstage: !_emojiShowing, offstage: !_emojiShowing,
child: EmojiPicker( child: EmojiPicker(

View file

@ -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/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.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/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -275,6 +276,33 @@ class _ContactViewState extends State<ContactView> {
onTap: () => handleUserRemoveRequest(contact), onTap: () => handleUserRemoveRequest(contact),
), ),
if (userService.currentUser.isDeveloper) ...[ 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(), const Divider(),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.5.0+167 version: 0.5.0+168
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0