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'**
|
/// **'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,49 +392,54 @@ Future<void> insertMediaFileInMessagesTable(
|
||||||
List<String> groupIds, {
|
List<String> groupIds, {
|
||||||
AdditionalMessageData? additionalData,
|
AdditionalMessageData? additionalData,
|
||||||
}) async {
|
}) async {
|
||||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
await twonlyDB.transaction(() async {
|
||||||
const MediaFilesCompanion(
|
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||||
isDraftMedia: Value(false),
|
const MediaFilesCompanion(
|
||||||
),
|
isDraftMedia: Value(false),
|
||||||
);
|
|
||||||
for (final groupId in groupIds) {
|
|
||||||
final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId);
|
|
||||||
if (groupMembers.length == 1) {
|
|
||||||
if (groupMembers.first.accountDeleted) {
|
|
||||||
Log.warn(
|
|
||||||
'Did not send media file to $groupId because the only account has deleted his account.',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final message = await twonlyDB.messagesDao.insertMessage(
|
|
||||||
MessagesCompanion(
|
|
||||||
groupId: Value(groupId),
|
|
||||||
mediaId: Value(mediaService.mediaFile.mediaId),
|
|
||||||
type: Value(MessageType.media.name),
|
|
||||||
additionalMessageData: Value.absentIfNull(
|
|
||||||
additionalData?.writeToBuffer(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
for (final groupId in groupIds) {
|
||||||
if (message != null) {
|
final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId);
|
||||||
Log.info(
|
if (groupMembers.length == 1) {
|
||||||
'Created message ${message.messageId} for media ${message.mediaId}',
|
if (groupMembers.first.accountDeleted) {
|
||||||
);
|
Log.warn(
|
||||||
// de-archive contact when sending a new message
|
'Did not send media file to $groupId because the only account has deleted his account.',
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
);
|
||||||
message.groupId,
|
continue;
|
||||||
const GroupsCompanion(
|
}
|
||||||
archived: Value(false),
|
}
|
||||||
deletedContent: Value(false),
|
|
||||||
|
final message = await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion(
|
||||||
|
groupId: Value(groupId),
|
||||||
|
mediaId: Value(mediaService.mediaFile.mediaId),
|
||||||
|
type: Value(MessageType.media.name),
|
||||||
|
additionalMessageData: Value.absentIfNull(
|
||||||
|
additionalData?.writeToBuffer(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
await twonlyDB.groupsDao.increaseLastMessageExchange(
|
||||||
Log.error('Error inserting media upload message in database.');
|
groupId,
|
||||||
|
clock.now(),
|
||||||
|
);
|
||||||
|
if (message != null) {
|
||||||
|
Log.info(
|
||||||
|
'Created message ${message.messageId} for media ${message.mediaId}',
|
||||||
|
);
|
||||||
|
// de-archive contact when sending a new message
|
||||||
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
|
message.groupId,
|
||||||
|
const GroupsCompanion(
|
||||||
|
archived: Value(false),
|
||||||
|
deletedContent: Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.error('Error inserting media upload message in database.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
unawaited(startBackgroundMediaUpload(mediaService));
|
unawaited(startBackgroundMediaUpload(mediaService));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
widget.mediaFileService,
|
insertMediaFileInMessagesTable(
|
||||||
widget.selectedGroupIds.toList(),
|
widget.mediaFileService,
|
||||||
additionalData: widget.additionalData,
|
widget.selectedGroupIds.toList(),
|
||||||
|
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,36 +174,31 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.delete,
|
title: context.lang.delete,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final delete = await showAlertDialog(
|
final action = await showDeleteMessageOptions(
|
||||||
navigator.context,
|
navigator.context,
|
||||||
navigator.context.lang.deleteTitle,
|
message,
|
||||||
null,
|
group,
|
||||||
customOk:
|
galleryItems,
|
||||||
(message.senderId == null && !message.isDeletedFromSender)
|
|
||||||
? navigator.context.lang.deleteOkBtnForAll
|
|
||||||
: navigator.context.lang.deleteOkBtnForMe,
|
|
||||||
);
|
);
|
||||||
if (delete) {
|
if (action == 'delete_for_all') {
|
||||||
if (message.senderId == null && !message.isDeletedFromSender) {
|
await twonlyDB.messagesDao.handleMessageDeletion(
|
||||||
await twonlyDB.messagesDao.handleMessageDeletion(
|
null,
|
||||||
null,
|
message.messageId,
|
||||||
message.messageId,
|
clock.now(),
|
||||||
clock.now(),
|
);
|
||||||
);
|
await sendCipherTextToGroup(
|
||||||
await sendCipherTextToGroup(
|
message.groupId,
|
||||||
message.groupId,
|
pb.EncryptedContent(
|
||||||
pb.EncryptedContent(
|
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
||||||
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
type: pb.EncryptedContent_MessageUpdate_Type.DELETE,
|
||||||
type: pb.EncryptedContent_MessageUpdate_Type.DELETE,
|
senderMessageId: message.messageId,
|
||||||
senderMessageId: message.messageId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} else {
|
);
|
||||||
await twonlyDB.messagesDao.deleteMessagesById(
|
} else if (action == 'delete_for_me') {
|
||||||
message.messageId,
|
await twonlyDB.messagesDao.deleteMessagesById(
|
||||||
);
|
message.messageId,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.trash,
|
icon: FontAwesomeIcons.trash,
|
||||||
|
|
@ -230,81 +227,215 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> editTextMessage(BuildContext context, Message message) async {
|
Future<String?> showDeleteMessageOptions(
|
||||||
var newText = message.content;
|
BuildContext context,
|
||||||
final controller = TextEditingController(text: message.content);
|
Message message,
|
||||||
await showDialog(
|
Group group,
|
||||||
|
List<MemoryItem> galleryItems,
|
||||||
|
) async {
|
||||||
|
return showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
final isForAll = message.senderId == null && !message.isDeletedFromSender;
|
||||||
content: StatefulBuilder(
|
return Container(
|
||||||
builder: (context, setState) {
|
width: double.infinity,
|
||||||
return SingleChildScrollView(
|
padding: const EdgeInsets.only(
|
||||||
child: Column(
|
left: 20,
|
||||||
mainAxisSize: MainAxisSize.min,
|
right: 20,
|
||||||
children: [
|
top: 20,
|
||||||
Padding(
|
bottom: 40,
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: 4,
|
|
||||||
minLines: 1,
|
|
||||||
onChanged: (value) => setState(() {
|
|
||||||
newText = value;
|
|
||||||
}),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
actions: [
|
decoration: BoxDecoration(
|
||||||
TextButton(
|
color: context.color.surfaceContainer,
|
||||||
onPressed: () {
|
borderRadius: const BorderRadius.only(
|
||||||
Navigator.of(context).pop();
|
topLeft: Radius.circular(24),
|
||||||
},
|
topRight: Radius.circular(24),
|
||||||
child: Text(context.lang.cancel),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () async {
|
child: Column(
|
||||||
if (newText != null &&
|
mainAxisSize: MainAxisSize.min,
|
||||||
newText != message.content &&
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
newText != '') {
|
children: [
|
||||||
final timestamp = clock.now();
|
Stack(
|
||||||
|
children: [
|
||||||
await twonlyDB.messagesDao.handleTextEdit(
|
ChatListEntry(
|
||||||
null,
|
group: group,
|
||||||
message.messageId,
|
message: message,
|
||||||
newText!,
|
galleryItems: galleryItems,
|
||||||
timestamp,
|
),
|
||||||
);
|
Positioned.fill(
|
||||||
await sendCipherTextToGroup(
|
child: GestureDetector(
|
||||||
message.groupId,
|
onTap: () {
|
||||||
pb.EncryptedContent(
|
// Prevent clicks
|
||||||
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
},
|
||||||
type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT,
|
child: Container(
|
||||||
senderMessageId: message.messageId,
|
color: Colors.transparent,
|
||||||
text: newText,
|
|
||||||
timestamp: Int64(
|
|
||||||
timestamp.millisecondsSinceEpoch,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
],
|
||||||
if (!context.mounted) return;
|
),
|
||||||
Navigator.of(context).pop();
|
const SizedBox(height: 24),
|
||||||
},
|
const Divider(),
|
||||||
child: Text(context.lang.ok),
|
const SizedBox(height: 24),
|
||||||
),
|
if (isForAll) ...[
|
||||||
],
|
Center(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.errorMiddle,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, 'delete_for_all');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.lang.deleteOkBtnForAll,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
Center(
|
||||||
|
child: MyButton(
|
||||||
|
variant: isForAll
|
||||||
|
? MyButtonVariant.secondaryDense
|
||||||
|
: MyButtonVariant.errorMiddle,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, 'delete_for_me');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
isForAll
|
||||||
|
? context.lang.deleteOnlyForMe
|
||||||
|
: context.lang.deleteOkBtnForMe,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Center(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.text,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, 'cancel');
|
||||||
|
},
|
||||||
|
child: Text(context.lang.cancel, textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
|
var newText = message.content;
|
||||||
|
final controller = TextEditingController(text: message.content);
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
top: 20,
|
||||||
|
bottom: 40,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(24),
|
||||||
|
topRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: 4,
|
||||||
|
minLines: 1,
|
||||||
|
onChanged: (value) => setState(() {
|
||||||
|
newText = value;
|
||||||
|
}),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.lang.cancel,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
|
onPressed: () async {
|
||||||
|
if (newText != null &&
|
||||||
|
newText != message.content &&
|
||||||
|
newText != '') {
|
||||||
|
final timestamp = clock.now();
|
||||||
|
|
||||||
|
await twonlyDB.messagesDao.handleTextEdit(
|
||||||
|
null,
|
||||||
|
message.messageId,
|
||||||
|
newText!,
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
await sendCipherTextToGroup(
|
||||||
|
message.groupId,
|
||||||
|
pb.EncryptedContent(
|
||||||
|
messageUpdate:
|
||||||
|
pb.EncryptedContent_MessageUpdate(
|
||||||
|
type: pb
|
||||||
|
.EncryptedContent_MessageUpdate_Type
|
||||||
|
.EDIT_TEXT,
|
||||||
|
senderMessageId: message.messageId,
|
||||||
|
text: newText,
|
||||||
|
timestamp: Int64(
|
||||||
|
timestamp.millisecondsSinceEpoch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.lang.ok,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
Timer? _nextTypingIndicator;
|
Timer? _nextTypingIndicator;
|
||||||
DateTime? _lastTextChangeTime;
|
DateTime? _lastTextChangeTime;
|
||||||
int? _contactId;
|
int? _contactId;
|
||||||
|
Timer? _recordingTimer;
|
||||||
|
DateTime? _recordingStartTime;
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
if (_textFieldController.text == '') return;
|
if (_textFieldController.text == '') return;
|
||||||
|
|
@ -104,6 +106,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
_textFieldController.removeListener(_handleTextChange);
|
_textFieldController.removeListener(_handleTextChange);
|
||||||
widget.textFieldFocus.removeListener(_handleTextFocusChange);
|
widget.textFieldFocus.removeListener(_handleTextFocusChange);
|
||||||
widget.textFieldFocus.dispose();
|
widget.textFieldFocus.dispose();
|
||||||
|
_recordingTimer?.cancel();
|
||||||
recorderController.dispose();
|
recorderController.dispose();
|
||||||
_nextTypingIndicator?.cancel();
|
_nextTypingIndicator?.cancel();
|
||||||
|
|
||||||
|
|
@ -124,12 +127,6 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
recorderController = RecorderController();
|
recorderController = RecorderController();
|
||||||
recorderController.onCurrentDuration.listen((duration) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_currentDuration = duration.inMilliseconds;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTextChange() {
|
void _handleTextChange() {
|
||||||
|
|
@ -161,6 +158,21 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
_recordingState = RecordingState.recording;
|
_recordingState = RecordingState.recording;
|
||||||
_currentDuration = 0;
|
_currentDuration = 0;
|
||||||
});
|
});
|
||||||
|
_recordingStartTime = clock.now();
|
||||||
|
_recordingTimer?.cancel();
|
||||||
|
_recordingTimer = Timer.periodic(const Duration(milliseconds: 100), (
|
||||||
|
timer,
|
||||||
|
) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
if (_recordingStartTime != null) {
|
||||||
|
_currentDuration = clock
|
||||||
|
.now()
|
||||||
|
.difference(_recordingStartTime!)
|
||||||
|
.inMilliseconds;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
await HapticFeedback.heavyImpact();
|
await HapticFeedback.heavyImpact();
|
||||||
final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
|
final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
|
||||||
unawaited(
|
unawaited(
|
||||||
|
|
@ -171,6 +183,8 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _stopAudioRecording() async {
|
Future<void> _stopAudioRecording() async {
|
||||||
|
_recordingTimer?.cancel();
|
||||||
|
_recordingTimer = null;
|
||||||
await HapticFeedback.heavyImpact();
|
await HapticFeedback.heavyImpact();
|
||||||
setState(() {
|
setState(() {
|
||||||
_audioRecordingLock = false;
|
_audioRecordingLock = false;
|
||||||
|
|
@ -200,6 +214,8 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cancelAudioRecording() async {
|
Future<void> _cancelAudioRecording() async {
|
||||||
|
_recordingTimer?.cancel();
|
||||||
|
_recordingTimer = null;
|
||||||
setState(() {
|
setState(() {
|
||||||
_audioRecordingLock = false;
|
_audioRecordingLock = false;
|
||||||
_cancelSlideOffset = 0;
|
_cancelSlideOffset = 0;
|
||||||
|
|
@ -285,307 +301,301 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 10, bottom: 10),
|
padding: const EdgeInsets.only(left: 10, bottom: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.color.surfaceContainer,
|
color: context.color.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (_recordingState != RecordingState.recording)
|
if (_recordingState != RecordingState.recording)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_emojiShowing = !_emojiShowing;
|
_emojiShowing = !_emojiShowing;
|
||||||
if (_emojiShowing) {
|
if (_emojiShowing) {
|
||||||
widget.textFieldFocus.unfocus();
|
widget.textFieldFocus.unfocus();
|
||||||
} else {
|
} else {
|
||||||
widget.textFieldFocus.requestFocus();
|
widget.textFieldFocus.requestFocus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
left: 12,
|
left: 12,
|
||||||
right: 8,
|
right: 8,
|
||||||
),
|
),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
size: 20,
|
size: 20,
|
||||||
_emojiShowing
|
_emojiShowing
|
||||||
? FontAwesomeIcons.keyboard
|
? FontAwesomeIcons.keyboard
|
||||||
: FontAwesomeIcons.faceSmile,
|
: FontAwesomeIcons.faceSmile,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Stack(
|
Expanded(
|
||||||
children: [
|
child: Stack(
|
||||||
TextField(
|
children: [
|
||||||
controller: _textFieldController,
|
TextField(
|
||||||
focusNode: widget.textFieldFocus,
|
controller: _textFieldController,
|
||||||
textCapitalization:
|
focusNode: widget.textFieldFocus,
|
||||||
TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
showCursor:
|
showCursor:
|
||||||
_recordingState != RecordingState.recording,
|
_recordingState != RecordingState.recording,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
},
|
},
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.lang.chatListDetailInput,
|
hintText: context.lang.chatListDetailInput,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (_recordingState == RecordingState.recording)
|
),
|
||||||
Container(
|
if (_recordingState == RecordingState.recording)
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: context.color.surfaceContainer,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
color: context.color.surfaceContainer,
|
||||||
),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Row(
|
),
|
||||||
children: [
|
child: Row(
|
||||||
const Padding(
|
children: [
|
||||||
padding: EdgeInsets.only(
|
const Padding(
|
||||||
top: 14,
|
padding: EdgeInsets.only(
|
||||||
bottom: 14,
|
top: 14,
|
||||||
left: 12,
|
bottom: 14,
|
||||||
right: 8,
|
left: 12,
|
||||||
),
|
right: 8,
|
||||||
child: FaIcon(
|
),
|
||||||
FontAwesomeIcons.microphone,
|
child: FaIcon(
|
||||||
size: 20,
|
FontAwesomeIcons.microphone,
|
||||||
color: Colors.red,
|
size: 20,
|
||||||
),
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
formatMsToMinSec(
|
||||||
|
_currentDuration,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDarkMode(context)
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_audioRecordingLock) ...[
|
||||||
|
SizedBox(
|
||||||
|
width: (100 - _cancelSlideOffset) % 101,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
Text(
|
||||||
formatMsToMinSec(
|
context.lang.voiceMessageSlideToCancel,
|
||||||
_currentDuration,
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
color: isDarkMode(context)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!_audioRecordingLock) ...[
|
] else ...[
|
||||||
SizedBox(
|
Expanded(
|
||||||
width:
|
child: Container(),
|
||||||
(100 - _cancelSlideOffset) % 101,
|
),
|
||||||
),
|
GestureDetector(
|
||||||
Text(
|
onTap: _cancelAudioRecording,
|
||||||
context
|
child: Text(
|
||||||
.lang
|
context.lang.voiceMessageCancel,
|
||||||
.voiceMessageSlideToCancel,
|
style: const TextStyle(
|
||||||
),
|
color: Colors.red,
|
||||||
] else ...[
|
|
||||||
Expanded(
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _cancelAudioRecording,
|
|
||||||
child: Text(
|
|
||||||
context.lang.voiceMessageCancel,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
),
|
||||||
],
|
const SizedBox(width: 20),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_textFieldController.text == '')
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return CameraSendToView(widget.group);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_textFieldController.text == '')
|
||||||
|
GestureDetector(
|
||||||
|
onLongPressMoveUpdate: (details) {
|
||||||
|
if (_audioRecordingLock) return;
|
||||||
|
if (_recordingOffset.dy -
|
||||||
|
details.localPosition.dy >=
|
||||||
|
100) {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
setState(() {
|
||||||
|
_audioRecordingLock = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_recordingOffset.dx -
|
||||||
|
details.localPosition.dx >=
|
||||||
|
90 &&
|
||||||
|
_recordingState == RecordingState.recording) {
|
||||||
|
_recordingState = RecordingState.none;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
_cancelAudioRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
final a =
|
||||||
|
_recordingOffset.dx -
|
||||||
|
details.localPosition.dx;
|
||||||
|
if (a > 0 && a <= 90) {
|
||||||
|
_cancelSlideOffset =
|
||||||
|
_recordingOffset.dx -
|
||||||
|
details.localPosition.dx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLongPressStart: (a) {
|
||||||
|
_recordingOffset = a.localPosition;
|
||||||
|
_startAudioRecording();
|
||||||
|
},
|
||||||
|
onLongPressCancel: _cancelAudioRecording,
|
||||||
|
onLongPressEnd: (a) {
|
||||||
|
if (_recordingState != RecordingState.recording) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_audioRecordingLock) {
|
||||||
|
_stopAudioRecording();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
if (_recordingState == RecordingState.recording &&
|
||||||
|
!_audioRecordingLock)
|
||||||
|
Positioned.fill(
|
||||||
|
top: -120,
|
||||||
|
left: -5,
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentGeometry.topCenter,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(top: 13),
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
90,
|
||||||
|
),
|
||||||
|
color: isDarkMode(context)
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.lock,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.angleUp,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_recordingState == RecordingState.recording &&
|
||||||
|
!_audioRecordingLock)
|
||||||
|
Positioned.fill(
|
||||||
|
top: -20,
|
||||||
|
left: -25,
|
||||||
|
bottom: -20,
|
||||||
|
right: -20,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
borderRadius: BorderRadius.circular(90),
|
||||||
|
),
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_audioRecordingLock)
|
||||||
|
ColoredBox(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
left: 8,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
size: 20,
|
||||||
|
color:
|
||||||
|
(_recordingState ==
|
||||||
|
RecordingState.recording)
|
||||||
|
? Colors.white
|
||||||
|
: null,
|
||||||
|
(_recordingState == RecordingState.none)
|
||||||
|
? FontAwesomeIcons.microphone
|
||||||
|
: (_recordingState ==
|
||||||
|
RecordingState.recording)
|
||||||
|
? FontAwesomeIcons.stop
|
||||||
|
: FontAwesomeIcons.play,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_textFieldController.text == '')
|
],
|
||||||
IconButton(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return CameraSendToView(widget.group);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_textFieldController.text == '')
|
|
||||||
GestureDetector(
|
|
||||||
onLongPressMoveUpdate: (details) {
|
|
||||||
if (_audioRecordingLock) return;
|
|
||||||
if (_recordingOffset.dy -
|
|
||||||
details.localPosition.dy >=
|
|
||||||
100) {
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
setState(() {
|
|
||||||
_audioRecordingLock = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_recordingOffset.dx -
|
|
||||||
details.localPosition.dx >=
|
|
||||||
90 &&
|
|
||||||
_recordingState == RecordingState.recording) {
|
|
||||||
_recordingState = RecordingState.none;
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
_cancelAudioRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
final a =
|
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
if (a > 0 && a <= 90) {
|
|
||||||
_cancelSlideOffset =
|
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onLongPressStart: (a) {
|
|
||||||
_recordingOffset = a.localPosition;
|
|
||||||
_startAudioRecording();
|
|
||||||
},
|
|
||||||
onLongPressCancel: _cancelAudioRecording,
|
|
||||||
onLongPressEnd: (a) {
|
|
||||||
if (_recordingState != RecordingState.recording) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_audioRecordingLock) {
|
|
||||||
_stopAudioRecording();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
if (_recordingState ==
|
|
||||||
RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
|
||||||
top: -120,
|
|
||||||
left: -5,
|
|
||||||
child: Align(
|
|
||||||
alignment: AlignmentGeometry.topCenter,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.only(top: 13),
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
90,
|
|
||||||
),
|
|
||||||
color: isDarkMode(context)
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.lock,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.angleUp,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_recordingState ==
|
|
||||||
RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
|
||||||
top: -20,
|
|
||||||
left: -25,
|
|
||||||
bottom: -20,
|
|
||||||
right: -20,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
borderRadius: BorderRadius.circular(90),
|
|
||||||
),
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_audioRecordingLock)
|
|
||||||
ColoredBox(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 8,
|
|
||||||
bottom: 8,
|
|
||||||
left: 8,
|
|
||||||
right: 12,
|
|
||||||
),
|
|
||||||
child: FaIcon(
|
|
||||||
size: 20,
|
|
||||||
color:
|
|
||||||
(_recordingState ==
|
|
||||||
RecordingState.recording)
|
|
||||||
? Colors.white
|
|
||||||
: null,
|
|
||||||
(_recordingState == RecordingState.none)
|
|
||||||
? FontAwesomeIcons.microphone
|
|
||||||
: (_recordingState ==
|
|
||||||
RecordingState.recording)
|
|
||||||
? FontAwesomeIcons.stop
|
|
||||||
: FontAwesomeIcons.play,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_textFieldController.text != '' || _audioRecordingLock)
|
),
|
||||||
IconButton(
|
if (_textFieldController.text != '' || _audioRecordingLock)
|
||||||
padding: const EdgeInsets.all(15),
|
IconButton(
|
||||||
icon: FaIcon(
|
padding: const EdgeInsets.all(15),
|
||||||
color: context.color.primary,
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.solidPaperPlane,
|
color: context.color.primary,
|
||||||
),
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
onPressed: _audioRecordingLock
|
|
||||||
? _stopAudioRecording
|
|
||||||
: _sendMessage,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SparksWidget(
|
|
||||||
animate: _showSparks,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.plus),
|
|
||||||
padding: const EdgeInsets.all(15),
|
|
||||||
onPressed: () => _showAdditionalShareModal(context),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
onPressed: _audioRecordingLock
|
||||||
),
|
? _stopAudioRecording
|
||||||
|
: _sendMessage,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SparksWidget(
|
||||||
|
animate: _showSparks,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.plus),
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
onPressed: () => _showAdditionalShareModal(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !_emojiShowing,
|
offstage: !_emojiShowing,
|
||||||
child: EmojiPicker(
|
child: EmojiPicker(
|
||||||
|
|
|
||||||
|
|
@ -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