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'**
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:

View file

@ -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?';

View file

@ -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?';

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

View file

@ -392,49 +392,54 @@ Future<void> insertMediaFileInMessagesTable(
List<String> 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));
}

View file

@ -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');
}
}
}

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;
Uint8List? tempEccPreKeyPublic;
if (userData.pqcBundle.hasPrekey()) {

View file

@ -701,7 +701,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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();
});

View file

@ -310,10 +310,12 @@ class _ShareImageView extends State<ShareImageView> {
});
// 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) {

View file

@ -359,11 +359,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
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),

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/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<void> editTextMessage(BuildContext context, Message message) async {
var newText = message.content;
final controller = TextEditingController(text: message.content);
await showDialog(
Future<String?> showDeleteMessageOptions(
BuildContext context,
Message message,
Group group,
List<MemoryItem> galleryItems,
) async {
return showModalBottomSheet<String>(
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<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;
DateTime? _lastTextChangeTime;
int? _contactId;
Timer? _recordingTimer;
DateTime? _recordingStartTime;
Future<void> _sendMessage() async {
if (_textFieldController.text == '') return;
@ -104,6 +106,7 @@ class _MessageInputState extends State<MessageInput> {
_textFieldController.removeListener(_handleTextChange);
widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose();
_recordingTimer?.cancel();
recorderController.dispose();
_nextTypingIndicator?.cancel();
@ -124,12 +127,6 @@ class _MessageInputState extends State<MessageInput> {
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<MessageInput> {
_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<MessageInput> {
}
Future<void> _stopAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
await HapticFeedback.heavyImpact();
setState(() {
_audioRecordingLock = false;
@ -200,6 +214,8 @@ class _MessageInputState extends State<MessageInput> {
}
Future<void> _cancelAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
setState(() {
_audioRecordingLock = false;
_cancelSlideOffset = 0;
@ -285,307 +301,301 @@ class _MessageInputState extends State<MessageInput> {
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(

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/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<ContactView> {
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),

View file

@ -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