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,6 +392,7 @@ Future<void> insertMediaFileInMessagesTable(
List<String> groupIds, {
AdditionalMessageData? additionalData,
}) async {
await twonlyDB.transaction(() async {
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
isDraftMedia: Value(false),
@ -418,7 +419,10 @@ Future<void> insertMediaFileInMessagesTable(
),
),
);
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
clock.now(),
);
if (message != null) {
Log.info(
'Created message ${message.messageId} for media ${message.mediaId}',
@ -435,6 +439,7 @@ Future<void> insertMediaFileInMessagesTable(
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(
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,17 +174,13 @@ 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) {
if (action == 'delete_for_all') {
await twonlyDB.messagesDao.handleMessageDeletion(
null,
message.messageId,
@ -197,12 +195,11 @@ class MessageContextMenu extends StatelessWidget {
),
),
);
} else {
} else if (action == 'delete_for_me') {
await twonlyDB.messagesDao.deleteMessagesById(
message.messageId,
);
}
}
},
icon: FontAwesomeIcons.trash,
),
@ -230,22 +227,138 @@ class MessageContextMenu extends StatelessWidget {
}
}
Future<String?> showDeleteMessageOptions(
BuildContext context,
Message message,
Group group,
List<MemoryItem> galleryItems,
) async {
return showModalBottomSheet<String>(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
final isForAll = message.senderId == null && !message.isDeletedFromSender;
return Container(
width: double.infinity,
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,
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,
),
),
),
],
),
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 showDialog(
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return AlertDialog(
content: StatefulBuilder(
return StatefulBuilder(
builder: (context, setState) {
return SingleChildScrollView(
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: [
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextField(
TextField(
controller: controller,
autofocus: true,
textCapitalization: TextCapitalization.sentences,
@ -259,20 +372,25 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
border: OutlineInputBorder(),
),
),
),
],
),
);
},
),
actions: [
TextButton(
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.secondaryMiddle,
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
child: Text(
context.lang.cancel,
textAlign: TextAlign.center,
),
TextButton(
),
),
const SizedBox(width: 12),
Expanded(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () async {
if (newText != null &&
newText != message.content &&
@ -288,8 +406,11 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
await sendCipherTextToGroup(
message.groupId,
pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT,
messageUpdate:
pb.EncryptedContent_MessageUpdate(
type: pb
.EncryptedContent_MessageUpdate_Type
.EDIT_TEXT,
senderMessageId: message.messageId,
text: newText,
timestamp: Int64(
@ -302,9 +423,19 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
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;
@ -330,8 +346,7 @@ class _MessageInputState extends State<MessageInput> {
TextField(
controller: _textFieldController,
focusNode: widget.textFieldFocus,
textCapitalization:
TextCapitalization.sentences,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline,
showCursor:
_recordingState != RecordingState.recording,
@ -385,13 +400,10 @@ class _MessageInputState extends State<MessageInput> {
),
if (!_audioRecordingLock) ...[
SizedBox(
width:
(100 - _cancelSlideOffset) % 101,
width: (100 - _cancelSlideOffset) % 101,
),
Text(
context
.lang
.voiceMessageSlideToCancel,
context.lang.voiceMessageSlideToCancel,
),
] else ...[
Expanded(
@ -476,8 +488,7 @@ class _MessageInputState extends State<MessageInput> {
child: Stack(
clipBehavior: Clip.none,
children: [
if (_recordingState ==
RecordingState.recording &&
if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill(
top: -120,
@ -513,8 +524,7 @@ class _MessageInputState extends State<MessageInput> {
),
),
),
if (_recordingState ==
RecordingState.recording &&
if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill(
top: -20,

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