mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +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'**
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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?';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue