mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:54:08 +00:00
fix multiple issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
1f269ae5b3
commit
8094949c76
14 changed files with 639 additions and 485 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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?';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -392,6 +392,7 @@ Future<void> insertMediaFileInMessagesTable(
|
||||||
List<String> groupIds, {
|
List<String> groupIds, {
|
||||||
AdditionalMessageData? additionalData,
|
AdditionalMessageData? additionalData,
|
||||||
}) async {
|
}) async {
|
||||||
|
await twonlyDB.transaction(() async {
|
||||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||||
const MediaFilesCompanion(
|
const MediaFilesCompanion(
|
||||||
isDraftMedia: Value(false),
|
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) {
|
if (message != null) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Created message ${message.messageId} for media ${message.mediaId}',
|
'Created message ${message.messageId} for media ${message.mediaId}',
|
||||||
|
|
@ -435,6 +439,7 @@ Future<void> insertMediaFileInMessagesTable(
|
||||||
Log.error('Error inserting media upload message in database.');
|
Log.error('Error inserting media upload message in database.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
unawaited(startBackgroundMediaUpload(mediaService));
|
unawaited(startBackgroundMediaUpload(mediaService));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
insertMediaFileInMessagesTable(
|
||||||
widget.mediaFileService,
|
widget.mediaFileService,
|
||||||
widget.selectedGroupIds.toList(),
|
widget.selectedGroupIds.toList(),
|
||||||
additionalData: widget.additionalData,
|
additionalData: widget.additionalData,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,17 +174,13 @@ 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,
|
||||||
|
|
@ -197,12 +195,11 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else if (action == 'delete_for_me') {
|
||||||
await twonlyDB.messagesDao.deleteMessagesById(
|
await twonlyDB.messagesDao.deleteMessagesById(
|
||||||
message.messageId,
|
message.messageId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.trash,
|
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 {
|
Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
var newText = message.content;
|
var newText = message.content;
|
||||||
final controller = TextEditingController(text: message.content);
|
final controller = TextEditingController(text: message.content);
|
||||||
await showDialog(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return StatefulBuilder(
|
||||||
content: StatefulBuilder(
|
|
||||||
builder: (context, setState) {
|
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
TextField(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: TextField(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
|
@ -259,20 +372,25 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
],
|
Row(
|
||||||
),
|
children: [
|
||||||
);
|
Expanded(
|
||||||
},
|
child: MyButton(
|
||||||
),
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
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 {
|
onPressed: () async {
|
||||||
if (newText != null &&
|
if (newText != null &&
|
||||||
newText != message.content &&
|
newText != message.content &&
|
||||||
|
|
@ -288,8 +406,11 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
await sendCipherTextToGroup(
|
await sendCipherTextToGroup(
|
||||||
message.groupId,
|
message.groupId,
|
||||||
pb.EncryptedContent(
|
pb.EncryptedContent(
|
||||||
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
messageUpdate:
|
||||||
type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT,
|
pb.EncryptedContent_MessageUpdate(
|
||||||
|
type: pb
|
||||||
|
.EncryptedContent_MessageUpdate_Type
|
||||||
|
.EDIT_TEXT,
|
||||||
senderMessageId: message.messageId,
|
senderMessageId: message.messageId,
|
||||||
text: newText,
|
text: newText,
|
||||||
timestamp: Int64(
|
timestamp: Int64(
|
||||||
|
|
@ -302,9 +423,19 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(context.lang.ok),
|
child: Text(
|
||||||
|
context.lang.ok,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -330,8 +346,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: _textFieldController,
|
controller: _textFieldController,
|
||||||
focusNode: widget.textFieldFocus,
|
focusNode: widget.textFieldFocus,
|
||||||
textCapitalization:
|
textCapitalization: TextCapitalization.sentences,
|
||||||
TextCapitalization.sentences,
|
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
showCursor:
|
showCursor:
|
||||||
_recordingState != RecordingState.recording,
|
_recordingState != RecordingState.recording,
|
||||||
|
|
@ -385,13 +400,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
),
|
),
|
||||||
if (!_audioRecordingLock) ...[
|
if (!_audioRecordingLock) ...[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width:
|
width: (100 - _cancelSlideOffset) % 101,
|
||||||
(100 - _cancelSlideOffset) % 101,
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
context
|
context.lang.voiceMessageSlideToCancel,
|
||||||
.lang
|
|
||||||
.voiceMessageSlideToCancel,
|
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -476,8 +488,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
if (_recordingState ==
|
if (_recordingState == RecordingState.recording &&
|
||||||
RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
!_audioRecordingLock)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: -120,
|
top: -120,
|
||||||
|
|
@ -513,8 +524,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_recordingState ==
|
if (_recordingState == RecordingState.recording &&
|
||||||
RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
!_audioRecordingLock)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: -20,
|
top: -20,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue