From ca62069652846cbe2805bf235de3d1db7a1782c5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 13:58:33 +0100 Subject: [PATCH 1/8] remove avatar indicators in group chats --- lib/src/database/daos/messages.dao.dart | 32 ------------ lib/src/views/chats/chat_messages.view.dart | 58 +-------------------- 2 files changed, 2 insertions(+), 88 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 1e41efc..71bf8d2 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -342,38 +342,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .getSingleOrNull(); } - Stream>> watchLastOpenedMessagePerContact( - String groupId, - ) { - const sql = ''' - SELECT m.*, c.* - FROM ( - SELECT ma.contact_id, ma.message_id, - ROW_NUMBER() OVER (PARTITION BY ma.contact_id - ORDER BY ma.action_at DESC, ma.message_id DESC) AS rn - FROM message_actions ma - WHERE ma.type = 'openedAt' - ) last_open - JOIN messages m ON m.message_id = last_open.message_id - JOIN contacts c ON c.user_id = last_open.contact_id - WHERE last_open.rn = 1 AND m.group_id = ?; - '''; - - return customSelect( - sql, - variables: [Variable.withString(groupId)], - readsFrom: {messages, messageActions, contacts}, - ).watch().map((rows) async { - final res = <(Message, Contact)>[]; - for (final row in rows) { - final message = await messages.mapFromRow(row); - final contact = await contacts.mapFromRow(row); - res.add((message, contact)); - } - return res; - }); - } - Future deleteMessagesById(String messageId) { return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index fab9358..422f18f 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -33,7 +33,6 @@ class ChatItem { const ChatItem._({ this.message, this.date, - this.lastOpenedPosition, this.groupAction, }); factory ChatItem.date(DateTime date) { @@ -42,20 +41,15 @@ class ChatItem { factory ChatItem.message(Message message) { return ChatItem._(message: message); } - factory ChatItem.lastOpenedPosition(List contacts) { - return ChatItem._(lastOpenedPosition: contacts); - } factory ChatItem.groupAction(GroupHistory groupAction) { return ChatItem._(groupAction: groupAction); } final GroupHistory? groupAction; final Message? message; final DateTime? date; - final List? lastOpenedPosition; bool get isMessage => message != null; bool get isDate => date != null; bool get isGroupAction => groupAction != null; - bool get isLastOpenedPosition => lastOpenedPosition != null; } /// Displays detailed information about a SampleItem. @@ -75,14 +69,11 @@ class _ChatMessagesViewState extends State { late StreamSubscription> messageSub; StreamSubscription>? groupActionsSub; StreamSubscription>? contactSub; - StreamSubscription>>? - lastOpenedMessageByContactSub; Map userIdToContact = {}; List messages = []; List allMessages = []; - List<(Message, Contact)> lastOpenedMessageByContact = []; List groupActions = []; List galleryItems = []; Message? quotesMessage; @@ -105,7 +96,6 @@ class _ChatMessagesViewState extends State { messageSub.cancel(); contactSub?.cancel(); groupActionsSub?.cancel(); - lastOpenedMessageByContactSub?.cancel(); super.dispose(); } @@ -121,19 +111,10 @@ class _ChatMessagesViewState extends State { }); if (!widget.group.isDirectChat) { - final lastOpenedStream = - twonlyDB.messagesDao.watchLastOpenedMessagePerContact(group.groupId); - lastOpenedMessageByContactSub = - lastOpenedStream.listen((lastActionsFuture) async { - final update = await lastActionsFuture; - lastOpenedMessageByContact = update; - await setMessages(allMessages, update, groupActions); - }); - final actionsStream = twonlyDB.groupsDao.watchGroupActions(group.groupId); groupActionsSub = actionsStream.listen((update) async { groupActions = update; - await setMessages(allMessages, lastOpenedMessageByContact, update); + await setMessages(allMessages, update); }); final contactsStream = twonlyDB.contactsDao.watchAllContacts(); @@ -154,14 +135,13 @@ class _ChatMessagesViewState extends State { // return; } await protectMessageUpdating.protect(() async { - await setMessages(update, lastOpenedMessageByContact, groupActions); + await setMessages(update, groupActions); }); }); } Future setMessages( List newMessages, - List<(Message, Contact)> lastOpenedMessageByContact, List groupActions, ) async { await flutterLocalNotificationsPlugin.cancelAll(); @@ -172,19 +152,7 @@ class _ChatMessagesViewState extends State { DateTime? lastDate; final openedMessages = >{}; - final lastOpenedMessageToContact = >{}; - final myLastMessageIndex = - newMessages.lastIndexWhere((t) => t.senderId == null); - - for (final opened in lastOpenedMessageByContact) { - if (!lastOpenedMessageToContact.containsKey(opened.$1.messageId)) { - lastOpenedMessageToContact[opened.$1.messageId] = [opened.$2]; - } else { - lastOpenedMessageToContact[opened.$1.messageId]!.add(opened.$2); - } - } - var index = 0; var groupHistoryIndex = 0; for (final msg in newMessages) { @@ -199,7 +167,6 @@ class _ChatMessagesViewState extends State { } } } - index += 1; if (msg.type != MessageType.media.name && msg.senderId != null && msg.openedAt == null) { @@ -221,16 +188,6 @@ class _ChatMessagesViewState extends State { lastDate = msg.createdAt; } chatItems.add(ChatItem.message(msg)); - - if (index <= myLastMessageIndex || index == newMessages.length) { - if (lastOpenedMessageToContact.containsKey(msg.messageId)) { - chatItems.add( - ChatItem.lastOpenedPosition( - lastOpenedMessageToContact[msg.messageId]!, - ), - ); - } - } } if (groupHistoryIndex < groupActions.length) { for (var i = groupHistoryIndex; i < groupActions.length; i++) { @@ -341,17 +298,6 @@ class _ChatMessagesViewState extends State { return ChatDateChip( item: messages[i], ); - } else if (messages[i].isLastOpenedPosition) { - return Wrap( - spacing: 8, - alignment: WrapAlignment.center, - children: messages[i].lastOpenedPosition!.map((w) { - return AvatarIcon( - contactId: w.userId, - fontSize: 12, - ); - }).toList(), - ); } else if (messages[i].isGroupAction) { return ChatGroupAction( key: Key(messages[i].groupAction!.groupHistoryId), From e9ea0a7f16ac2e94b7e7ffacea58707e7cb503b6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 14:54:09 +0100 Subject: [PATCH 2/8] feature: Show link in chat if the saved media file contains one --- CHANGELOG.md | 3 +- .../entries/chat_media_entry.dart | 95 ++++++++++++++----- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47c024e..da6d758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## 0.0.94 +## 0.0.95 +Feature: Show link in chat if the saved media file contains one Fix: Problem with decrypting messages fixed ## 0.0.93 diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart index 4a497b3..313fbff 100644 --- a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' hide Message; import 'package:twonly/src/services/api/mediafiles/download.service.dart' @@ -13,8 +14,10 @@ import 'package:twonly/src/services/api/mediafiles/download.service.dart' import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart'; import 'package:twonly/src/views/chats/media_viewer.view.dart'; +import 'package:twonly/src/views/components/better_text.dart'; class ChatMediaEntry extends StatefulWidget { const ChatMediaEntry({ @@ -114,31 +117,79 @@ class _ChatMediaEntryState extends State { context, ); - return GestureDetector( - key: reopenMediaFile, - onDoubleTap: onDoubleTap, - onTap: (widget.message.type == MessageType.media.name) ? onTap : null, - child: SizedBox( - width: (widget.minWidth > 150) ? widget.minWidth : 150, - height: (widget.message.mediaStored && - widget.mediaService.imagePreviewAvailable) - ? 271 - : null, - child: Align( - alignment: Alignment.centerRight, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: InChatMediaViewer( - message: widget.message, - group: widget.group, - mediaService: widget.mediaService, - color: color, - galleryItems: widget.galleryItems, - canBeReopened: _canBeReopened, + var imageBorderRadius = BorderRadius.circular(12); + + Widget additionalMessageData = Container(); + + final addData = widget.message.additionalMessageData; + if (addData != null) { + final info = + getBubbleInfo(context, widget.message, null, null, null, 200); + final data = AdditionalMessageData.fromBuffer(addData); + if (data.hasLink()) { + imageBorderRadius = const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(5), + bottomRight: Radius.circular(5), + ); + + additionalMessageData = Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + padding: + const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: info.color, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BetterText(text: data.link, textColor: info.textColor), + ], + ), + ); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + key: reopenMediaFile, + onDoubleTap: onDoubleTap, + onTap: (widget.message.type == MessageType.media.name) ? onTap : null, + child: SizedBox( + width: (widget.minWidth > 150) ? widget.minWidth : 150, + height: (widget.message.mediaStored && + widget.mediaService.imagePreviewAvailable) + ? 271 + : null, + child: Align( + alignment: Alignment.centerRight, + child: ClipRRect( + borderRadius: imageBorderRadius, + child: InChatMediaViewer( + message: widget.message, + group: widget.group, + mediaService: widget.mediaService, + color: color, + galleryItems: widget.galleryItems, + canBeReopened: _canBeReopened, + ), + ), ), ), ), - ), + additionalMessageData, + ], ); } } From d04828b020d3cfd3f0215fee7d2f1d7337d8de53 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 15:19:04 +0100 Subject: [PATCH 3/8] Improve: Verification badge for groups --- CHANGELOG.md | 1 + assets/icons/verified_badge_yellow.svg | 4 ---- lib/src/views/components/better_list_title.dart | 16 ++++++++++------ lib/src/views/components/svg_icon.dart | 1 - lib/src/views/components/verified_shield.dart | 2 +- lib/src/views/groups/group.view.dart | 13 ++++++++++++- lib/src/views/settings/help/faq/verifybadge.dart | 6 +++++- 7 files changed, 29 insertions(+), 14 deletions(-) delete mode 100644 assets/icons/verified_badge_yellow.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index da6d758..c9cb3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.0.95 Feature: Show link in chat if the saved media file contains one +Improve: Verification badge for groups Fix: Problem with decrypting messages fixed ## 0.0.93 diff --git a/assets/icons/verified_badge_yellow.svg b/assets/icons/verified_badge_yellow.svg deleted file mode 100644 index 29ee80a..0000000 --- a/assets/icons/verified_badge_yellow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index fac2f5e..e15aca3 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -3,7 +3,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class BetterListTile extends StatelessWidget { const BetterListTile({ - required this.text, + this.text, + this.textWidget, this.onTap, this.icon, this.leading, @@ -17,7 +18,8 @@ class BetterListTile extends StatelessWidget { final IconData? icon; final Widget? leading; final Widget? trailing; - final String text; + final String? text; + final Widget? textWidget; final Widget? subtitle; final Color? color; final VoidCallback? onTap; @@ -40,10 +42,12 @@ class BetterListTile extends StatelessWidget { ), ), trailing: trailing, - title: Text( - text, - style: TextStyle(color: color), - ), + title: text != null + ? Text( + text!, + style: TextStyle(color: color), + ) + : textWidget, subtitle: subtitle, onTap: onTap, ); diff --git a/lib/src/views/components/svg_icon.dart b/lib/src/views/components/svg_icon.dart index efd067a..8691f59 100644 --- a/lib/src/views/components/svg_icon.dart +++ b/lib/src/views/components/svg_icon.dart @@ -3,7 +3,6 @@ import 'package:flutter_svg/flutter_svg.dart'; class SvgIcons { static const String verifiedGreen = 'assets/icons/verified_badge_green.svg'; - static const String verifiedYellow = 'assets/icons/verified_badge_yellow.svg'; static const String verifiedRed = 'assets/icons/verified_badge_red.svg'; } diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index a18684d..f2042d7 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -37,7 +37,7 @@ class _VerifiedShieldState extends State { contact = contacts.first; } setState(() { - isVerified = contacts.any((t) => t.verified); + isVerified = contacts.every((t) => t.verified); }); }); } else if (widget.contact != null) { diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index fcfae5f..8f56bda 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -242,7 +242,18 @@ class _GroupViewState extends State { contactId: member.$1.userId, fontSize: 16, ), - text: getContactDisplayName(member.$1, maxLength: 25), + textWidget: Row( + children: [ + Text(getContactDisplayName(member.$1, maxLength: 25)), + Padding( + padding: const EdgeInsets.only(left: 5), + child: VerifiedShield( + key: Key(member.$2.contactId.toString()), + contact: member.$1, + ), + ), + ], + ), trailing: (member.$2.memberState == MemberState.admin) ? Text(context.lang.admin) : null, diff --git a/lib/src/views/settings/help/faq/verifybadge.dart b/lib/src/views/settings/help/faq/verifybadge.dart index f3efb3d..b0568a7 100644 --- a/lib/src/views/settings/help/faq/verifybadge.dart +++ b/lib/src/views/settings/help/faq/verifybadge.dart @@ -30,7 +30,11 @@ class _VerificationBadeFaqViewState extends State { description: context.lang.verificationBadgeGreenDesc, ), _buildItem( - icon: const SvgIcon(assetPath: SvgIcons.verifiedYellow, size: 40), + icon: const SvgIcon( + assetPath: SvgIcons.verifiedGreen, + size: 40, + color: Color.fromARGB(255, 227, 227, 3), + ), description: context.lang.verificationBadgeYellowDesc, ), _buildItem( From 87ddb8ebdb6c3c5e50ab2a3bef32f800d8e84d6e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 16:32:21 +0100 Subject: [PATCH 4/8] remove ffmpeg from dependencies to reduce file size --- CHANGELOG.md | 5 ++ ios/Podfile.lock | 21 +++--- lib/src/model/json/userdata.dart | 3 - lib/src/model/json/userdata.g.dart | 3 - .../mediafiles/compression.service.dart | 72 +++++++++---------- .../mediafiles/thumbnail.service.dart | 26 ++++--- .../views/camera/share_image_editor.view.dart | 2 +- .../settings/developer/developer.view.dart | 19 ----- .../views/settings/help/diagnostics.view.dart | 12 +++- pubspec.lock | 32 ++++----- pubspec.yaml | 5 +- 11 files changed, 95 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9cb3ba..641f7bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Feature: Show link in chat if the saved media file contains one Improve: Verification badge for groups +Improve: Huge reduction in app size +Fix: Crash on older devices when compressing a video + +## 0.0.94 + Fix: Problem with decrypting messages fixed ## 0.0.93 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3b9e45f..ae7f32b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -46,11 +46,6 @@ PODS: - SwiftyGif - emoji_picker_flutter (0.0.1): - Flutter - - ffmpeg_kit_flutter_new (1.0.0): - - ffmpeg_kit_flutter_new/full-gpl (= 1.0.0) - - Flutter - - ffmpeg_kit_flutter_new/full-gpl (1.0.0): - - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -278,6 +273,8 @@ PODS: - Flutter - permission_handler_apple (9.3.0): - Flutter + - pro_video_editor (0.0.1): + - Flutter - PromisesObjC (2.4.0) - restart_app (0.0.1): - Flutter @@ -329,6 +326,8 @@ PODS: - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter + - video_compress (0.3.0): + - Flutter - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS @@ -342,7 +341,6 @@ DEPENDENCIES: - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - - ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -368,6 +366,7 @@ DEPENDENCIES: - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`) - restart_app (from `.symlinks/plugins/restart_app/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -376,6 +375,7 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - SwiftProtobuf - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: @@ -428,8 +428,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/device_info_plus/ios" emoji_picker_flutter: :path: ".symlinks/plugins/emoji_picker_flutter/ios" - ffmpeg_kit_flutter_new: - :path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" firebase_core: @@ -470,6 +468,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + pro_video_editor: + :path: ".symlinks/plugins/pro_video_editor/ios" restart_app: :path: ".symlinks/plugins/restart_app/ios" sentry_flutter: @@ -484,6 +484,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" @@ -498,7 +500,6 @@ SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc - ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 @@ -540,6 +541,7 @@ SPEC CHECKSUMS: no_screenshot: 5e345998c43ffcad5d6834f249590483fcc037bd package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 @@ -554,6 +556,7 @@ SPEC CHECKSUMS: SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7 diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 9945264..39bd078 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -31,9 +31,6 @@ class UserData { @JsonKey(defaultValue: false) bool isDeveloper = false; - @JsonKey(defaultValue: false) - bool disableVideoCompression = false; - @JsonKey(defaultValue: 0) int deviceId = 0; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 3dc2fbb..83ddff8 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -17,8 +17,6 @@ UserData _$UserDataFromJson(Map json) => UserData( ..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..isDeveloper = json['isDeveloper'] as bool? ?? false - ..disableVideoCompression = - json['disableVideoCompression'] as bool? ?? false ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? ..lastImageSend = json['lastImageSend'] == null @@ -95,7 +93,6 @@ Map _$UserDataToJson(UserData instance) => { 'appVersion': instance.appVersion, 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, - 'disableVideoCompression': instance.disableVideoCompression, 'deviceId': instance.deviceId, 'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index d2e492b..da9f156 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'dart:io'; import 'package:drift/drift.dart' show Value; -import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_new/return_code.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:video_compress/video_compress.dart'; Future compressImage( File sourceFile, @@ -69,51 +69,47 @@ Future compressAndOverlayVideo(MediaFileService media) async { media.ffmpegOutputPath.deleteSync(); } - if (gUser.disableVideoCompression) { - media.originalPath.copySync(media.tempPath.path); - return; - } - - var overLayCommand = ''; - if (media.overlayImagePath.existsSync()) { - if (Platform.isAndroid) { - overLayCommand = - '-i "${media.overlayImagePath.path}" -filter_complex "[1:v]format=yuva420p[ovr_in];[0:v]format=yuv420p[base_in];[ovr_in][base_in]scale2ref=w=rw:h=rh[ovr_out][base_out];[base_out][ovr_out]overlay=0:0"'; - } else { - overLayCommand = - '-i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0"'; - } - } - final stopwatch = Stopwatch()..start(); - var additionalParams = ''; + try { + final task = VideoRenderData( + video: EditorVideo.file(media.originalPath), + // qualityPreset: VideoQualityPreset.p720High, + imageBytes: media.overlayImagePath.readAsBytesSync(), + enableAudio: !media.removeAudio, + ); - if (Platform.isAndroid) { - additionalParams += ' -c:v libx264'; - } + final result = await ProVideoEditor.instance.renderVideo(task); + media.ffmpegOutputPath.writeAsBytesSync(result); - var command = - '-i "${media.originalPath.path}" $overLayCommand -map "0:a?" $additionalParams -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"'; + MediaInfo? mediaInfo; + try { + mediaInfo = await VideoCompress.compressVideo( + media.ffmpegOutputPath.path, + quality: VideoQuality.Res640x480Quality, + includeAudio: true, + ); + Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); + } catch (e) { + Log.error('during video compression: $e'); + } - if (media.removeAudio) { - command = - '-i "${media.originalPath.path}" $overLayCommand $additionalParams -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"'; - } + if (mediaInfo == null) { + Log.error('Could not compress video using original video.'); + // as a fall back use the non compressed version + media.ffmpegOutputPath.renameSync(media.tempPath.path); + } else { + mediaInfo.file!.renameSync(media.tempPath.path); + } - final session = await FFmpegKit.execute(command); - final returnCode = await session.getReturnCode(); - - if (ReturnCode.isSuccess(returnCode)) { - media.ffmpegOutputPath.copySync(media.tempPath.path); stopwatch.stop(); Log.info( - 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video', + 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from ${media.ffmpegOutputPath.statSync().size} to ${media.tempPath.statSync().size} bytes.', ); - } else { - Log.info(command); - Log.error('Compression failed for the video with exit code $returnCode.'); - Log.error(await session.getAllLogsAsString()); + } catch (e) { + Log.error(e); + // Log.error('Compression failed for the video with exit code $returnCode.'); + // Log.error(await session.getAllLogsAsString()); // This should not happen, but in case "notify" the user that the video was not send... This is absolutely bad, but // better this way then sending an uncompressed media file which potentially is 100MB big :/ // Hopefully the user will report the strange behavior <3 diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index 53275b3..c6c0d81 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_new/return_code.dart'; +import 'dart:ui'; +import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:twonly/src/utils/log.dart'; Future createThumbnailsForVideo( @@ -13,22 +13,26 @@ Future createThumbnailsForVideo( return; } - final command = - '-y -i "${sourceFile.path}" -ss 00:00:00 -vframes 1 -vf "scale=iw:ih:flags=lanczos" -c:v libwebp -q:v 100 -compression_level 6 "${destinationFile.path}"'; + final images = await ProVideoEditor.instance.getThumbnails( + ThumbnailConfigs( + video: EditorVideo.file(sourceFile), + outputFormat: ThumbnailFormat.webp, + timestamps: const [ + Duration.zero, + ], + outputSize: const Size(272, 153), + ), + ); - final session = await FFmpegKit.execute(command); - final returnCode = await session.getReturnCode(); - - if (ReturnCode.isSuccess(returnCode)) { + if (images.isNotEmpty) { stopwatch.stop(); + destinationFile.writeAsBytesSync(images.first); Log.info( 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', ); } else { - Log.info(command); Log.error( - 'Thumbnail creation failed for the video with exit code $returnCode.', + 'Thumbnail creation failed for the video with exit code.', ); - Log.error(await session.getAllLogsAsString()); } } diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index 9d12615..4e31b88 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -444,7 +444,7 @@ class _ShareImageEditorView extends State { setState(() {}); // Make a short delay, so the setState does have its effect... - await Future.delayed(const Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 80)); final image = await screenshotController.capture( pixelRatio: pixelRatio, diff --git a/lib/src/views/settings/developer/developer.view.dart b/lib/src/views/settings/developer/developer.view.dart index 37413cf..920eba9 100644 --- a/lib/src/views/settings/developer/developer.view.dart +++ b/lib/src/views/settings/developer/developer.view.dart @@ -29,14 +29,6 @@ class _DeveloperSettingsViewState extends State { setState(() {}); } - Future toggleVideoCompression() async { - await updateUserdata((u) { - u.disableVideoCompression = !u.disableVideoCompression; - return u; - }); - setState(() {}); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -75,17 +67,6 @@ class _DeveloperSettingsViewState extends State { } }, ), - ListTile( - title: const Text('Disable ffmpeg'), - subtitle: const Text( - 'If your smartphone crashes, you can disable ffmpeg. This will prevent your videos from being compressed and NO FILTER will be applied to the video! This is a workaround, until the root-cause in ffmpeg is found.', - ), - onTap: toggleVideoCompression, - trailing: Switch( - value: gUser.disableVideoCompression, - onChanged: (a) => toggleVideoCompression(), - ), - ), if (!kReleaseMode) ListTile( title: const Text('Automated Testing'), diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index a96cd1c..faa5bc0 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/loader.dart'; class DiagnosticsView extends StatefulWidget { @@ -150,14 +151,19 @@ class _LogViewerWidgetState extends State { } TextSpan _formatLineSpan(_LogEntry e) { - final tsStyle = - TextStyle(color: Colors.grey.shade500, fontFamily: 'monospace'); + final tsStyle = TextStyle( + color: isDarkMode(context) ? Colors.white : Colors.black, + fontFamily: 'monospace', + ); final levelStyle = TextStyle( color: Colors.blueGrey.shade600, fontWeight: FontWeight.bold, fontFamily: 'monospace', ); - const msgStyle = TextStyle(fontFamily: 'monospace'); + final msgStyle = TextStyle( + color: isDarkMode(context) ? Colors.white : Colors.black, + fontFamily: 'monospace', + ); return TextSpan( children: [ diff --git a/pubspec.lock b/pubspec.lock index 240bf52..3ee608d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,22 +456,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - ffmpeg_kit_flutter_new: - dependency: "direct main" - description: - name: ffmpeg_kit_flutter_new - sha256: d127635f27e93a7f21f0a14ce0a1a148e80919c402dac4a2118d73bfb17ce841 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - ffmpeg_kit_flutter_platform_interface: - dependency: transitive - description: - name: ffmpeg_kit_flutter_platform_interface - sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee - url: "https://pub.dev" - source: hosted - version: "0.2.1" file: dependency: transitive description: @@ -1513,6 +1497,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + pro_video_editor: + dependency: "direct main" + description: + name: pro_video_editor + sha256: "0d985f7653c59e2b521d19db49351476eb74eb4001689b33fb8112ab1a9c4330" + url: "https://pub.dev" + source: hosted + version: "1.6.1" protobuf: dependency: "direct main" description: @@ -1996,6 +1988,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.1.0" + video_compress: + dependency: "direct main" + description: + name: video_compress + sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" + url: "https://pub.dev" + source: hosted + version: "3.1.4" video_player: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 097f6c6..73a3b38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.94+94 +version: 0.0.95+95 environment: sdk: ^3.6.0 @@ -83,7 +83,6 @@ dependencies: cached_network_image: ^3.4.1 cryptography_flutter_plus: ^2.3.4 cryptography_plus: ^2.7.0 - ffmpeg_kit_flutter_new: ^4.1.0 flutter_android_volume_keydown: ^1.0.1 flutter_image_compress: ^2.4.0 flutter_volume_controller: ^1.3.4 @@ -113,6 +112,8 @@ dependencies: flutter_sharing_intent: ^2.0.4 no_screenshot: ^0.3.1 google_mlkit_face_detection: ^0.13.1 + pro_video_editor: ^1.6.1 + video_compress: ^3.1.4 dependency_overrides: dots_indicator: From 609c7abb55b6e55916e249605ebed0b8c69e1e35 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 16:46:04 +0100 Subject: [PATCH 5/8] fixes issue on android --- CHANGELOG.md | 2 +- lib/src/services/mediafiles/compression.service.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 641f7bd..078742f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.0.95 +## 0.0.96 Feature: Show link in chat if the saved media file contains one Improve: Verification badge for groups diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index da9f156..a5a828c 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -97,9 +97,9 @@ Future compressAndOverlayVideo(MediaFileService media) async { if (mediaInfo == null) { Log.error('Could not compress video using original video.'); // as a fall back use the non compressed version - media.ffmpegOutputPath.renameSync(media.tempPath.path); + media.ffmpegOutputPath.copySync(media.tempPath.path); } else { - mediaInfo.file!.renameSync(media.tempPath.path); + mediaInfo.file!.copySync(media.tempPath.path); } stopwatch.stop(); diff --git a/pubspec.yaml b/pubspec.yaml index 73a3b38..4ef5a04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.95+95 +version: 0.0.96+96 environment: sdk: ^3.6.0 From cd00910e8624a2375e6c6de4a9e55d6ab89fc570 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 21:37:57 +0100 Subject: [PATCH 6/8] fix link shown even if message was not stored --- .../chat_messages_components/entries/chat_media_entry.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart index 313fbff..6668e78 100644 --- a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -126,7 +126,7 @@ class _ChatMediaEntryState extends State { final info = getBubbleInfo(context, widget.message, null, null, null, 200); final data = AdditionalMessageData.fromBuffer(addData); - if (data.hasLink()) { + if (data.hasLink() && widget.message.mediaStored) { imageBorderRadius = const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), @@ -160,7 +160,9 @@ class _ChatMediaEntryState extends State { } return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: widget.message.senderId == null + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ GestureDetector( key: reopenMediaFile, From 3806525653c07c6484bcf045ec14af8742b849a8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 21:38:32 +0100 Subject: [PATCH 7/8] use sql match --- lib/src/database/daos/messages.dao.dart | 53 +++++++++++-------- .../api/client2client/messages.c2c.dart | 22 ++++---- lib/src/services/api/messages.dart | 21 ++++---- lib/src/views/chats/chat_messages.view.dart | 6 --- 4 files changed, 53 insertions(+), 49 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 71bf8d2..db0b2d6 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -212,31 +212,40 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); } - Future handleMessageOpened( + Future handleMessagesOpened( int contactId, - String messageId, + List messageIds, DateTime timestamp, ) async { - await into(messageActions).insertOnConflictUpdate( - MessageActionsCompanion( - messageId: Value(messageId), - contactId: Value(contactId), - type: const Value(MessageActionType.openedAt), - actionAt: Value(timestamp), - ), - ); - // Directly show as message opened as soon as one person has opened it - final openedByAll = - await haveAllMembers(messageId, MessageActionType.openedAt) - ? clock.now() - : null; - await twonlyDB.messagesDao.updateMessageId( - messageId, - MessagesCompanion( - openedAt: Value(clock.now()), - openedByAll: Value(openedByAll), - ), - ); + await batch((batch) async { + for (final messageId in messageIds) { + batch.insert( + messageActions, + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.openedAt), + actionAt: Value(timestamp), + ), + mode: InsertMode.insertOrReplace, + ); + } + + for (final messageId in messageIds) { + final isOpenedByAll = + await haveAllMembers(messageId, MessageActionType.openedAt); + final now = clock.now(); + + batch.update( + twonlyDB.messages, + MessagesCompanion( + openedAt: Value(now), + openedByAll: Value(isOpenedByAll ? now : null), + ), + where: (tbl) => tbl.messageId.equals(messageId), + ); + } + }); } Future handleMessageAckByServer( diff --git a/lib/src/services/api/client2client/messages.c2c.dart b/lib/src/services/api/client2client/messages.c2c.dart index 8a50ac6..e193ed3 100644 --- a/lib/src/services/api/client2client/messages.c2c.dart +++ b/lib/src/services/api/client2client/messages.c2c.dart @@ -9,19 +9,17 @@ Future handleMessageUpdate( ) async { switch (messageUpdate.type) { case EncryptedContent_MessageUpdate_Type.OPENED: - for (final targetMessageId in messageUpdate.multipleTargetMessageIds) { - Log.info( - 'Opened message $targetMessageId', + Log.info( + 'Opened message ${messageUpdate.multipleTargetMessageIds}', + ); + try { + await twonlyDB.messagesDao.handleMessagesOpened( + contactId, + messageUpdate.multipleTargetMessageIds, + fromTimestamp(messageUpdate.timestamp), ); - try { - await twonlyDB.messagesDao.handleMessageOpened( - contactId, - targetMessageId, - fromTimestamp(messageUpdate.timestamp), - ); - } catch (e) { - Log.warn(e); - } + } catch (e) { + Log.warn(e); } case EncryptedContent_MessageUpdate_Type.DELETE: if (!await isSender(contactId, messageUpdate.senderMessageId)) { diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index b4452eb..2e83f50 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -375,15 +375,18 @@ Future notifyContactAboutOpeningMessage( ), blocking: false, ); - for (final messageId in messageOtherIds) { - await twonlyDB.messagesDao.updateMessageId( - messageId, - MessagesCompanion( - openedAt: Value(actionAt), - openedByAll: Value(actionAt), - ), - ); - } + await twonlyDB.batch((batch) { + for (final messageId in messageOtherIds) { + batch.update( + twonlyDB.messages, + MessagesCompanion( + openedAt: Value(actionAt), + openedByAll: Value(actionAt), + ), + where: (tbl) => tbl.messageId.equals(messageId), + ); + } + }); await updateLastMessageId(contactId, biggestMessageId); } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 422f18f..df66b41 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -128,12 +128,6 @@ class _ChatMessagesViewState extends State { final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); messageSub = msgStream.listen((update) async { allMessages = update; - - /// In case a message is not open yet the message is updated, which will trigger this watch to be called again. - /// So as long as the Mutex is locked just return... - if (protectMessageUpdating.isLocked) { - // return; - } await protectMessageUpdating.protect(() async { await setMessages(update, groupActions); }); From fb1e286cf994a4cf29e8985e551eecb28f8e0605 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Mar 2026 23:34:07 +0100 Subject: [PATCH 8/8] fix signal out of sync issue --- CHANGELOG.md | 3 - .../client/generated/messages.pbenum.dart | 10 +- .../client/generated/messages.pbjson.dart | 126 +++++++++--------- lib/src/model/protobuf/client/messages.proto | 1 + .../api/client2client/errors.c2c.dart | 8 +- lib/src/services/api/server_messages.dart | 49 +++---- lib/src/services/api/utils.dart | 2 +- lib/src/services/group.services.dart | 2 +- .../services/signal/encryption.signal.dart | 33 ++++- lib/src/services/signal/session.signal.dart | 41 ++---- lib/src/services/signal/utils.signal.dart | 5 + .../developer/automated_testing.view.dart | 106 +++++++++++---- pubspec.yaml | 2 +- 13 files changed, 232 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078742f..d21b202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,6 @@ Feature: Show link in chat if the saved media file contains one Improve: Verification badge for groups Improve: Huge reduction in app size Fix: Crash on older devices when compressing a video - -## 0.0.94 - Fix: Problem with decrypting messages fixed ## 0.0.93 diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart index 8277780..a8d9e86 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -76,17 +76,21 @@ class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum { static const EncryptedContent_ErrorMessages_Type UNKNOWN_MESSAGE_TYPE = EncryptedContent_ErrorMessages_Type._( 2, _omitEnumNames ? '' : 'UNKNOWN_MESSAGE_TYPE'); + static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC = + EncryptedContent_ErrorMessages_Type._( + 3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC'); static const $core.List values = [ ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, UNKNOWN_MESSAGE_TYPE, + SESSION_OUT_OF_SYNC, ]; - static final $core.Map<$core.int, EncryptedContent_ErrorMessages_Type> - _byValue = $pb.ProtobufEnum.initByValue(values); + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) => - _byValue[value]; + value < 0 || value >= _byValue.length ? null : _byValue[value]; const EncryptedContent_ErrorMessages_Type._(super.value, super.name); } diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index ac60f8f..a8a1fa3 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -395,6 +395,7 @@ const EncryptedContent_ErrorMessages_Type$json = { '2': [ {'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0}, {'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2}, + {'1': 'SESSION_OUT_OF_SYNC', '2': 3}, ], }; @@ -863,67 +864,68 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS' 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h' - 'bERhdGFNZXNzYWdliAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' + 'bERhdGFNZXNzYWdliAEBGvABCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' 'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf' - 'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' + 'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' '1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT' - 'U0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5Ei' - 'YKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoO' - 'Z3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibG' - 'ljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0' - 'aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZI' - 'gBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0' - 'ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZn' - 'Rlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFt' - 'ZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2' - 'USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEo' - 'CVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZB' - 'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0' - 'aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2' - 'FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUS' - 'OwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYX' - 'RhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRN' - 'ZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFg' - 'oGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIk' - 'LkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3' - 'NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNz' - 'YWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUg' - 'R0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRF' - 'EAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdG' - 'V4dBrwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQS' - 'MAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaX' - 'NwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vj' - 'b25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbn' - 'RpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlk' - 'GAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG' - '93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmI' - 'AQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cH' - 'Rpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNz' - 'YWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUk' - 'VVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtf' - 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG' - '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0' - 'aW9uTm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2Cg' - 'R0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigK' - 'D3RhcmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUE' - 'VORUQQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVl' - 'c3QSOQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZV' - 'IEdHlwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoN' - 'Q29udGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VX' - 'BkYXRlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT' - 'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3' - 'BsYXlOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoK' - 'BlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcG' - 'xheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1' - 'c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgAS' - 'gMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUS' - 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQX' - 'QaqQEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZs' - 'YXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCm' - 'Jlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNl' - 'VXBkYXRlQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3' - 'VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2Nv' - 'bnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZX' - 'lzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VCDgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91' - 'cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbmRHcm91cFB1YmxpY0tleUIRCg9fZXJyb3' - 'JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdl'); + 'U0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADGlEKC0dyb3VwQ3JlYXRlEhoKCH' + 'N0YXRlS2V5GAMgASgMUghzdGF0ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQ' + 'dWJsaWNLZXkaMwoJR3JvdXBKb2luEiYKDmdyb3VwUHVibGljS2V5GAEgASgMUg5ncm91cFB1Ym' + 'xpY0tleRoWChRSZXNlbmRHcm91cFB1YmxpY0tleRq2AgoLR3JvdXBVcGRhdGUSKAoPZ3JvdXBB' + 'Y3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZWRDb250YWN0SWQYAi' + 'ABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1lGAMgASgJSAFSDG5l' + 'd0dyb3VwTmFtZYgBARJTCiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgAS' + 'gDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFAoSX2FmZmVjdGVk' + 'Q29udGFjdElkQg8KDV9uZXdHcm91cE5hbWVCJQojX25ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW' + 'xsaXNlY29uZHMaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgBIAEoCVIPc2Vu' + 'ZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW' + '1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEQoP' + 'X3F1b3RlTWVzc2FnZUlkGs4BChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3' + 'NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVz' + 'dGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgAS' + 'gMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBAUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2Rh' + 'dGEaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSW' + 'QSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNz' + 'YWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdG' + 'UuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlk' + 'iAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZX' + 'NzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRp' + 'bWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQh' + 'IKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJ' + 'ZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW' + '50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANI' + 'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdG' + 'lvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRp' + 'bWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg' + '1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktl' + 'eRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW' + '5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v' + 'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX' + 'NzYWdlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxAC' + 'EgcKA0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD1' + '9xdW90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5f' + 'ZW5jcnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2' + 'VfZGF0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQu' + 'TWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE' + '1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElP' + 'Tl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb2' + '50ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoG' + 'UkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' + 'VuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0Nv' + 'bXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIA' + 'EoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgB' + 'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' + 'NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEg' + 'ASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgAS' + 'gDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgD' + 'SAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2' + 'V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVy' + 'GAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbG' + 'FzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAK' + 'C2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3' + 'RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVk' + 'aWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdE' + 'IMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdl' + 'Qg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW' + '5kR3JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFf' + 'bWVzc2FnZQ=='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 062fc6f..7154bac 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -58,6 +58,7 @@ message EncryptedContent { enum Type { ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0; UNKNOWN_MESSAGE_TYPE = 2; + SESSION_OUT_OF_SYNC = 3; } Type type = 1; string related_receipt_id = 2; diff --git a/lib/src/services/api/client2client/errors.c2c.dart b/lib/src/services/api/client2client/errors.c2c.dart index d5259f0..598424e 100644 --- a/lib/src/services/api/client2client/errors.c2c.dart +++ b/lib/src/services/api/client2client/errors.c2c.dart @@ -3,14 +3,15 @@ import 'package:drift/drift.dart' show Value; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; +import 'package:twonly/src/utils/log.dart'; Future handleErrorMessage( int fromUserId, EncryptedContent_ErrorMessages error, ) async { + Log.error('Got error from $fromUserId: $error'); + switch (error.type) { - case EncryptedContent_ErrorMessages_Type.UNKNOWN_MESSAGE_TYPE: - break; case EncryptedContent_ErrorMessages_Type .ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD: await twonlyDB.receiptsDao.updateReceiptWidthUserId( @@ -25,5 +26,8 @@ Future handleErrorMessage( requested: Value(true), ), ); + // ignore: no_default_cases + default: + break; } } diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 5e84cfe..3f88459 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -32,38 +32,41 @@ import 'package:twonly/src/utils/misc.dart'; final lockHandleServerMessage = Mutex(); Future handleServerMessage(server.ServerToClient msg) async { - /// Returns means, that the server can delete the message from the server. - final ok = client.Response_Ok()..none = true; - var response = client.Response()..ok = ok; + return lockHandleServerMessage.protect(() async { + /// Returns means, that the server can delete the message from the server. + final ok = client.Response_Ok()..none = true; + var response = client.Response()..ok = ok; - try { - if (msg.v0.hasRequestNewPreKeys()) { - response = await handleRequestNewPreKey(); - } else if (msg.v0.hasNewMessage()) { - await handleClient2ClientMessage(msg.v0.newMessage); - } else if (msg.v0.hasNewMessages()) { - for (final newMessage in msg.v0.newMessages.newMessages) { - try { - await handleClient2ClientMessage(newMessage); - } catch (e) { - Log.error(e); + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + await handleClient2ClientMessage(msg.v0.newMessage); + } else if (msg.v0.hasNewMessages()) { + for (final newMessage in msg.v0.newMessages.newMessages) { + try { + await handleClient2ClientMessage(newMessage); + } catch (e) { + Log.error(e); + } } + } else { + Log.error('Unknown server message: $msg'); } - } else { - Log.error('Unknown server message: $msg'); + } catch (e) { + Log.error(e); } - } catch (e) { - Log.error(e); - } - final v0 = client.V0() - ..seq = msg.v0.seq - ..response = response; + final v0 = client.V0() + ..seq = msg.v0.seq + ..response = response; - await apiService.sendResponse(ClientToServer()..v0 = v0); + await apiService.sendResponse(ClientToServer()..v0 = v0); + }); } DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); +bool alreadyPerformedAnResync = false; Mutex protectReceiptCheck = Mutex(); diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index facc198..38d5501 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -84,7 +84,7 @@ Future handleMediaError(MediaFile media) async { Future importSignalContactAndCreateRequest( server.Response_UserData userdata, ) async { - if (await createNewSignalSession(userdata)) { + if (await processSignalUserData(userdata)) { // 1. Setup notifications keys with the other user await setupNotificationWithUsers( forceContact: userdata.userId.toInt(), diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index e9c8697..e975778 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -472,7 +472,7 @@ Future addNewHiddenContact(int contactId) async { const Value(true), // this will hide the contact in the contact list ), ); - await createNewSignalSession(userData); + await processSignalUserData(userData); unawaited(setupNotificationWithUsers(forceContact: contactId)); return true; } diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index d499f95..a565f32 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -1,9 +1,12 @@ import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +// ignore: implementation_imports +import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/signal/consts.signal.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; @@ -17,7 +20,7 @@ Future signalEncryptMessage( return lockingSignalEncryption.protect(() async { try { final signalStore = (await getSignalStore())!; - final address = SignalProtocolAddress(target.toString(), defaultDeviceId); + final address = getSignalAddress(target); final session = SessionCipher.fromStore(signalStore, address); return await session.encrypt(plaintextContent); } catch (e) { @@ -27,16 +30,18 @@ Future signalEncryptMessage( }); } +bool alreadyPerformedAnResync = false; + Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> signalDecryptMessage( - int source, + int fromUserId, Uint8List encryptedContentRaw, int type, ) async { try { final session = SessionCipher.fromStore( (await getSignalStore())!, - SignalProtocolAddress(source.toString(), defaultDeviceId), + getSignalAddress(fromUserId), ); Uint8List plaintext; @@ -59,6 +64,26 @@ Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> } on InvalidKeyIdException catch (e) { Log.warn(e); return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN); + } on InvalidMessageException catch (e) { + if (!alreadyPerformedAnResync) { + if (await handleSessionResync(fromUserId)) { + // This flag prevents from resyncing the session the client received multiple new + // messages from the server he could not decrypt + alreadyPerformedAnResync = true; + + // This message contains a new PreKeyBundle establishing a new signal session + await sendCipherText( + fromUserId, + EncryptedContent( + errorMessages: EncryptedContent_ErrorMessages( + type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, + ), + ), + ); + } + } + Log.warn(e); + return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } catch (e) { Log.error(e); return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 6e48445..e00520e 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -6,17 +6,14 @@ import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -Future createNewSignalSession(Response_UserData userData) async { +Future processSignalUserData(Response_UserData userData) async { final SignalProtocolStore? signalStore = await getSignalStore(); if (signalStore == null) { return false; } - final targetAddress = SignalProtocolAddress( - userData.userId.toString(), - defaultDeviceId, - ); + final targetAddress = getSignalAddress(userData.userId.toInt()); final sessionBuilder = SessionBuilder.fromSignalStore( signalStore, @@ -82,30 +79,6 @@ Future deleteSessionWithTarget(int target) async { await signalStore.sessionStore.deleteSession(address); } -Future generateSessionFingerPrint(int target) async { - final signalStore = await getSignalStore(); - if (signalStore == null) return null; - try { - final targetIdentity = await signalStore - .getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId)); - if (targetIdentity != null) { - final generator = NumericFingerprintGenerator(5200); - final localFingerprint = generator.createFor( - 1, - Uint8List.fromList([gUser.userId]), - (await signalStore.getIdentityKeyPair()).getPublicKey(), - Uint8List.fromList([target]), - targetIdentity, - ); - - return localFingerprint; - } - return null; - } catch (e) { - return null; - } -} - Future getPublicKeyFromContact(int contactId) async { final signalStore = await getSignalStore(); if (signalStore == null) return null; @@ -124,3 +97,13 @@ Future getPublicKeyFromContact(int contactId) async { return null; } } + +Future handleSessionResync(int fromUserId) async { + final userData = await apiService.getUserById(fromUserId); + if (userData != null) { + Log.info('Got new session data from the server to re-sync the session'); + return processSignalUserData(userData); + } + Log.info('Could not download userdata from the server.'); + return false; +} diff --git a/lib/src/services/signal/utils.signal.dart b/lib/src/services/signal/utils.signal.dart index ef4a1cd..dc8ef36 100644 --- a/lib/src/services/signal/utils.signal.dart +++ b/lib/src/services/signal/utils.signal.dart @@ -1,6 +1,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/model/json/signal_identity.dart'; +import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; Future getSignalStore() async { @@ -18,3 +19,7 @@ Future getSignalStoreFromIdentity( signalIdentity.registrationId, ); } + +SignalProtocolAddress getSignalAddress(int userId) { + return SignalProtocolAddress(userId.toString(), defaultDeviceId); +} diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index d4beded..aafa579 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -26,42 +28,92 @@ class _AutomatedTestingViewState extends State { @override Widget build(BuildContext context) { + if (kReleaseMode) return Container(); return Scaffold( appBar: AppBar( title: const Text('Automated Testing'), ), body: ListView( children: [ - if (!kReleaseMode) - ListTile( - title: const Text('Sending a lot of messages.'), - subtitle: Text(lotsOfMessagesStatus), - onTap: () async { - final username = await showUserNameDialog(context); - if (username == null) return; - Log.info('Requested to send to $username'); + ListTile( + title: const Text('Trigger Signal Out-Of-Sync'), + onTap: () async { + final username = await showUserNameDialog(context); + if (username == null) return; + final contacts = await twonlyDB.contactsDao + .getContactsByUsername(username.toLowerCase()); + if (contacts.length != 1) { + Log.error('No single user fund'); + return; + } + final userId = contacts.first.userId; - final contacts = await twonlyDB.contactsDao - .getContactsByUsername(username.toLowerCase()); + final group = await twonlyDB.groupsDao.getDirectChat(userId); + if (group == null) { + Log.error('Target user must have a group!'); + return; + } - for (final contact in contacts) { - Log.info('Sending to ${contact.username}'); - final group = - await twonlyDB.groupsDao.getDirectChat(contact.userId); - for (var i = 0; i < 200; i++) { - setState(() { - lotsOfMessagesStatus = - 'At message $i to ${contact.username}.'; - }); - await insertAndSendTextMessage( - group!.groupId, - 'Message $i.', - null, - ); - } + final sessionStore = await getSignalStore(); + + // 1. Store a valid session + final originalSession = + await sessionStore!.loadSession(getSignalAddress(userId)); + final serializedSession = originalSession.serialize(); + + for (var i = 0; i < 10; i++) { + await insertAndSendTextMessage( + group.groupId, + 'DesyncTest_1', + null, + ); + } + + final corruptedSession = + SessionRecord.fromSerialized(serializedSession); + await sessionStore.storeSession( + getSignalAddress(userId), + corruptedSession, + ); + + await insertAndSendTextMessage( + group.groupId, + 'DesyncTest_2', + null, + ); + + // The other client should res + }, + ), + ListTile( + title: const Text('Sending a lot of messages.'), + subtitle: Text(lotsOfMessagesStatus), + onTap: () async { + final username = await showUserNameDialog(context); + if (username == null) return; + Log.info('Requested to send to $username'); + + final contacts = await twonlyDB.contactsDao + .getContactsByUsername(username.toLowerCase()); + + for (final contact in contacts) { + Log.info('Sending to ${contact.username}'); + final group = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + for (var i = 0; i < 200; i++) { + setState(() { + lotsOfMessagesStatus = + 'At message $i to ${contact.username}.'; + }); + await insertAndSendTextMessage( + group!.groupId, + 'Message $i.', + null, + ); } - }, - ), + } + }, + ), ], ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 4ef5a04..9eb29ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.96+96 +version: 0.0.97+97 environment: sdk: ^3.6.0