From 90e6aa659855b52b87e5be5f4d1a1677817ffe27 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 18 Oct 2025 17:20:34 +0200 Subject: [PATCH 01/76] added tests and some fixes --- lib/src/services/api/server_messages.dart | 40 +++++------ lib/src/services/signal/identity.signal.dart | 3 +- lib/src/views/chats/chat_messages.view.dart | 13 ++-- .../developer/automated_testing.view.dart | 66 +++++++++++++++++++ .../settings/developer/developer.view.dart | 15 +++++ test/signal/key_generation.dart | 25 +++++++ 6 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 lib/src/views/settings/developer/automated_testing.view.dart create mode 100644 test/signal/key_generation.dart diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index dbe4f7b..ba89de0 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -35,30 +35,30 @@ import 'package:twonly/src/views/components/animate_icon.dart'; final lockHandleServerMessage = Mutex(); Future handleServerMessage(server.ServerToClient msg) async { - return lockHandleServerMessage.protect(() async { - client.Response? response; + // return lockHandleServerMessage.protect(() async { + client.Response? response; - try { - if (msg.v0.hasRequestNewPreKeys()) { - response = await handleRequestNewPreKey(); - } else if (msg.v0.hasNewMessage()) { - final body = Uint8List.fromList(msg.v0.newMessage.body); - final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - response = await handleNewMessage(fromUserId, body); - } else { - Log.error('Got a unknown message from the server: $msg'); - response = client.Response()..error = ErrorCode.InternalError; - } - } catch (e) { + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + final body = Uint8List.fromList(msg.v0.newMessage.body); + final fromUserId = msg.v0.newMessage.fromUserId.toInt(); + response = await handleNewMessage(fromUserId, body); + } else { + Log.error('Got a unknown message from the server: $msg'); response = client.Response()..error = ErrorCode.InternalError; } + } catch (e) { + response = client.Response()..error = ErrorCode.InternalError; + } - 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 lastSignalDecryptMessage = @@ -128,6 +128,8 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { .getRetransmissionFromHash(fromUserId, hash); if (message != null) { unawaited(sendRetransmitMessage(message.retransmissionId)); + } else { + Log.error('Could not find message to retransmit!'); } } diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 866b66c..c4554d7 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -63,7 +63,8 @@ Future> signalGetPreKeys() async { final start = user.currentPreKeyIndexStart; await updateUserdata((user) { - user.currentPreKeyIndexStart += 200; + user.currentPreKeyIndexStart = + (user.currentPreKeyIndexStart + 200) % maxValue; return user; }); final preKeys = generatePreKeys(start, 200); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index c00c514..654bd7c 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -133,7 +133,8 @@ class _ChatMessagesViewState extends State { DateTime? lastDate; final tmpEmojiReactionsToMessageId = >{}; - final openedMessageOtherIds = []; + // only send openedMessage to one text message, as receiver will then set all as read... + int? openedTextMessageOtherIds; final messageOtherMessageIdToMyMessageId = {}; final messageIdToMessage = {}; @@ -150,8 +151,10 @@ class _ChatMessagesViewState extends State { for (final msg in newMessages) { if (msg.kind == MessageKind.textMessage && msg.messageOtherId != null && - msg.openedAt == null) { - openedMessageOtherIds.add(msg.messageOtherId!); + msg.openedAt == null && + (openedTextMessageOtherIds == null || + openedTextMessageOtherIds < msg.messageOtherId!)) { + openedTextMessageOtherIds = msg.messageOtherId; } Message? responseTo; @@ -207,10 +210,10 @@ class _ChatMessagesViewState extends State { } } - if (openedMessageOtherIds.isNotEmpty) { + if (openedTextMessageOtherIds != null) { await notifyContactAboutOpeningMessage( widget.contact.userId, - openedMessageOtherIds, + [openedTextMessageOtherIds], ); } diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart new file mode 100644 index 0000000..8d9c415 --- /dev/null +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/services/api/messages.dart'; + +class AutomatedTestingView extends StatefulWidget { + const AutomatedTestingView({super.key}); + + @override + State createState() => _AutomatedTestingViewState(); +} + +class _AutomatedTestingViewState extends State { + String lotsOfMessagesStatus = ''; + @override + void initState() { + super.initState(); + unawaited(initAsync()); + } + + Future initAsync() async {} + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Automated Testing'), + ), + body: ListView( + children: [ + if (kDebugMode) + ListTile( + title: const Text('Sending a lot of messages.'), + subtitle: Text(lotsOfMessagesStatus), + onTap: () async { + await twonlyDB.messageRetransmissionDao + .clearRetransmissionTable(); + + final contacts = + await twonlyDB.contactsDao.getAllNotBlockedContacts(); + + for (final contact in contacts) { + for (var i = 0; i < 200; i++) { + setState(() { + lotsOfMessagesStatus = + 'At message $i to ${contact.username}.'; + }); + await sendTextMessage( + contact.userId, + TextMessageContent( + text: 'TestMessage $i', + ), + null, + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/views/settings/developer/developer.view.dart b/lib/src/views/settings/developer/developer.view.dart index c2cd49c..d4df29f 100644 --- a/lib/src/views/settings/developer/developer.view.dart +++ b/lib/src/views/settings/developer/developer.view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; class DeveloperSettingsView extends StatefulWidget { @@ -76,6 +77,20 @@ class _DeveloperSettingsViewState extends State { await syncFlameCounters(); }, ), + if (kDebugMode) + ListTile( + title: const Text('Automated Testing'), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AutomatedTestingView(); + }, + ), + ); + }, + ), ], ), ); diff --git a/test/signal/key_generation.dart b/test/signal/key_generation.dart new file mode 100644 index 0000000..ae45c74 --- /dev/null +++ b/test/signal/key_generation.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; + +void main() { + group('testing api', () { + test('testing api connection', () async { + const offset = 100; + const count = 400; + + var prekeys = generatePreKeys(offset, count); + expect(count, prekeys.length); + + for (var i = 0; i < prekeys.length; i++) { + expect(prekeys[i].id, offset + i); + } + + prekeys += generatePreKeys(offset + count, count); + expect(count * 2, prekeys.length); + + for (var i = 0; i < (count * 2); i++) { + expect(prekeys[i].id, offset + i); + } + }); + }); +} From a4ccefec75aad309feda3c5c860f09fd3a880ac2 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Oct 2025 02:45:17 +0200 Subject: [PATCH 02/76] starting with #227 --- .gitmodules | 6 - build.yaml | 3 +- dependencies/flutter-pie-menu | 1 - dependencies/flutter_secure_storage | 1 - .../push_notification.pb.swift | 52 +- lib/globals.dart | 4 +- lib/main.dart | 18 +- .../{contacts_dao.dart => contacts.dao.dart} | 70 +- ...ontacts_dao.g.dart => contacts.dao.g.dart} | 4 +- lib/src/database/daos/groups.dao.dart | 21 + lib/src/database/daos/groups.dao.g.dart | 10 + lib/src/database/daos/media_uploads_dao.dart | 50 - .../database/daos/media_uploads_dao.g.dart | 8 - .../daos/message_retransmissions.dao.dart | 123 - .../daos/message_retransmissions.dao.g.dart | 11 - lib/src/database/daos/messages.dao.dart | 377 + lib/src/database/daos/messages.dao.g.dart | 12 + lib/src/database/daos/messages_dao.dart | 311 - lib/src/database/daos/messages_dao.g.dart | 9 - lib/src/database/daos/reactions.dao.dart | 44 + lib/src/database/daos/reactions.dao.g.dart | 11 + lib/src/database/daos/receipts.dao.dart | 52 + lib/src/database/daos/receipts.dao.g.dart | 11 + .../daos/{signal_dao.dart => signal.dao.dart} | 10 +- .../{signal_dao.g.dart => signal.dao.g.dart} | 5 +- .../signal/connect_identity_key_store.dart | 2 +- .../signal/connect_pre_key_store.dart | 2 +- .../signal/connect_sender_key_store.dart | 2 +- .../signal/connect_session_store.dart | 2 +- lib/src/database/tables/contacts.table.dart | 41 + lib/src/database/tables/groups.table.dart | 34 + lib/src/database/tables/mediafiles.table.dart | 48 + lib/src/database/tables/messages.table.dart | 52 + lib/src/database/tables/reactions.table.dart | 21 + lib/src/database/tables/receipts.table.dart | 30 + .../tables/signal_contact_prekey.table.dart | 13 + .../signal_contact_signed_prekey.table.dart | 14 + ...t => signal_identity_key_store.table.dart} | 0 ...e.dart => signal_pre_key_store.table.dart} | 0 ...art => signal_sender_key_store.table.dart} | 0 ...e.dart => signal_session_store.table.dart} | 0 .../contacts_table.dart | 0 .../media_uploads_table.dart | 0 .../message_retransmissions.dart | 4 +- .../messages_table.dart | 2 +- .../signal_contact_prekey_table.dart | 0 .../signal_contact_signed_prekey_table.dart | 0 .../signal_identity_key_store_table.dart | 12 + .../signal_pre_key_store_table.dart | 11 + .../signal_sender_key_store_table.dart | 10 + .../signal_session_store_table.dart | 12 + lib/src/database/twonly.db.dart | 124 + lib/src/database/twonly.db.g.dart | 10544 ++++++++++++++++ ...database.dart => twonly_database_old.dart} | 43 +- ...base.g.dart => twonly_database_old.g.dart} | 188 +- ...ps.dart => twonly_database_old.steps.dart} | 0 .../json/{message.dart => message_old.dart} | 2 +- lib/src/model/memory_item.model.dart | 4 +- .../protobuf/{backup => client}/backup.proto | 0 .../generated}/backup.pb.dart | 0 .../generated}/backup.pbenum.dart | 0 .../generated}/backup.pbjson.dart | 0 .../generated}/backup.pbserver.dart | 0 .../client/generated/messages.pb.dart | 1172 ++ .../client/generated/messages.pbenum.dart | 149 + .../client/generated/messages.pbjson.dart | 358 + .../client/generated/messages.pbserver.dart | 14 + .../generated}/push_notification.pb.dart | 0 .../generated}/push_notification.pbenum.dart | 0 .../generated}/push_notification.pbjson.dart | 0 .../push_notification.pbserver.dart | 0 lib/src/model/protobuf/client/messages.proto | 135 + .../push_notification.proto | 0 lib/src/services/api.service.dart | 2 +- lib/src/services/api/media_download.dart | 4 +- lib/src/services/api/media_upload.dart | 4 +- lib/src/services/api/messages.dart | 192 +- lib/src/services/api/server_messages.dart | 588 +- .../contact.server_messages.dart | 121 + .../media.server_messages.dart | 92 + .../messages.server_messages.dart | 35 + .../prekeys.server_messages.dart | 20 + .../pushkeys.server_messages.dart | 23 + .../reaction.server_message.dart | 25 + .../text_message.server_messages.dart | 125 + lib/src/services/api/utils.dart | 8 +- lib/src/services/flame.service.dart | 6 +- lib/src/services/mediafile.service.dart | 5 + .../notifications/pushkeys.notifications.dart | 8 +- .../services/signal/encryption.signal.dart | 60 +- lib/src/services/signal/prekeys.signal.dart | 2 +- .../create_backup.twonly_safe.dart | 2 +- .../twonly_safe/restore.twonly_safe.dart | 2 +- lib/src/utils/misc.dart | 4 +- .../camera_preview_controller_view.dart | 6 +- lib/src/views/camera/camera_send_to_view.dart | 2 +- .../best_friends_selector.dart | 4 +- .../views/camera/share_image_editor_view.dart | 4 +- lib/src/views/camera/share_image_view.dart | 4 +- lib/src/views/chats/add_new_user.view.dart | 6 +- lib/src/views/chats/chat_list.view.dart | 4 +- .../last_message_time.dart | 2 +- lib/src/views/chats/chat_messages.view.dart | 14 +- .../chat_list_entry.dart | 4 +- .../chat_media_entry.dart | 4 +- .../chat_reaction_row.dart | 4 +- .../in_chat_media_viewer.dart | 2 +- .../message_actions.dart | 2 +- .../message_context_menu.dart | 4 +- .../message_send_state_icon.dart | 4 +- .../response_container.dart | 6 +- lib/src/views/chats/media_viewer.view.dart | 6 +- lib/src/views/chats/start_new_chat.view.dart | 4 +- lib/src/views/components/flame.dart | 2 +- lib/src/views/components/initialsavatar.dart | 2 +- .../views/components/user_context_menu.dart | 2 +- lib/src/views/components/verified_shield.dart | 2 +- lib/src/views/contact/contact.view.dart | 4 +- .../views/contact/contact_verify.view.dart | 4 +- .../contact/contact_verify_qr_scan.view.dart | 2 +- lib/src/views/memories/memories.view.dart | 2 +- .../memories/memories_photo_slider.view.dart | 2 +- .../developer/automated_testing.view.dart | 2 +- .../developer/retransmission_data.view.dart | 4 +- .../settings/privacy_view_block.users.dart | 4 +- .../subscription/additional_users.view.dart | 2 +- .../subscription/subscription.view.dart | 2 +- pubspec.lock | 29 +- pubspec.yaml | 20 +- scripts/generate_proto.sh | 13 +- test/unit_test.dart | 9 + 131 files changed, 14355 insertions(+), 1435 deletions(-) delete mode 160000 dependencies/flutter-pie-menu delete mode 160000 dependencies/flutter_secure_storage rename lib/src/database/daos/{contacts_dao.dart => contacts.dao.dart} (82%) rename lib/src/database/daos/{contacts_dao.g.dart => contacts.dao.g.dart} (59%) create mode 100644 lib/src/database/daos/groups.dao.dart create mode 100644 lib/src/database/daos/groups.dao.g.dart delete mode 100644 lib/src/database/daos/media_uploads_dao.dart delete mode 100644 lib/src/database/daos/media_uploads_dao.g.dart delete mode 100644 lib/src/database/daos/message_retransmissions.dao.dart delete mode 100644 lib/src/database/daos/message_retransmissions.dao.g.dart create mode 100644 lib/src/database/daos/messages.dao.dart create mode 100644 lib/src/database/daos/messages.dao.g.dart delete mode 100644 lib/src/database/daos/messages_dao.dart delete mode 100644 lib/src/database/daos/messages_dao.g.dart create mode 100644 lib/src/database/daos/reactions.dao.dart create mode 100644 lib/src/database/daos/reactions.dao.g.dart create mode 100644 lib/src/database/daos/receipts.dao.dart create mode 100644 lib/src/database/daos/receipts.dao.g.dart rename lib/src/database/daos/{signal_dao.dart => signal.dao.dart} (95%) rename lib/src/database/daos/{signal_dao.g.dart => signal.dao.g.dart} (67%) create mode 100644 lib/src/database/tables/contacts.table.dart create mode 100644 lib/src/database/tables/groups.table.dart create mode 100644 lib/src/database/tables/mediafiles.table.dart create mode 100644 lib/src/database/tables/messages.table.dart create mode 100644 lib/src/database/tables/reactions.table.dart create mode 100644 lib/src/database/tables/receipts.table.dart create mode 100644 lib/src/database/tables/signal_contact_prekey.table.dart create mode 100644 lib/src/database/tables/signal_contact_signed_prekey.table.dart rename lib/src/database/tables/{signal_identity_key_store_table.dart => signal_identity_key_store.table.dart} (100%) rename lib/src/database/tables/{signal_pre_key_store_table.dart => signal_pre_key_store.table.dart} (100%) rename lib/src/database/tables/{signal_sender_key_store_table.dart => signal_sender_key_store.table.dart} (100%) rename lib/src/database/tables/{signal_session_store_table.dart => signal_session_store.table.dart} (100%) rename lib/src/database/{tables => tables_old}/contacts_table.dart (100%) rename lib/src/database/{tables => tables_old}/media_uploads_table.dart (100%) rename lib/src/database/{tables => tables_old}/message_retransmissions.dart (85%) rename lib/src/database/{tables => tables_old}/messages_table.dart (96%) rename lib/src/database/{tables => tables_old}/signal_contact_prekey_table.dart (100%) rename lib/src/database/{tables => tables_old}/signal_contact_signed_prekey_table.dart (100%) create mode 100644 lib/src/database/tables_old/signal_identity_key_store_table.dart create mode 100644 lib/src/database/tables_old/signal_pre_key_store_table.dart create mode 100644 lib/src/database/tables_old/signal_sender_key_store_table.dart create mode 100644 lib/src/database/tables_old/signal_session_store_table.dart create mode 100644 lib/src/database/twonly.db.dart create mode 100644 lib/src/database/twonly.db.g.dart rename lib/src/database/{twonly_database.dart => twonly_database_old.dart} (80%) rename lib/src/database/{twonly_database.g.dart => twonly_database_old.g.dart} (98%) rename lib/src/database/{twonly_database.steps.dart => twonly_database_old.steps.dart} (100%) rename lib/src/model/json/{message.dart => message_old.dart} (99%) rename lib/src/model/protobuf/{backup => client}/backup.proto (100%) rename lib/src/model/protobuf/{backup => client/generated}/backup.pb.dart (100%) rename lib/src/model/protobuf/{backup => client/generated}/backup.pbenum.dart (100%) rename lib/src/model/protobuf/{backup => client/generated}/backup.pbjson.dart (100%) rename lib/src/model/protobuf/{backup => client/generated}/backup.pbserver.dart (100%) create mode 100644 lib/src/model/protobuf/client/generated/messages.pb.dart create mode 100644 lib/src/model/protobuf/client/generated/messages.pbenum.dart create mode 100644 lib/src/model/protobuf/client/generated/messages.pbjson.dart create mode 100644 lib/src/model/protobuf/client/generated/messages.pbserver.dart rename lib/src/model/protobuf/{push_notification => client/generated}/push_notification.pb.dart (100%) rename lib/src/model/protobuf/{push_notification => client/generated}/push_notification.pbenum.dart (100%) rename lib/src/model/protobuf/{push_notification => client/generated}/push_notification.pbjson.dart (100%) rename lib/src/model/protobuf/{push_notification => client/generated}/push_notification.pbserver.dart (100%) create mode 100644 lib/src/model/protobuf/client/messages.proto rename lib/src/model/protobuf/{push_notification => client}/push_notification.proto (100%) create mode 100644 lib/src/services/api/server_messages/contact.server_messages.dart create mode 100644 lib/src/services/api/server_messages/media.server_messages.dart create mode 100644 lib/src/services/api/server_messages/messages.server_messages.dart create mode 100644 lib/src/services/api/server_messages/prekeys.server_messages.dart create mode 100644 lib/src/services/api/server_messages/pushkeys.server_messages.dart create mode 100644 lib/src/services/api/server_messages/reaction.server_message.dart create mode 100644 lib/src/services/api/server_messages/text_message.server_messages.dart create mode 100644 lib/src/services/mediafile.service.dart diff --git a/.gitmodules b/.gitmodules index a3642d1..41a0a1b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "dependencies/flutter_secure_storage"] - path = dependencies/flutter_secure_storage - url = https://github.com/juliansteenbakker/flutter_secure_storage [submodule "dependencies/flutter_zxing"] path = dependencies/flutter_zxing url = https://github.com/khoren93/flutter_zxing.git -[submodule "dependencies/flutter-pie-menu"] - path = dependencies/flutter-pie-menu - url = https://github.com/otsmr/flutter-pie-menu.git diff --git a/build.yaml b/build.yaml index 71f1ccd..01b6605 100644 --- a/build.yaml +++ b/build.yaml @@ -10,4 +10,5 @@ targets: drift_dev: options: databases: - twonly_database: lib/src/database/twonly_database.dart \ No newline at end of file + twonly_db: lib/src/database/twonly.db.dart + twonly_database: lib/src/database/twonly_database_old.dart \ No newline at end of file diff --git a/dependencies/flutter-pie-menu b/dependencies/flutter-pie-menu deleted file mode 160000 index 22df3f2..0000000 --- a/dependencies/flutter-pie-menu +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 22df3f2ab9ad71db60526668578a5309b3cc84ef diff --git a/dependencies/flutter_secure_storage b/dependencies/flutter_secure_storage deleted file mode 160000 index 71b75a3..0000000 --- a/dependencies/flutter_secure_storage +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71b75a36f35f2ce945998e20c6c6aa1820babfc6 diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index be89a9d..8bdc778 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -103,7 +103,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { } -struct EncryptedPushNotification: @unchecked Sendable { +struct EncryptedPushNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -195,7 +195,7 @@ struct PushUser: Sendable { fileprivate var _lastMessageID: Int64? = nil } -struct PushKey: @unchecked Sendable { +struct PushKey: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -214,32 +214,12 @@ struct PushKey: @unchecked Sendable { // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PushKind: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "reaction"), - 1: .same(proto: "response"), - 2: .same(proto: "text"), - 3: .same(proto: "video"), - 4: .same(proto: "twonly"), - 5: .same(proto: "image"), - 6: .same(proto: "contactRequest"), - 7: .same(proto: "acceptRequest"), - 8: .same(proto: "storedMediaFile"), - 9: .same(proto: "testNotification"), - 10: .same(proto: "reopenedMedia"), - 11: .same(proto: "reactionToVideo"), - 12: .same(proto: "reactionToText"), - 13: .same(proto: "reactionToImage"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0") } extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "EncryptedPushNotification" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "keyId"), - 2: .same(proto: "nonce"), - 3: .same(proto: "ciphertext"), - 4: .same(proto: "mac"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}keyId\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -284,11 +264,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushNotification" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "kind"), - 2: .same(proto: "messageId"), - 3: .same(proto: "reactionContent"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}reactionContent\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -332,9 +308,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUsers" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "users"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}users\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -364,13 +338,7 @@ extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUser" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "userId"), - 2: .same(proto: "displayName"), - 3: .same(proto: "blocked"), - 4: .same(proto: "lastMessageId"), - 5: .same(proto: "pushKeys"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}userId\0\u{1}displayName\0\u{1}blocked\0\u{1}lastMessageId\0\u{1}pushKeys\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -424,11 +392,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushKey" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .same(proto: "key"), - 3: .same(proto: "createdAtUnixTimestamp"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{1}createdAtUnixTimestamp\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/lib/globals.dart b/lib/globals.dart index b5c0a9c..809be94 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,11 +1,11 @@ import 'package:camera/camera.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api.service.dart'; late ApiService apiService; // uses for background notification -late TwonlyDatabase twonlyDB; +late TwonlyDB twonlyDB; List gCameras = []; diff --git a/lib/main.dart b/lib/main.dart index f2fb4a4..1d1ba15 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; @@ -43,18 +43,18 @@ void main() async { gCameras = await availableCameras(); apiService = ApiService(); - twonlyDB = TwonlyDatabase(); + twonlyDB = TwonlyDB(); - await twonlyDB.messagesDao.resetPendingDownloadState(); - await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); - await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); - await twonlyDB.signalDao.purgeOutDatedPreKeys(); + // await twonlyDB.messagesDao.resetPendingDownloadState(); + // await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); + // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); + // await twonlyDB.signalDao.purgeOutDatedPreKeys(); // Purge media files in the background - unawaited(purgeReceivedMediaFiles()); - unawaited(purgeSendMediaFiles()); + // unawaited(purgeReceivedMediaFiles()); + // unawaited(purgeSendMediaFiles()); - unawaited(performTwonlySafeBackup()); + // unawaited(performTwonlySafeBackup()); await initFileDownloader(); diff --git a/lib/src/database/daos/contacts_dao.dart b/lib/src/database/daos/contacts.dao.dart similarity index 82% rename from lib/src/database/daos/contacts_dao.dart rename to lib/src/database/daos/contacts.dao.dart index f97918f..9050ee0 100644 --- a/lib/src/database/daos/contacts_dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -1,13 +1,12 @@ import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -part 'contacts_dao.g.dart'; +part 'contacts.dao.g.dart'; @DriftAccessor(tables: [Contacts]) -class ContactsDao extends DatabaseAccessor - with _$ContactsDaoMixin { +class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. // ignore: matching_super_parameters @@ -135,42 +134,39 @@ class ContactsDao extends DatabaseAccessor .watchSingleOrNull(); } - Stream> watchContactsForShareView() { - return (select(contacts) - ..where( - (t) => - t.accepted.equals(true) & - t.blocked.equals(false) & - t.deleted.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } + // Stream> watchContactsForShareView() { + // return (select(contacts) + // ..where( + // (t) => + // t.accepted.equals(true) & + // t.blocked.equals(false) & + // t.deleted.equals(false), + // ) + // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + // .watch(); + // } - Stream> watchContactsForStartNewChat() { - return (select(contacts) - ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } + // Stream> watchContactsForStartNewChat() { + // return (select(contacts) + // ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) + // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + // .watch(); + // } - Stream> watchContactsForChatList() { - return (select(contacts) - ..where( - (t) => - t.accepted.equals(true) & - t.blocked.equals(false) & - t.archived.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } + // Stream> watchContactsForChatList() { + // return (select(contacts) + // ..where( + // (t) => + // t.accepted.equals(true) & + // t.blocked.equals(false) & + // t.archived.equals(false), + // ) + // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + // .watch(); + // } Future> getAllNotBlockedContacts() { - return (select(contacts) - ..where((t) => t.blocked.equals(false)) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .get(); + return (select(contacts)..where((t) => t.blocked.equals(false))).get(); } Stream watchContactsBlocked() { diff --git a/lib/src/database/daos/contacts_dao.g.dart b/lib/src/database/daos/contacts.dao.g.dart similarity index 59% rename from lib/src/database/daos/contacts_dao.g.dart rename to lib/src/database/daos/contacts.dao.g.dart index 7f5fb6b..626cccb 100644 --- a/lib/src/database/daos/contacts_dao.g.dart +++ b/lib/src/database/daos/contacts.dao.g.dart @@ -1,8 +1,8 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'contacts_dao.dart'; +part of 'contacts.dao.dart'; // ignore_for_file: type=lint -mixin _$ContactsDaoMixin on DatabaseAccessor { +mixin _$ContactsDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart new file mode 100644 index 0000000..6c9536a --- /dev/null +++ b/lib/src/database/daos/groups.dao.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +part 'groups.dao.g.dart'; + +@DriftAccessor(tables: [Groups, GroupMembers]) +class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + GroupsDao(super.db); + + Future isContactInGroup(int contactId, String groupId) async { + final entry = await (select(groupMembers) + ..where( + (t) => t.contactId.equals(contactId) & t.groupId.equals(groupId))) + .getSingleOrNull(); + return entry != null; + } +} diff --git a/lib/src/database/daos/groups.dao.g.dart b/lib/src/database/daos/groups.dao.g.dart new file mode 100644 index 0000000..3489f8c --- /dev/null +++ b/lib/src/database/daos/groups.dao.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'groups.dao.dart'; + +// ignore_for_file: type=lint +mixin _$GroupsDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; + $ContactsTable get contacts => attachedDatabase.contacts; + $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; +} diff --git a/lib/src/database/daos/media_uploads_dao.dart b/lib/src/database/daos/media_uploads_dao.dart deleted file mode 100644 index 97135b3..0000000 --- a/lib/src/database/daos/media_uploads_dao.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'media_uploads_dao.g.dart'; - -@DriftAccessor(tables: [MediaUploads]) -class MediaUploadsDao extends DatabaseAccessor - with _$MediaUploadsDaoMixin { - // ignore: matching_super_parameters - MediaUploadsDao(super.db); - - Future> getMediaUploadsForRetry() { - return (select(mediaUploads) - ..where( - (t) => t.state.equals(UploadState.receiverNotified.name).not(), - )) - .get(); - } - - Future updateMediaUpload( - int mediaUploadId, - MediaUploadsCompanion updatedValues, - ) { - return (update(mediaUploads) - ..where((c) => c.mediaUploadId.equals(mediaUploadId))) - .write(updatedValues); - } - - Future insertMediaUpload(MediaUploadsCompanion values) async { - try { - return await into(mediaUploads).insert(values); - } catch (e) { - Log.error('Error while inserting media upload: $e'); - return null; - } - } - - Future deleteMediaUpload(int mediaUploadId) { - return (delete(mediaUploads) - ..where((t) => t.mediaUploadId.equals(mediaUploadId))) - .go(); - } - - SingleOrNullSelectable getMediaUploadById(int mediaUploadId) { - return select(mediaUploads) - ..where((t) => t.mediaUploadId.equals(mediaUploadId)); - } -} diff --git a/lib/src/database/daos/media_uploads_dao.g.dart b/lib/src/database/daos/media_uploads_dao.g.dart deleted file mode 100644 index c2aa995..0000000 --- a/lib/src/database/daos/media_uploads_dao.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'media_uploads_dao.dart'; - -// ignore_for_file: type=lint -mixin _$MediaUploadsDaoMixin on DatabaseAccessor { - $MediaUploadsTable get mediaUploads => attachedDatabase.mediaUploads; -} diff --git a/lib/src/database/daos/message_retransmissions.dao.dart b/lib/src/database/daos/message_retransmissions.dao.dart deleted file mode 100644 index b4a7934..0000000 --- a/lib/src/database/daos/message_retransmissions.dao.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/message_retransmissions.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'message_retransmissions.dao.g.dart'; - -@DriftAccessor(tables: [MessageRetransmissions]) -class MessageRetransmissionDao extends DatabaseAccessor - with _$MessageRetransmissionDaoMixin { - // this constructor is required so that the main database can create an instance - // of this object. - // ignore: matching_super_parameters - MessageRetransmissionDao(super.db); - - Future insertRetransmission( - MessageRetransmissionsCompanion message, - ) async { - try { - return await into(messageRetransmissions).insert(message); - } catch (e) { - Log.error('Error while inserting message for retransmission: $e'); - return null; - } - } - - Future purgeOldRetransmissions() async { - // delete entries older than two weeks - await (delete(messageRetransmissions) - ..where( - (t) => (t.acknowledgeByServerAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 25), - ), - )), - )) - .go(); - } - - Future> getRetransmitAbleMessages() async { - final countDeleted = await (delete(messageRetransmissions) - ..where( - (t) => - t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull(), - )) - .go(); - - if (countDeleted > 0) { - Log.info('Deleted $countDeleted faulty retransmissions'); - } - - return (await (select(messageRetransmissions) - ..where((t) => t.acknowledgeByServerAt.isNull())) - .get()) - .map((msg) => msg.retransmissionId) - .toList(); - } - - SingleOrNullSelectable getRetransmissionById( - int retransmissionId, - ) { - return select(messageRetransmissions) - ..where((t) => t.retransmissionId.equals(retransmissionId)); - } - - Stream> watchAllMessages() { - return (select(messageRetransmissions) - ..orderBy([(t) => OrderingTerm.asc(t.retransmissionId)])) - .watch(); - } - - Future updateRetransmission( - int retransmissionId, - MessageRetransmissionsCompanion updatedValues, - ) { - return (update(messageRetransmissions) - ..where((c) => c.retransmissionId.equals(retransmissionId))) - .write(updatedValues); - } - - Future resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async { - return ((update(messageRetransmissions)) - ..where( - (m) => - m.contactId.equals(fromUserId) & - m.encryptedHash.equals(encryptedHash), - )) - .write( - const MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(null), - ), - ); - } - - Future getRetransmissionFromHash( - int fromUserId, - Uint8List encryptedHash, - ) async { - return ((select(messageRetransmissions)) - ..where( - (m) => - m.contactId.equals(fromUserId) & - m.encryptedHash.equals(encryptedHash), - )) - .getSingleOrNull(); - } - - Future deleteRetransmissionById(int retransmissionId) { - return (delete(messageRetransmissions) - ..where((t) => t.retransmissionId.equals(retransmissionId))) - .go(); - } - - Future clearRetransmissionTable() { - return delete(messageRetransmissions).go(); - } - - Future deleteRetransmissionByMessageId(int messageId) { - return (delete(messageRetransmissions) - ..where((t) => t.messageId.equals(messageId))) - .go(); - } -} diff --git a/lib/src/database/daos/message_retransmissions.dao.g.dart b/lib/src/database/daos/message_retransmissions.dao.g.dart deleted file mode 100644 index cd7ca29..0000000 --- a/lib/src/database/daos/message_retransmissions.dao.g.dart +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'message_retransmissions.dao.dart'; - -// ignore_for_file: type=lint -mixin _$MessageRetransmissionDaoMixin on DatabaseAccessor { - $ContactsTable get contacts => attachedDatabase.contacts; - $MessagesTable get messages => attachedDatabase.messages; - $MessageRetransmissionsTable get messageRetransmissions => - attachedDatabase.messageRetransmissions; -} diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart new file mode 100644 index 0000000..1244086 --- /dev/null +++ b/lib/src/database/daos/messages.dao.dart @@ -0,0 +1,377 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +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/services/mediafile.service.dart'; + +part 'messages.dao.g.dart'; + +@DriftAccessor(tables: [Messages, Contacts, MediaFiles, MessageHistories]) +class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + MessagesDao(super.db); + + // Stream> watchMessageNotOpened(int contactId) { + // return (select(messages) + // ..where( + // (t) => + // t.openedAt.isNull() & + // t.contactId.equals(contactId) & + // t.errorWhileSending.equals(false), + // ) + // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) + // .watch(); + // } + + // Stream> watchMediaMessageNotOpened(int contactId) { + // return (select(messages) + // ..where( + // (t) => + // t.openedAt.isNull() & + // t.contactId.equals(contactId) & + // t.errorWhileSending.equals(false) & + // t.messageOtherId.isNotNull() & + // t.kind.equals(MessageKind.media.name), + // ) + // ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) + // .watch(); + // } + + // Stream> watchLastMessage(int contactId) { + // return (select(messages) + // ..where((t) => t.contactId.equals(contactId)) + // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) + // ..limit(1)) + // .watch(); + // } + + // Stream> watchAllMessagesFrom(int contactId) { + // return (select(messages) + // ..where( + // (t) => + // t.contactId.equals(contactId) & + // t.contentJson.isNotNull() & + // (t.openedAt.isNull() | + // t.mediaStored.equals(true) | + // t.openedAt.isBiggerThanValue( + // DateTime.now().subtract(const Duration(days: 1)), + // )), + // ) + // ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) + // .watch(); + // } + + // Future removeOldMessages() { + // return (update(messages) + // ..where( + // (t) => + // (t.openedAt.isSmallerThanValue( + // DateTime.now().subtract(const Duration(days: 1)), + // ) | + // (t.sendAt.isSmallerThanValue( + // DateTime.now().subtract(const Duration(days: 3)), + // ) & + // t.errorWhileSending.equals(true))) & + // t.kind.equals(MessageKind.textMessage.name), + // )) + // .write(const MessagesCompanion(contentJson: Value(null))); + // } + + // Future handleMediaFilesOlderThan30Days() { + // /// media files will be deleted by the server after 30 days, so delete them here also + // return (update(messages) + // ..where( + // (t) => (t.kind.equals(MessageKind.media.name) & + // t.openedAt.isNull() & + // t.messageOtherId.isNull() & + // (t.sendAt.isSmallerThanValue( + // DateTime.now().subtract( + // const Duration(days: 30), + // ), + // ))), + // )) + // .write(const MessagesCompanion(errorWhileSending: Value(true))); + // } + + // Future> getAllMessagesPendingDownloading() { + // return (select(messages) + // ..where( + // (t) => + // t.downloadState.equals(DownloadState.downloaded.index).not() & + // t.messageOtherId.isNotNull() & + // t.errorWhileSending.equals(false) & + // t.kind.equals(MessageKind.media.name), + // )) + // .get(); + // } + + // Future> getAllNonACKMessagesFromUser() { + // return (select(messages) + // ..where( + // (t) => + // t.acknowledgeByUser.equals(false) & + // t.messageOtherId.isNull() & + // t.errorWhileSending.equals(false) & + // t.sendAt.isBiggerThanValue( + // DateTime.now().subtract(const Duration(minutes: 10)), + // ), + // )) + // .get(); + // } + + // Stream> getAllStoredMediaFiles() { + // return (select(messages) + // ..where((t) => t.mediaStored.equals(true)) + // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) + // .watch(); + // } + + // Future> getAllMessagesPendingUpload() { + // return (select(messages) + // ..where( + // (t) => + // t.acknowledgeByServer.equals(false) & + // t.messageOtherId.isNull() & + // t.mediaUploadId.isNotNull() & + // t.downloadState.equals(DownloadState.pending.index) & + // t.errorWhileSending.equals(false) & + // t.kind.equals(MessageKind.media.name), + // )) + // .get(); + // } + + // Future openedAllNonMediaMessages(int contactId) { + // final updates = MessagesCompanion(openedAt: Value(DateTime.now())); + // return (update(messages) + // ..where( + // (t) => + // t.contactId.equals(contactId) & + // t.messageOtherId.isNotNull() & + // t.openedAt.isNull() & + // t.kind.equals(MessageKind.media.name).not(), + // )) + // .write(updates); + // } + + // Future resetPendingDownloadState() { + // // All media files in the downloading state are reset to the pending state + // // When the app is used in mobile network, they will not be downloaded at the start + // // if they are not yet downloaded... + // const updates = + // MessagesCompanion(downloadState: Value(DownloadState.pending)); + // return (update(messages) + // ..where( + // (t) => + // t.messageOtherId.isNotNull() & + // t.downloadState.equals(DownloadState.downloading.index) & + // t.kind.equals(MessageKind.media.name), + // )) + // .write(updates); + // } + + Future handleMessageDeletion( + int contactId, + String messageId, + DateTime timestamp, + ) async { + final msg = await getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.senderId != contactId) return; + if (msg.mediaId != null) { + await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) + .go(); + await removeMediaFile(msg.mediaId!); + } + await (delete(messageHistories) + ..where((t) => t.messageId.equals(messageId))) + .go(); + + await (update(messages) + ..where( + (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), + )) + .write( + MessagesCompanion( + isDeletedFromSender: const Value(true), + content: const Value(null), + modifiedAt: Value(timestamp), + mediaId: const Value(null), + ), + ); + } + + Future handleTextEdit( + int contactId, + String messageId, + String text, + DateTime timestamp, + ) async { + final msg = await getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.content == null || msg.senderId == contactId) { + return; + } + await into(messageHistories).insert( + MessageHistoriesCompanion( + messageId: Value(messageId), + content: Value(msg.content), + ), + ); + await (update(messages) + ..where( + (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), + )) + .write( + MessagesCompanion( + content: Value(text), + modifiedAt: Value(timestamp), + ), + ); + } + + Future handleMessageOpened( + String groupId, + String messageId, + DateTime timestamp, + ) async { + final msg = await getMessageById(messageId).getSingleOrNull(); + if (msg == null) return; + await (update(messages) + ..where( + (t) => + t.groupId.equals(groupId) & + t.messageId.equals(messageId) & + t.senderId.isNull(), + )) + .write( + MessagesCompanion( + openedAt: Value(timestamp), + openedByCounter: Value(msg.openedByCounter + 1), + ), + ); + } + + // Future updateMessageByOtherUser( + // int userId, + // int messageId, + // MessagesCompanion updatedValues, + // ) { + // return (update(messages) + // ..where( + // (c) => c.contactId.equals(userId) & c.messageId.equals(messageId), + // )) + // .write(updatedValues); + // } + + // Future updateMessageByOtherMessageId( + // int userId, + // int messageOtherId, + // MessagesCompanion updatedValues, + // ) { + // return (update(messages) + // ..where( + // (c) => + // c.contactId.equals(userId) & + // c.messageOtherId.equals(messageOtherId), + // )) + // .write(updatedValues); + // } + + // Future updateMessageByMessageId( + // int messageId, + // MessagesCompanion updatedValues, + // ) { + // return (update(messages)..where((c) => c.messageId.equals(messageId))) + // .write(updatedValues); + // } + + // Future insertMessage(MessagesCompanion message) async { + // try { + // await (update(contacts) + // ..where( + // (c) => c.userId.equals(message.contactId.value), + // )) + // .write(ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); + + // return await into(messages).insert(message); + // } catch (e) { + // Log.error('Error while inserting message: $e'); + // return null; + // } + // } + + // Future deleteMessagesByContactId(int contactId) { + // return (delete(messages) + // ..where( + // (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false), + // )) + // .go(); + // } + + // Future deleteMessagesByContactIdAndOtherMessageId( + // int contactId, + // int messageOtherId, + // ) { + // return (delete(messages) + // ..where( + // (t) => + // t.contactId.equals(contactId) & + // t.messageOtherId.equals(messageOtherId), + // )) + // .go(); + // } + + // Future deleteMessagesByMessageId(int messageId) { + // return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); + // } + + // Future deleteAllMessagesByContactId(int contactId) { + // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); + // } + + // Future containsOtherMessageId( + // int fromUserId, + // int messageOtherId, + // ) async { + // final query = select(messages) + // ..where( + // (t) => + // t.messageOtherId.equals(messageOtherId) & + // t.contactId.equals(fromUserId), + // ); + // final entry = await query.get(); + // return entry.isNotEmpty; + // } + + SingleOrNullSelectable getMessageById(String messageId) { + return select(messages)..where((t) => t.messageId.equals(messageId)); + } + + // Future> getMessagesByMediaUploadId(int mediaUploadId) async { + // return (select(messages) + // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) + // .get(); + // } + + // SingleOrNullSelectable getMessageByOtherMessageId( + // int fromUserId, + // int messageId, + // ) { + // return select(messages) + // ..where( + // (t) => + // t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId), + // ); + // } + + // SingleOrNullSelectable getMessageByIdAndContactId( + // int fromUserId, + // int messageId, + // ) { + // return select(messages) + // ..where( + // (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId), + // ); + // } +} diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart new file mode 100644 index 0000000..bad5c9e --- /dev/null +++ b/lib/src/database/daos/messages.dao.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'messages.dao.dart'; + +// ignore_for_file: type=lint +mixin _$MessagesDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $MessageHistoriesTable get messageHistories => + attachedDatabase.messageHistories; +} diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart deleted file mode 100644 index 7bb05e3..0000000 --- a/lib/src/database/daos/messages_dao.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'messages_dao.g.dart'; - -@DriftAccessor(tables: [Messages, Contacts]) -class MessagesDao extends DatabaseAccessor - with _$MessagesDaoMixin { - // this constructor is required so that the main database can create an instance - // of this object. - // ignore: matching_super_parameters - MessagesDao(super.db); - - Stream> watchMessageNotOpened(int contactId) { - return (select(messages) - ..where( - (t) => - t.openedAt.isNull() & - t.contactId.equals(contactId) & - t.errorWhileSending.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - .watch(); - } - - Stream> watchMediaMessageNotOpened(int contactId) { - return (select(messages) - ..where( - (t) => - t.openedAt.isNull() & - t.contactId.equals(contactId) & - t.errorWhileSending.equals(false) & - t.messageOtherId.isNotNull() & - t.kind.equals(MessageKind.media.name), - ) - ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - .watch(); - } - - Stream> watchLastMessage(int contactId) { - return (select(messages) - ..where((t) => t.contactId.equals(contactId)) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) - ..limit(1)) - .watch(); - } - - Stream> watchAllMessagesFrom(int contactId) { - return (select(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.contentJson.isNotNull() & - (t.openedAt.isNull() | - t.mediaStored.equals(true) | - t.openedAt.isBiggerThanValue( - DateTime.now().subtract(const Duration(days: 1)), - )), - ) - ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - .watch(); - } - - Future removeOldMessages() { - return (update(messages) - ..where( - (t) => - (t.openedAt.isSmallerThanValue( - DateTime.now().subtract(const Duration(days: 1)), - ) | - (t.sendAt.isSmallerThanValue( - DateTime.now().subtract(const Duration(days: 3)), - ) & - t.errorWhileSending.equals(true))) & - t.kind.equals(MessageKind.textMessage.name), - )) - .write(const MessagesCompanion(contentJson: Value(null))); - } - - Future handleMediaFilesOlderThan30Days() { - /// media files will be deleted by the server after 30 days, so delete them here also - return (update(messages) - ..where( - (t) => (t.kind.equals(MessageKind.media.name) & - t.openedAt.isNull() & - t.messageOtherId.isNull() & - (t.sendAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 30), - ), - ))), - )) - .write(const MessagesCompanion(errorWhileSending: Value(true))); - } - - Future> getAllMessagesPendingDownloading() { - return (select(messages) - ..where( - (t) => - t.downloadState.equals(DownloadState.downloaded.index).not() & - t.messageOtherId.isNotNull() & - t.errorWhileSending.equals(false) & - t.kind.equals(MessageKind.media.name), - )) - .get(); - } - - Future> getAllNonACKMessagesFromUser() { - return (select(messages) - ..where( - (t) => - t.acknowledgeByUser.equals(false) & - t.messageOtherId.isNull() & - t.errorWhileSending.equals(false) & - t.sendAt.isBiggerThanValue( - DateTime.now().subtract(const Duration(minutes: 10)), - ), - )) - .get(); - } - - Stream> getAllStoredMediaFiles() { - return (select(messages) - ..where((t) => t.mediaStored.equals(true)) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - .watch(); - } - - Future> getAllMessagesPendingUpload() { - return (select(messages) - ..where( - (t) => - t.acknowledgeByServer.equals(false) & - t.messageOtherId.isNull() & - t.mediaUploadId.isNotNull() & - t.downloadState.equals(DownloadState.pending.index) & - t.errorWhileSending.equals(false) & - t.kind.equals(MessageKind.media.name), - )) - .get(); - } - - Future openedAllNonMediaMessages(int contactId) { - final updates = MessagesCompanion(openedAt: Value(DateTime.now())); - return (update(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId.isNotNull() & - t.openedAt.isNull() & - t.kind.equals(MessageKind.media.name).not(), - )) - .write(updates); - } - - Future resetPendingDownloadState() { - // All media files in the downloading state are reset to the pending state - // When the app is used in mobile network, they will not be downloaded at the start - // if they are not yet downloaded... - const updates = - MessagesCompanion(downloadState: Value(DownloadState.pending)); - return (update(messages) - ..where( - (t) => - t.messageOtherId.isNotNull() & - t.downloadState.equals(DownloadState.downloading.index) & - t.kind.equals(MessageKind.media.name), - )) - .write(updates); - } - - Future openedAllNonMediaMessagesFromOtherUser(int contactId) { - final updates = MessagesCompanion(openedAt: Value(DateTime.now())); - return (update(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId - .isNull() & // only mark messages open that where send - t.openedAt.isNull() & - t.kind.equals(MessageKind.media.name).not(), - )) - .write(updates); - } - - Future updateMessageByOtherUser( - int userId, - int messageId, - MessagesCompanion updatedValues, - ) { - return (update(messages) - ..where( - (c) => c.contactId.equals(userId) & c.messageId.equals(messageId), - )) - .write(updatedValues); - } - - Future updateMessageByOtherMessageId( - int userId, - int messageOtherId, - MessagesCompanion updatedValues, - ) { - return (update(messages) - ..where( - (c) => - c.contactId.equals(userId) & - c.messageOtherId.equals(messageOtherId), - )) - .write(updatedValues); - } - - Future updateMessageByMessageId( - int messageId, - MessagesCompanion updatedValues, - ) { - return (update(messages)..where((c) => c.messageId.equals(messageId))) - .write(updatedValues); - } - - Future insertMessage(MessagesCompanion message) async { - try { - await (update(contacts) - ..where( - (c) => c.userId.equals(message.contactId.value), - )) - .write(ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); - - return await into(messages).insert(message); - } catch (e) { - Log.error('Error while inserting message: $e'); - return null; - } - } - - Future deleteMessagesByContactId(int contactId) { - return (delete(messages) - ..where( - (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false), - )) - .go(); - } - - Future deleteMessagesByContactIdAndOtherMessageId( - int contactId, - int messageOtherId, - ) { - return (delete(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId.equals(messageOtherId), - )) - .go(); - } - - Future deleteMessagesByMessageId(int messageId) { - return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); - } - - Future deleteAllMessagesByContactId(int contactId) { - return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); - } - - Future containsOtherMessageId( - int fromUserId, - int messageOtherId, - ) async { - final query = select(messages) - ..where( - (t) => - t.messageOtherId.equals(messageOtherId) & - t.contactId.equals(fromUserId), - ); - final entry = await query.get(); - return entry.isNotEmpty; - } - - SingleOrNullSelectable getMessageByMessageId(int messageId) { - return select(messages)..where((t) => t.messageId.equals(messageId)); - } - - Future> getMessagesByMediaUploadId(int mediaUploadId) async { - return (select(messages) - ..where((t) => t.mediaUploadId.equals(mediaUploadId))) - .get(); - } - - SingleOrNullSelectable getMessageByOtherMessageId( - int fromUserId, - int messageId, - ) { - return select(messages) - ..where( - (t) => - t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId), - ); - } - - SingleOrNullSelectable getMessageByIdAndContactId( - int fromUserId, - int messageId, - ) { - return select(messages) - ..where( - (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId), - ); - } -} diff --git a/lib/src/database/daos/messages_dao.g.dart b/lib/src/database/daos/messages_dao.g.dart deleted file mode 100644 index 1967aec..0000000 --- a/lib/src/database/daos/messages_dao.g.dart +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'messages_dao.dart'; - -// ignore_for_file: type=lint -mixin _$MessagesDaoMixin on DatabaseAccessor { - $ContactsTable get contacts => attachedDatabase.contacts; - $MessagesTable get messages => attachedDatabase.messages; -} diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart new file mode 100644 index 0000000..65eb24f --- /dev/null +++ b/lib/src/database/daos/reactions.dao.dart @@ -0,0 +1,44 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/reactions.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'reactions.dao.g.dart'; + +@DriftAccessor(tables: [Reactions]) +class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + ReactionsDao(super.db); + + Future updateReaction( + int contactId, + String messageId, + String groupId, + String? emoji, + ) async { + final msg = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.groupId != groupId) return; + + try { + await (delete(reactions) + ..where( + (t) => + t.senderId.equals(contactId) & t.messageId.equals(messageId), + )) + .go(); + if (emoji != null) { + await into(reactions).insert(ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: Value(contactId), + )); + } + } catch (e) { + Log.error(e); + } + } +} diff --git a/lib/src/database/daos/reactions.dao.g.dart b/lib/src/database/daos/reactions.dao.g.dart new file mode 100644 index 0000000..2fcd9af --- /dev/null +++ b/lib/src/database/daos/reactions.dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reactions.dao.dart'; + +// ignore_for_file: type=lint +mixin _$ReactionsDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $ReactionsTable get reactions => attachedDatabase.reactions; +} diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart new file mode 100644 index 0000000..cf2193e --- /dev/null +++ b/lib/src/database/daos/receipts.dao.dart @@ -0,0 +1,52 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'receipts.dao.g.dart'; + +@DriftAccessor(tables: [Receipts, Messages]) +class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + ReceiptsDao(super.db); + + Future confirmReceipt(String receiptId, int fromUserId) async { + final receipt = await (select(receipts) + ..where((t) => + t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId))) + .getSingleOrNull(); + + if (receipt == null) return; + + if (receipt.messageId != null) { + await (update(messages) + ..where((t) => t.messageId.equals(receipt.messageId!))) + .write( + const MessagesCompanion( + acknowledgeByUser: Value(true), + ), + ); + } + + await (delete(receipts) + ..where( + (t) => + t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId), + )) + .go(); + } + + Future insertReceipt(ReceiptsCompanion entry) async { + try { + final id = await into(receipts).insert(entry); + return await (select(receipts)..where((t) => t.rowId.equals(id))) + .getSingle(); + } catch (e) { + Log.error(e); + return null; + } + } +} diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart new file mode 100644 index 0000000..d0b9b41 --- /dev/null +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'receipts.dao.dart'; + +// ignore_for_file: type=lint +mixin _$ReceiptsDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $ReceiptsTable get receipts => attachedDatabase.receipts; +} diff --git a/lib/src/database/daos/signal_dao.dart b/lib/src/database/daos/signal.dao.dart similarity index 95% rename from lib/src/database/daos/signal_dao.dart rename to lib/src/database/daos/signal.dao.dart index e8f4732..4458f50 100644 --- a/lib/src/database/daos/signal_dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -1,11 +1,11 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; -part 'signal_dao.g.dart'; +part 'signal.dao.g.dart'; @DriftAccessor( tables: [ @@ -13,7 +13,7 @@ part 'signal_dao.g.dart'; SignalContactSignedPreKeys, ], ) -class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { +class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { // this constructor is required so that the main database can create an instance // of this object. // ignore: matching_super_parameters diff --git a/lib/src/database/daos/signal_dao.g.dart b/lib/src/database/daos/signal.dao.g.dart similarity index 67% rename from lib/src/database/daos/signal_dao.g.dart rename to lib/src/database/daos/signal.dao.g.dart index a3b77d6..a9eea13 100644 --- a/lib/src/database/daos/signal_dao.g.dart +++ b/lib/src/database/daos/signal.dao.g.dart @@ -1,9 +1,10 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'signal_dao.dart'; +part of 'signal.dao.dart'; // ignore_for_file: type=lint -mixin _$SignalDaoMixin on DatabaseAccessor { +mixin _$SignalDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; $SignalContactPreKeysTable get signalContactPreKeys => attachedDatabase.signalContactPreKeys; $SignalContactSignedPreKeysTable get signalContactSignedPreKeys => diff --git a/lib/src/database/signal/connect_identity_key_store.dart b/lib/src/database/signal/connect_identity_key_store.dart index 47d4d88..2b36b13 100644 --- a/lib/src/database/signal/connect_identity_key_store.dart +++ b/lib/src/database/signal/connect_identity_key_store.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectIdentityKeyStore extends IdentityKeyStore { ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId); diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 6fb193b..1c3da47 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; class ConnectPreKeyStore extends PreKeyStore { diff --git a/lib/src/database/signal/connect_sender_key_store.dart b/lib/src/database/signal/connect_sender_key_store.dart index e9b0ee2..69b25c7 100644 --- a/lib/src/database/signal/connect_sender_key_store.dart +++ b/lib/src/database/signal/connect_sender_key_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectSenderKeyStore extends SenderKeyStore { @override diff --git a/lib/src/database/signal/connect_session_store.dart b/lib/src/database/signal/connect_session_store.dart index d851600..bb738c4 100644 --- a/lib/src/database/signal/connect_session_store.dart +++ b/lib/src/database/signal/connect_session_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectSessionStore extends SessionStore { @override diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart new file mode 100644 index 0000000..0f9e2b0 --- /dev/null +++ b/lib/src/database/tables/contacts.table.dart @@ -0,0 +1,41 @@ +import 'package:drift/drift.dart'; + +class Contacts extends Table { + IntColumn get userId => integer()(); + + TextColumn get username => text()(); + TextColumn get displayName => text().nullable()(); + TextColumn get nickName => text().nullable()(); + TextColumn get avatarSvg => text().nullable()(); + + IntColumn get senderProfileCounter => + integer().withDefault(const Constant(0))(); + + BoolColumn get accepted => boolean().withDefault(const Constant(false))(); + BoolColumn get requested => boolean().withDefault(const Constant(false))(); + BoolColumn get hidden => boolean().withDefault(const Constant(false))(); + BoolColumn get blocked => boolean().withDefault(const Constant(false))(); + BoolColumn get verified => boolean().withDefault(const Constant(false))(); + BoolColumn get archived => boolean().withDefault(const Constant(false))(); + BoolColumn get deleted => boolean().withDefault(const Constant(false))(); + + BoolColumn get alsoBestFriend => + boolean().withDefault(const Constant(false))(); + + IntColumn get deleteMessagesAfterXMinutes => + integer().withDefault(const Constant(60 * 24))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))(); + + DateTimeColumn get lastMessageSend => dateTime().nullable()(); + DateTimeColumn get lastMessageReceived => dateTime().nullable()(); + DateTimeColumn get lastFlameCounterChange => dateTime().nullable()(); + DateTimeColumn get lastFlameSync => dateTime().nullable()(); + + IntColumn get flameCounter => integer().withDefault(const Constant(0))(); + + @override + Set get primaryKey => {userId}; +} diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart new file mode 100644 index 0000000..08148ae --- /dev/null +++ b/lib/src/database/tables/groups.table.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +@DataClassName('Group') +class Groups extends Table { + TextColumn get groupId => text().clientDefault(() => uuid.v4())(); + + BoolColumn get isGroupAdmin => boolean()(); + BoolColumn get isGroupOfTwo => boolean()(); + BoolColumn get pinned => boolean().withDefault(const Constant(false))(); + + DateTimeColumn get lastMessageExchange => + dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupId}; +} + +enum MemberState { invited, accepted, admin } + +@DataClassName('GroupMember') +class GroupMembers extends Table { + TextColumn get groupId => text()(); + + IntColumn get contactId => integer().references(Contacts, #userId)(); + TextColumn get memberState => textEnum().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupId, contactId}; +} diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart new file mode 100644 index 0000000..24edcd4 --- /dev/null +++ b/lib/src/database/tables/mediafiles.table.dart @@ -0,0 +1,48 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; + +enum MediaType { + image, + video, + gif, +} + +enum UploadState { + pending, + readyToUpload, + uploadTaskStarted, + receiverNotified, +} + +enum DownloadState { + pending, +} + +@DataClassName('MediaFile') +class MediaFiles extends Table { + TextColumn get mediaId => text().clientDefault(() => uuid.v4())(); + + TextColumn get type => textEnum()(); + + TextColumn get uploadState => textEnum().nullable()(); + TextColumn get downloadState => textEnum().nullable()(); + + BoolColumn get requiresAuthentication => boolean()(); + BoolColumn get reopenByContact => + boolean().withDefault(const Constant(false))(); + + BoolColumn get storedByContact => + boolean().withDefault(const Constant(false))(); + + IntColumn get displayLimitInMilliseconds => integer().nullable()(); + + BlobColumn get downloadToken => blob().nullable()(); + BlobColumn get encryptionKey => blob().nullable()(); + BlobColumn get encryptionMac => blob().nullable()(); + BlobColumn get encryptionNonce => blob().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {mediaId}; +} diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart new file mode 100644 index 0000000..2e33471 --- /dev/null +++ b/lib/src/database/tables/messages.table.dart @@ -0,0 +1,52 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; + +@DataClassName('Message') +class Messages extends Table { + TextColumn get groupId => text()(); + TextColumn get messageId => text()(); + + // in case senderId is null, it was send by user itself + IntColumn get senderId => + integer().nullable().references(Contacts, #userId)(); + + TextColumn get content => text().nullable()(); + TextColumn get mediaId => + text().nullable().references(MediaFiles, #mediaId)(); + + TextColumn get quotesMessageId => + text().nullable().references(Messages, #messageId)(); + + BoolColumn get isDeletedFromSender => + boolean().withDefault(const Constant(false))(); + + BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); + + BoolColumn get acknowledgeByUser => + boolean().withDefault(const Constant(false))(); + BoolColumn get acknowledgeByServer => + boolean().withDefault(const Constant(false))(); + + IntColumn get openedByCounter => integer().withDefault(const Constant(0))(); + DateTimeColumn get openedAt => dateTime().nullable()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get modifiedAt => + dateTime().nullable().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId}; +} + +@DataClassName('MessageHistory') +class MessageHistories extends Table { + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + TextColumn get content => text().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId, createdAt}; +} diff --git a/lib/src/database/tables/reactions.table.dart b/lib/src/database/tables/reactions.table.dart new file mode 100644 index 0000000..1f29c06 --- /dev/null +++ b/lib/src/database/tables/reactions.table.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; + +@DataClassName('Reaction') +class Reactions extends Table { + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + TextColumn get emoji => text()(); + + // in case senderId is null, it was send by user itself + IntColumn get senderId => integer() + .nullable() + .references(Contacts, #userId, onDelete: KeyAction.cascade)(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId, senderId, createdAt}; +} diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart new file mode 100644 index 0000000..3ccf85e --- /dev/null +++ b/lib/src/database/tables/receipts.table.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; + +@DataClassName('Receipt') +class Receipts extends Table { + TextColumn get receiptId => text().clientDefault(() => uuid.v4())(); + + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + + // in case a message is deleted, it should be also deleted from the receipts table + TextColumn get messageId => text() + .nullable() + .references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + BlobColumn get message => blob()(); + + BoolColumn get contactWillSendsReceipt => + boolean().withDefault(const Constant(true))(); + + IntColumn get retryCount => integer().withDefault(const Constant(0))(); + DateTimeColumn get lastRetry => dateTime().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {receiptId}; +} diff --git a/lib/src/database/tables/signal_contact_prekey.table.dart b/lib/src/database/tables/signal_contact_prekey.table.dart new file mode 100644 index 0000000..d14f522 --- /dev/null +++ b/lib/src/database/tables/signal_contact_prekey.table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +@DataClassName('SignalContactPreKey') +class SignalContactPreKeys extends Table { + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + IntColumn get preKeyId => integer()(); + BlobColumn get preKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + @override + Set get primaryKey => {contactId, preKeyId}; +} diff --git a/lib/src/database/tables/signal_contact_signed_prekey.table.dart b/lib/src/database/tables/signal_contact_signed_prekey.table.dart new file mode 100644 index 0000000..5888abe --- /dev/null +++ b/lib/src/database/tables/signal_contact_signed_prekey.table.dart @@ -0,0 +1,14 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +@DataClassName('SignalContactSignedPreKey') +class SignalContactSignedPreKeys extends Table { + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + IntColumn get signedPreKeyId => integer()(); + BlobColumn get signedPreKey => blob()(); + BlobColumn get signedPreKeySignature => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + @override + Set get primaryKey => {contactId}; +} diff --git a/lib/src/database/tables/signal_identity_key_store_table.dart b/lib/src/database/tables/signal_identity_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_identity_key_store_table.dart rename to lib/src/database/tables/signal_identity_key_store.table.dart diff --git a/lib/src/database/tables/signal_pre_key_store_table.dart b/lib/src/database/tables/signal_pre_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_pre_key_store_table.dart rename to lib/src/database/tables/signal_pre_key_store.table.dart diff --git a/lib/src/database/tables/signal_sender_key_store_table.dart b/lib/src/database/tables/signal_sender_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_sender_key_store_table.dart rename to lib/src/database/tables/signal_sender_key_store.table.dart diff --git a/lib/src/database/tables/signal_session_store_table.dart b/lib/src/database/tables/signal_session_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_session_store_table.dart rename to lib/src/database/tables/signal_session_store.table.dart diff --git a/lib/src/database/tables/contacts_table.dart b/lib/src/database/tables_old/contacts_table.dart similarity index 100% rename from lib/src/database/tables/contacts_table.dart rename to lib/src/database/tables_old/contacts_table.dart diff --git a/lib/src/database/tables/media_uploads_table.dart b/lib/src/database/tables_old/media_uploads_table.dart similarity index 100% rename from lib/src/database/tables/media_uploads_table.dart rename to lib/src/database/tables_old/media_uploads_table.dart diff --git a/lib/src/database/tables/message_retransmissions.dart b/lib/src/database/tables_old/message_retransmissions.dart similarity index 85% rename from lib/src/database/tables/message_retransmissions.dart rename to lib/src/database/tables_old/message_retransmissions.dart index a746f98..113d775 100644 --- a/lib/src/database/tables/message_retransmissions.dart +++ b/lib/src/database/tables_old/message_retransmissions.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/messages_table.dart'; @DataClassName('MessageRetransmission') class MessageRetransmissions extends Table { diff --git a/lib/src/database/tables/messages_table.dart b/lib/src/database/tables_old/messages_table.dart similarity index 96% rename from lib/src/database/tables/messages_table.dart rename to lib/src/database/tables_old/messages_table.dart index 6a2af91..547857e 100644 --- a/lib/src/database/tables/messages_table.dart +++ b/lib/src/database/tables_old/messages_table.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; enum MessageKind { textMessage, diff --git a/lib/src/database/tables/signal_contact_prekey_table.dart b/lib/src/database/tables_old/signal_contact_prekey_table.dart similarity index 100% rename from lib/src/database/tables/signal_contact_prekey_table.dart rename to lib/src/database/tables_old/signal_contact_prekey_table.dart diff --git a/lib/src/database/tables/signal_contact_signed_prekey_table.dart b/lib/src/database/tables_old/signal_contact_signed_prekey_table.dart similarity index 100% rename from lib/src/database/tables/signal_contact_signed_prekey_table.dart rename to lib/src/database/tables_old/signal_contact_signed_prekey_table.dart diff --git a/lib/src/database/tables_old/signal_identity_key_store_table.dart b/lib/src/database/tables_old/signal_identity_key_store_table.dart new file mode 100644 index 0000000..1f7d380 --- /dev/null +++ b/lib/src/database/tables_old/signal_identity_key_store_table.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalIdentityKeyStore') +class SignalIdentityKeyStores extends Table { + IntColumn get deviceId => integer()(); + TextColumn get name => text()(); + BlobColumn get identityKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {deviceId, name}; +} diff --git a/lib/src/database/tables_old/signal_pre_key_store_table.dart b/lib/src/database/tables_old/signal_pre_key_store_table.dart new file mode 100644 index 0000000..eb74263 --- /dev/null +++ b/lib/src/database/tables_old/signal_pre_key_store_table.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalPreKeyStore') +class SignalPreKeyStores extends Table { + IntColumn get preKeyId => integer()(); + BlobColumn get preKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {preKeyId}; +} diff --git a/lib/src/database/tables_old/signal_sender_key_store_table.dart b/lib/src/database/tables_old/signal_sender_key_store_table.dart new file mode 100644 index 0000000..1c10183 --- /dev/null +++ b/lib/src/database/tables_old/signal_sender_key_store_table.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalSenderKeyStore') +class SignalSenderKeyStores extends Table { + TextColumn get senderKeyName => text()(); + BlobColumn get senderKey => blob()(); + + @override + Set get primaryKey => {senderKeyName}; +} diff --git a/lib/src/database/tables_old/signal_session_store_table.dart b/lib/src/database/tables_old/signal_session_store_table.dart new file mode 100644 index 0000000..e522700 --- /dev/null +++ b/lib/src/database/tables_old/signal_session_store_table.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalSessionStore') +class SignalSessionStores extends Table { + IntColumn get deviceId => integer()(); + TextColumn get name => text()(); + BlobColumn get sessionRecord => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {deviceId, name}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart new file mode 100644 index 0000000..d4f21f0 --- /dev/null +++ b/lib/src/database/twonly.db.dart @@ -0,0 +1,124 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart' + show DriftNativeOptions, driftDatabase; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/daos/messages.dao.dart'; +import 'package:twonly/src/database/daos/reactions.dao.dart'; +import 'package:twonly/src/database/daos/receipts.dao.dart'; +import 'package:twonly/src/database/daos/signal.dao.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/tables/reactions.table.dart'; +import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_session_store.table.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'twonly.db.g.dart'; + +// You can then create a database class that includes this table +@DriftDatabase( + tables: [ + Contacts, + Messages, + MessageHistories, + MediaFiles, + Reactions, + Groups, + GroupMembers, + Receipts, + SignalIdentityKeyStores, + SignalPreKeyStores, + SignalSenderKeyStores, + SignalSessionStores, + SignalContactPreKeys, + SignalContactSignedPreKeys, + ], + daos: [ + MessagesDao, + ContactsDao, + SignalDao, + ReceiptsDao, + GroupsDao, + ReactionsDao + ], +) +class TwonlyDB extends _$TwonlyDB { + TwonlyDB([QueryExecutor? e]) + : super( + e ?? _openConnection(), + ); + + // ignore: matching_super_parameters + TwonlyDB.forTesting(DatabaseConnection super.connection); + + @override + int get schemaVersion => 1; + + static QueryExecutor _openConnection() { + return driftDatabase( + name: 'twonly', + native: const DriftNativeOptions( + databaseDirectory: getApplicationSupportDirectory, + ), + ); + } + + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + // onUpgrade: stepByStep(), + ); + } + + void markUpdated() { + notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)}); + notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)}); + } + + Future printTableSizes() async { + final result = await customSelect( + 'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name', + ).get(); + + for (final row in result) { + final tableName = row.read('name'); + final tableSize = row.read('size'); + Log.info('Table: $tableName, Size: $tableSize bytes'); + } + } + + Future deleteDataForTwonlySafe() async { + // await delete(messages).go(); + // await delete(messageRetransmissions).go(); + // await delete(mediaUploads).go(); + // await update(contacts).write( + // const ContactsCompanion( + // avatarSvg: Value(null), + // myAvatarCounter: Value(0), + // ), + // ); + // await delete(signalContactPreKeys).go(); + // await delete(signalContactSignedPreKeys).go(); + // await (delete(signalPreKeyStores) + // ..where( + // (t) => (t.createdAt.isSmallerThanValue( + // DateTime.now().subtract( + // const Duration(days: 25), + // ), + // )), + // )) + // .go(); + } +} diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart new file mode 100644 index 0000000..8238265 --- /dev/null +++ b/lib/src/database/twonly.db.g.dart @@ -0,0 +1,10544 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'twonly.db.dart'; + +// ignore_for_file: type=lint +class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ContactsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _usernameMeta = + const VerificationMeta('username'); + @override + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _displayNameMeta = + const VerificationMeta('displayName'); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _nickNameMeta = + const VerificationMeta('nickName'); + @override + late final GeneratedColumn nickName = GeneratedColumn( + 'nick_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _avatarSvgMeta = + const VerificationMeta('avatarSvg'); + @override + late final GeneratedColumn avatarSvg = GeneratedColumn( + 'avatar_svg', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _senderProfileCounterMeta = + const VerificationMeta('senderProfileCounter'); + @override + late final GeneratedColumn senderProfileCounter = GeneratedColumn( + 'sender_profile_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _acceptedMeta = + const VerificationMeta('accepted'); + @override + late final GeneratedColumn accepted = GeneratedColumn( + 'accepted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _requestedMeta = + const VerificationMeta('requested'); + @override + late final GeneratedColumn requested = GeneratedColumn( + 'requested', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); + @override + late final GeneratedColumn hidden = GeneratedColumn( + 'hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("hidden" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _blockedMeta = + const VerificationMeta('blocked'); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _verifiedMeta = + const VerificationMeta('verified'); + @override + late final GeneratedColumn verified = GeneratedColumn( + 'verified', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _archivedMeta = + const VerificationMeta('archived'); + @override + late final GeneratedColumn archived = GeneratedColumn( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deletedMeta = + const VerificationMeta('deleted'); + @override + late final GeneratedColumn deleted = GeneratedColumn( + 'deleted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _alsoBestFriendMeta = + const VerificationMeta('alsoBestFriend'); + @override + late final GeneratedColumn alsoBestFriend = GeneratedColumn( + 'also_best_friend', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("also_best_friend" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deleteMessagesAfterXMinutesMeta = + const VerificationMeta('deleteMessagesAfterXMinutes'); + @override + late final GeneratedColumn deleteMessagesAfterXMinutes = + GeneratedColumn( + 'delete_messages_after_x_minutes', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(60 * 24)); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _totalMediaCounterMeta = + const VerificationMeta('totalMediaCounter'); + @override + late final GeneratedColumn totalMediaCounter = GeneratedColumn( + 'total_media_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _lastMessageSendMeta = + const VerificationMeta('lastMessageSend'); + @override + late final GeneratedColumn lastMessageSend = + GeneratedColumn('last_message_send', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastMessageReceivedMeta = + const VerificationMeta('lastMessageReceived'); + @override + late final GeneratedColumn lastMessageReceived = + GeneratedColumn('last_message_received', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameCounterChangeMeta = + const VerificationMeta('lastFlameCounterChange'); + @override + late final GeneratedColumn lastFlameCounterChange = + GeneratedColumn('last_flame_counter_change', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameSyncMeta = + const VerificationMeta('lastFlameSync'); + @override + late final GeneratedColumn lastFlameSync = + GeneratedColumn('last_flame_sync', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _flameCounterMeta = + const VerificationMeta('flameCounter'); + @override + late final GeneratedColumn flameCounter = GeneratedColumn( + 'flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + List get $columns => [ + userId, + username, + displayName, + nickName, + avatarSvg, + senderProfileCounter, + accepted, + requested, + hidden, + blocked, + verified, + archived, + deleted, + alsoBestFriend, + deleteMessagesAfterXMinutes, + createdAt, + totalMediaCounter, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'contacts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } + if (data.containsKey('username')) { + context.handle(_usernameMeta, + username.isAcceptableOrUnknown(data['username']!, _usernameMeta)); + } else if (isInserting) { + context.missing(_usernameMeta); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, _displayNameMeta)); + } + if (data.containsKey('nick_name')) { + context.handle(_nickNameMeta, + nickName.isAcceptableOrUnknown(data['nick_name']!, _nickNameMeta)); + } + if (data.containsKey('avatar_svg')) { + context.handle(_avatarSvgMeta, + avatarSvg.isAcceptableOrUnknown(data['avatar_svg']!, _avatarSvgMeta)); + } + if (data.containsKey('sender_profile_counter')) { + context.handle( + _senderProfileCounterMeta, + senderProfileCounter.isAcceptableOrUnknown( + data['sender_profile_counter']!, _senderProfileCounterMeta)); + } + if (data.containsKey('accepted')) { + context.handle(_acceptedMeta, + accepted.isAcceptableOrUnknown(data['accepted']!, _acceptedMeta)); + } + if (data.containsKey('requested')) { + context.handle(_requestedMeta, + requested.isAcceptableOrUnknown(data['requested']!, _requestedMeta)); + } + if (data.containsKey('hidden')) { + context.handle(_hiddenMeta, + hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta)); + } + if (data.containsKey('blocked')) { + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); + } + if (data.containsKey('verified')) { + context.handle(_verifiedMeta, + verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta)); + } + if (data.containsKey('archived')) { + context.handle(_archivedMeta, + archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); + } + if (data.containsKey('deleted')) { + context.handle(_deletedMeta, + deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); + } + if (data.containsKey('also_best_friend')) { + context.handle( + _alsoBestFriendMeta, + alsoBestFriend.isAcceptableOrUnknown( + data['also_best_friend']!, _alsoBestFriendMeta)); + } + if (data.containsKey('delete_messages_after_x_minutes')) { + context.handle( + _deleteMessagesAfterXMinutesMeta, + deleteMessagesAfterXMinutes.isAcceptableOrUnknown( + data['delete_messages_after_x_minutes']!, + _deleteMessagesAfterXMinutesMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('total_media_counter')) { + context.handle( + _totalMediaCounterMeta, + totalMediaCounter.isAcceptableOrUnknown( + data['total_media_counter']!, _totalMediaCounterMeta)); + } + if (data.containsKey('last_message_send')) { + context.handle( + _lastMessageSendMeta, + lastMessageSend.isAcceptableOrUnknown( + data['last_message_send']!, _lastMessageSendMeta)); + } + if (data.containsKey('last_message_received')) { + context.handle( + _lastMessageReceivedMeta, + lastMessageReceived.isAcceptableOrUnknown( + data['last_message_received']!, _lastMessageReceivedMeta)); + } + if (data.containsKey('last_flame_counter_change')) { + context.handle( + _lastFlameCounterChangeMeta, + lastFlameCounterChange.isAcceptableOrUnknown( + data['last_flame_counter_change']!, _lastFlameCounterChangeMeta)); + } + if (data.containsKey('last_flame_sync')) { + context.handle( + _lastFlameSyncMeta, + lastFlameSync.isAcceptableOrUnknown( + data['last_flame_sync']!, _lastFlameSyncMeta)); + } + if (data.containsKey('flame_counter')) { + context.handle( + _flameCounterMeta, + flameCounter.isAcceptableOrUnknown( + data['flame_counter']!, _flameCounterMeta)); + } + return context; + } + + @override + Set get $primaryKey => {userId}; + @override + Contact map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Contact( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + displayName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}display_name']), + nickName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), + avatarSvg: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}avatar_svg']), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, + accepted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, + requested: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, + hidden: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}hidden'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + verified: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + deleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + alsoBestFriend: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}also_best_friend'])!, + deleteMessagesAfterXMinutes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_x_minutes'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + totalMediaCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}total_media_counter'])!, + lastMessageSend: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_message_send']), + lastMessageReceived: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_received']), + lastFlameCounterChange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_flame_counter_change']), + lastFlameSync: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_flame_sync']), + flameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}flame_counter'])!, + ); + } + + @override + $ContactsTable createAlias(String alias) { + return $ContactsTable(attachedDatabase, alias); + } +} + +class Contact extends DataClass implements Insertable { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final String? avatarSvg; + final int senderProfileCounter; + final bool accepted; + final bool requested; + final bool hidden; + final bool blocked; + final bool verified; + final bool archived; + final bool deleted; + final bool alsoBestFriend; + final int deleteMessagesAfterXMinutes; + final DateTime createdAt; + final int totalMediaCounter; + final DateTime? lastMessageSend; + final DateTime? lastMessageReceived; + final DateTime? lastFlameCounterChange; + final DateTime? lastFlameSync; + final int flameCounter; + const Contact( + {required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvg, + required this.senderProfileCounter, + required this.accepted, + required this.requested, + required this.hidden, + required this.blocked, + required this.verified, + required this.archived, + required this.deleted, + required this.alsoBestFriend, + required this.deleteMessagesAfterXMinutes, + required this.createdAt, + required this.totalMediaCounter, + this.lastMessageSend, + this.lastMessageReceived, + this.lastFlameCounterChange, + this.lastFlameSync, + required this.flameCounter}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['username'] = Variable(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable(nickName); + } + if (!nullToAbsent || avatarSvg != null) { + map['avatar_svg'] = Variable(avatarSvg); + } + map['sender_profile_counter'] = Variable(senderProfileCounter); + map['accepted'] = Variable(accepted); + map['requested'] = Variable(requested); + map['hidden'] = Variable(hidden); + map['blocked'] = Variable(blocked); + map['verified'] = Variable(verified); + map['archived'] = Variable(archived); + map['deleted'] = Variable(deleted); + map['also_best_friend'] = Variable(alsoBestFriend); + map['delete_messages_after_x_minutes'] = + Variable(deleteMessagesAfterXMinutes); + map['created_at'] = Variable(createdAt); + map['total_media_counter'] = Variable(totalMediaCounter); + if (!nullToAbsent || lastMessageSend != null) { + map['last_message_send'] = Variable(lastMessageSend); + } + if (!nullToAbsent || lastMessageReceived != null) { + map['last_message_received'] = Variable(lastMessageReceived); + } + if (!nullToAbsent || lastFlameCounterChange != null) { + map['last_flame_counter_change'] = + Variable(lastFlameCounterChange); + } + if (!nullToAbsent || lastFlameSync != null) { + map['last_flame_sync'] = Variable(lastFlameSync); + } + map['flame_counter'] = Variable(flameCounter); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvg: avatarSvg == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvg), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + requested: Value(requested), + hidden: Value(hidden), + blocked: Value(blocked), + verified: Value(verified), + archived: Value(archived), + deleted: Value(deleted), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterXMinutes: Value(deleteMessagesAfterXMinutes), + createdAt: Value(createdAt), + totalMediaCounter: Value(totalMediaCounter), + lastMessageSend: lastMessageSend == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageSend), + lastMessageReceived: lastMessageReceived == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageReceived), + lastFlameCounterChange: lastFlameCounterChange == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameCounterChange), + lastFlameSync: lastFlameSync == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameSync), + flameCounter: Value(flameCounter), + ); + } + + factory Contact.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Contact( + userId: serializer.fromJson(json['userId']), + username: serializer.fromJson(json['username']), + displayName: serializer.fromJson(json['displayName']), + nickName: serializer.fromJson(json['nickName']), + avatarSvg: serializer.fromJson(json['avatarSvg']), + senderProfileCounter: + serializer.fromJson(json['senderProfileCounter']), + accepted: serializer.fromJson(json['accepted']), + requested: serializer.fromJson(json['requested']), + hidden: serializer.fromJson(json['hidden']), + blocked: serializer.fromJson(json['blocked']), + verified: serializer.fromJson(json['verified']), + archived: serializer.fromJson(json['archived']), + deleted: serializer.fromJson(json['deleted']), + alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), + deleteMessagesAfterXMinutes: + serializer.fromJson(json['deleteMessagesAfterXMinutes']), + createdAt: serializer.fromJson(json['createdAt']), + totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), + lastMessageSend: serializer.fromJson(json['lastMessageSend']), + lastMessageReceived: + serializer.fromJson(json['lastMessageReceived']), + lastFlameCounterChange: + serializer.fromJson(json['lastFlameCounterChange']), + lastFlameSync: serializer.fromJson(json['lastFlameSync']), + flameCounter: serializer.fromJson(json['flameCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'username': serializer.toJson(username), + 'displayName': serializer.toJson(displayName), + 'nickName': serializer.toJson(nickName), + 'avatarSvg': serializer.toJson(avatarSvg), + 'senderProfileCounter': serializer.toJson(senderProfileCounter), + 'accepted': serializer.toJson(accepted), + 'requested': serializer.toJson(requested), + 'hidden': serializer.toJson(hidden), + 'blocked': serializer.toJson(blocked), + 'verified': serializer.toJson(verified), + 'archived': serializer.toJson(archived), + 'deleted': serializer.toJson(deleted), + 'alsoBestFriend': serializer.toJson(alsoBestFriend), + 'deleteMessagesAfterXMinutes': + serializer.toJson(deleteMessagesAfterXMinutes), + 'createdAt': serializer.toJson(createdAt), + 'totalMediaCounter': serializer.toJson(totalMediaCounter), + 'lastMessageSend': serializer.toJson(lastMessageSend), + 'lastMessageReceived': serializer.toJson(lastMessageReceived), + 'lastFlameCounterChange': + serializer.toJson(lastFlameCounterChange), + 'lastFlameSync': serializer.toJson(lastFlameSync), + 'flameCounter': serializer.toJson(flameCounter), + }; + } + + Contact copyWith( + {int? userId, + String? username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + int? senderProfileCounter, + bool? accepted, + bool? requested, + bool? hidden, + bool? blocked, + bool? verified, + bool? archived, + bool? deleted, + bool? alsoBestFriend, + int? deleteMessagesAfterXMinutes, + DateTime? createdAt, + int? totalMediaCounter, + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + int? flameCounter}) => + Contact( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + requested: requested ?? this.requested, + hidden: hidden ?? this.hidden, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + archived: archived ?? this.archived, + deleted: deleted ?? this.deleted, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterXMinutes: + deleteMessagesAfterXMinutes ?? this.deleteMessagesAfterXMinutes, + createdAt: createdAt ?? this.createdAt, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + lastMessageSend: lastMessageSend.present + ? lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: lastMessageReceived.present + ? lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange.present + ? lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: + lastFlameSync.present ? lastFlameSync.value : this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + ); + Contact copyWithCompanion(ContactsCompanion data) { + return Contact( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvg: data.avatarSvg.present ? data.avatarSvg.value : this.avatarSvg, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + requested: data.requested.present ? data.requested.value : this.requested, + hidden: data.hidden.present ? data.hidden.value : this.hidden, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + archived: data.archived.present ? data.archived.value : this.archived, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterXMinutes: data.deleteMessagesAfterXMinutes.present + ? data.deleteMessagesAfterXMinutes.value + : this.deleteMessagesAfterXMinutes, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + totalMediaCounter: data.totalMediaCounter.present + ? data.totalMediaCounter.value + : this.totalMediaCounter, + lastMessageSend: data.lastMessageSend.present + ? data.lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: data.lastMessageReceived.present + ? data.lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: data.lastFlameCounterChange.present + ? data.lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: data.lastFlameSync.present + ? data.lastFlameSync.value + : this.lastFlameSync, + flameCounter: data.flameCounter.present + ? data.flameCounter.value + : this.flameCounter, + ); + } + + @override + String toString() { + return (StringBuffer('Contact(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('requested: $requested, ') + ..write('hidden: $hidden, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('archived: $archived, ') + ..write('deleted: $deleted, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') + ..write('createdAt: $createdAt, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + userId, + username, + displayName, + nickName, + avatarSvg, + senderProfileCounter, + accepted, + requested, + hidden, + blocked, + verified, + archived, + deleted, + alsoBestFriend, + deleteMessagesAfterXMinutes, + createdAt, + totalMediaCounter, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Contact && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + other.avatarSvg == this.avatarSvg && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.requested == this.requested && + other.hidden == this.hidden && + other.blocked == this.blocked && + other.verified == this.verified && + other.archived == this.archived && + other.deleted == this.deleted && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterXMinutes == + this.deleteMessagesAfterXMinutes && + other.createdAt == this.createdAt && + other.totalMediaCounter == this.totalMediaCounter && + other.lastMessageSend == this.lastMessageSend && + other.lastMessageReceived == this.lastMessageReceived && + other.lastFlameCounterChange == this.lastFlameCounterChange && + other.lastFlameSync == this.lastFlameSync && + other.flameCounter == this.flameCounter); +} + +class ContactsCompanion extends UpdateCompanion { + final Value userId; + final Value username; + final Value displayName; + final Value nickName; + final Value avatarSvg; + final Value senderProfileCounter; + final Value accepted; + final Value requested; + final Value hidden; + final Value blocked; + final Value verified; + final Value archived; + final Value deleted; + final Value alsoBestFriend; + final Value deleteMessagesAfterXMinutes; + final Value createdAt; + final Value totalMediaCounter; + final Value lastMessageSend; + final Value lastMessageReceived; + final Value lastFlameCounterChange; + final Value lastFlameSync; + final Value flameCounter; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.requested = const Value.absent(), + this.hidden = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.archived = const Value.absent(), + this.deleted = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterXMinutes = const Value.absent(), + this.createdAt = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.requested = const Value.absent(), + this.hidden = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.archived = const Value.absent(), + this.deleted = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterXMinutes = const Value.absent(), + this.createdAt = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + }) : username = Value(username); + static Insertable custom({ + Expression? userId, + Expression? username, + Expression? displayName, + Expression? nickName, + Expression? avatarSvg, + Expression? senderProfileCounter, + Expression? accepted, + Expression? requested, + Expression? hidden, + Expression? blocked, + Expression? verified, + Expression? archived, + Expression? deleted, + Expression? alsoBestFriend, + Expression? deleteMessagesAfterXMinutes, + Expression? createdAt, + Expression? totalMediaCounter, + Expression? lastMessageSend, + Expression? lastMessageReceived, + Expression? lastFlameCounterChange, + Expression? lastFlameSync, + Expression? flameCounter, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvg != null) 'avatar_svg': avatarSvg, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (requested != null) 'requested': requested, + if (hidden != null) 'hidden': hidden, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (archived != null) 'archived': archived, + if (deleted != null) 'deleted': deleted, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterXMinutes != null) + 'delete_messages_after_x_minutes': deleteMessagesAfterXMinutes, + if (createdAt != null) 'created_at': createdAt, + if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (lastMessageSend != null) 'last_message_send': lastMessageSend, + if (lastMessageReceived != null) + 'last_message_received': lastMessageReceived, + if (lastFlameCounterChange != null) + 'last_flame_counter_change': lastFlameCounterChange, + if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, + if (flameCounter != null) 'flame_counter': flameCounter, + }); + } + + ContactsCompanion copyWith( + {Value? userId, + Value? username, + Value? displayName, + Value? nickName, + Value? avatarSvg, + Value? senderProfileCounter, + Value? accepted, + Value? requested, + Value? hidden, + Value? blocked, + Value? verified, + Value? archived, + Value? deleted, + Value? alsoBestFriend, + Value? deleteMessagesAfterXMinutes, + Value? createdAt, + Value? totalMediaCounter, + Value? lastMessageSend, + Value? lastMessageReceived, + Value? lastFlameCounterChange, + Value? lastFlameSync, + Value? flameCounter}) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvg: avatarSvg ?? this.avatarSvg, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + requested: requested ?? this.requested, + hidden: hidden ?? this.hidden, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + archived: archived ?? this.archived, + deleted: deleted ?? this.deleted, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterXMinutes: + deleteMessagesAfterXMinutes ?? this.deleteMessagesAfterXMinutes, + createdAt: createdAt ?? this.createdAt, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + lastMessageSend: lastMessageSend ?? this.lastMessageSend, + lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, + lastFlameCounterChange: + lastFlameCounterChange ?? this.lastFlameCounterChange, + lastFlameSync: lastFlameSync ?? this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable(nickName.value); + } + if (avatarSvg.present) { + map['avatar_svg'] = Variable(avatarSvg.value); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable(accepted.value); + } + if (requested.present) { + map['requested'] = Variable(requested.value); + } + if (hidden.present) { + map['hidden'] = Variable(hidden.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (archived.present) { + map['archived'] = Variable(archived.value); + } + if (deleted.present) { + map['deleted'] = Variable(deleted.value); + } + if (alsoBestFriend.present) { + map['also_best_friend'] = Variable(alsoBestFriend.value); + } + if (deleteMessagesAfterXMinutes.present) { + map['delete_messages_after_x_minutes'] = + Variable(deleteMessagesAfterXMinutes.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable(totalMediaCounter.value); + } + if (lastMessageSend.present) { + map['last_message_send'] = Variable(lastMessageSend.value); + } + if (lastMessageReceived.present) { + map['last_message_received'] = + Variable(lastMessageReceived.value); + } + if (lastFlameCounterChange.present) { + map['last_flame_counter_change'] = + Variable(lastFlameCounterChange.value); + } + if (lastFlameSync.present) { + map['last_flame_sync'] = Variable(lastFlameSync.value); + } + if (flameCounter.present) { + map['flame_counter'] = Variable(flameCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('requested: $requested, ') + ..write('hidden: $hidden, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('archived: $archived, ') + ..write('deleted: $deleted, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') + ..write('createdAt: $createdAt, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter') + ..write(')')) + .toString(); + } +} + +class $MediaFilesTable extends MediaFiles + with TableInfo<$MediaFilesTable, MediaFile> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MediaFilesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _mediaIdMeta = + const VerificationMeta('mediaId'); + @override + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v4()); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($MediaFilesTable.$convertertype); + @override + late final GeneratedColumnWithTypeConverter + uploadState = GeneratedColumn('upload_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter($MediaFilesTable.$converteruploadStaten); + @override + late final GeneratedColumnWithTypeConverter + downloadState = GeneratedColumn( + 'download_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter( + $MediaFilesTable.$converterdownloadStaten); + static const VerificationMeta _requiresAuthenticationMeta = + const VerificationMeta('requiresAuthentication'); + @override + late final GeneratedColumn requiresAuthentication = + GeneratedColumn('requires_authentication', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("requires_authentication" IN (0, 1))')); + static const VerificationMeta _reopenByContactMeta = + const VerificationMeta('reopenByContact'); + @override + late final GeneratedColumn reopenByContact = GeneratedColumn( + 'reopen_by_contact', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("reopen_by_contact" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _storedByContactMeta = + const VerificationMeta('storedByContact'); + @override + late final GeneratedColumn storedByContact = GeneratedColumn( + 'stored_by_contact', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("stored_by_contact" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _displayLimitInMillisecondsMeta = + const VerificationMeta('displayLimitInMilliseconds'); + @override + late final GeneratedColumn displayLimitInMilliseconds = + GeneratedColumn('display_limit_in_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _downloadTokenMeta = + const VerificationMeta('downloadToken'); + @override + late final GeneratedColumn downloadToken = + GeneratedColumn('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionKeyMeta = + const VerificationMeta('encryptionKey'); + @override + late final GeneratedColumn encryptionKey = + GeneratedColumn('encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionMacMeta = + const VerificationMeta('encryptionMac'); + @override + late final GeneratedColumn encryptionMac = + GeneratedColumn('encryption_mac', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionNonceMeta = + const VerificationMeta('encryptionNonce'); + @override + late final GeneratedColumn encryptionNonce = + GeneratedColumn('encryption_nonce', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + reopenByContact, + storedByContact, + displayLimitInMilliseconds, + downloadToken, + encryptionKey, + encryptionMac, + encryptionNonce, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('media_id')) { + context.handle(_mediaIdMeta, + mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); + } + if (data.containsKey('requires_authentication')) { + context.handle( + _requiresAuthenticationMeta, + requiresAuthentication.isAcceptableOrUnknown( + data['requires_authentication']!, _requiresAuthenticationMeta)); + } else if (isInserting) { + context.missing(_requiresAuthenticationMeta); + } + if (data.containsKey('reopen_by_contact')) { + context.handle( + _reopenByContactMeta, + reopenByContact.isAcceptableOrUnknown( + data['reopen_by_contact']!, _reopenByContactMeta)); + } + if (data.containsKey('stored_by_contact')) { + context.handle( + _storedByContactMeta, + storedByContact.isAcceptableOrUnknown( + data['stored_by_contact']!, _storedByContactMeta)); + } + if (data.containsKey('display_limit_in_milliseconds')) { + context.handle( + _displayLimitInMillisecondsMeta, + displayLimitInMilliseconds.isAcceptableOrUnknown( + data['display_limit_in_milliseconds']!, + _displayLimitInMillisecondsMeta)); + } + if (data.containsKey('download_token')) { + context.handle( + _downloadTokenMeta, + downloadToken.isAcceptableOrUnknown( + data['download_token']!, _downloadTokenMeta)); + } + if (data.containsKey('encryption_key')) { + context.handle( + _encryptionKeyMeta, + encryptionKey.isAcceptableOrUnknown( + data['encryption_key']!, _encryptionKeyMeta)); + } + if (data.containsKey('encryption_mac')) { + context.handle( + _encryptionMacMeta, + encryptionMac.isAcceptableOrUnknown( + data['encryption_mac']!, _encryptionMacMeta)); + } + if (data.containsKey('encryption_nonce')) { + context.handle( + _encryptionNonceMeta, + encryptionNonce.isAcceptableOrUnknown( + data['encryption_nonce']!, _encryptionNonceMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {mediaId}; + @override + MediaFile map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFile( + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id'])!, + type: $MediaFilesTable.$convertertype.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + uploadState: $MediaFilesTable.$converteruploadStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}upload_state'])), + downloadState: $MediaFilesTable.$converterdownloadStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_state'])), + requiresAuthentication: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}requires_authentication'])!, + reopenByContact: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, + storedByContact: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!, + displayLimitInMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_limit_in_milliseconds']), + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), + encryptionKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_key']), + encryptionMac: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_mac']), + encryptionNonce: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_nonce']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $MediaFilesTable createAlias(String alias) { + return $MediaFilesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MediaType.values); + static JsonTypeConverter2 $converteruploadState = + const EnumNameConverter(UploadState.values); + static JsonTypeConverter2 + $converteruploadStaten = + JsonTypeConverter2.asNullable($converteruploadState); + static JsonTypeConverter2 + $converterdownloadState = + const EnumNameConverter(DownloadState.values); + static JsonTypeConverter2 + $converterdownloadStaten = + JsonTypeConverter2.asNullable($converterdownloadState); +} + +class MediaFile extends DataClass implements Insertable { + final String mediaId; + final MediaType type; + final UploadState? uploadState; + final DownloadState? downloadState; + final bool requiresAuthentication; + final bool reopenByContact; + final bool storedByContact; + final int? displayLimitInMilliseconds; + final Uint8List? downloadToken; + final Uint8List? encryptionKey; + final Uint8List? encryptionMac; + final Uint8List? encryptionNonce; + final DateTime createdAt; + const MediaFile( + {required this.mediaId, + required this.type, + this.uploadState, + this.downloadState, + required this.requiresAuthentication, + required this.reopenByContact, + required this.storedByContact, + this.displayLimitInMilliseconds, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['media_id'] = Variable(mediaId); + { + map['type'] = + Variable($MediaFilesTable.$convertertype.toSql(type)); + } + if (!nullToAbsent || uploadState != null) { + map['upload_state'] = Variable( + $MediaFilesTable.$converteruploadStaten.toSql(uploadState)); + } + if (!nullToAbsent || downloadState != null) { + map['download_state'] = Variable( + $MediaFilesTable.$converterdownloadStaten.toSql(downloadState)); + } + map['requires_authentication'] = Variable(requiresAuthentication); + map['reopen_by_contact'] = Variable(reopenByContact); + map['stored_by_contact'] = Variable(storedByContact); + if (!nullToAbsent || displayLimitInMilliseconds != null) { + map['display_limit_in_milliseconds'] = + Variable(displayLimitInMilliseconds); + } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || encryptionKey != null) { + map['encryption_key'] = Variable(encryptionKey); + } + if (!nullToAbsent || encryptionMac != null) { + map['encryption_mac'] = Variable(encryptionMac); + } + if (!nullToAbsent || encryptionNonce != null) { + map['encryption_nonce'] = Variable(encryptionNonce); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + mediaId: Value(mediaId), + type: Value(type), + uploadState: uploadState == null && nullToAbsent + ? const Value.absent() + : Value(uploadState), + downloadState: downloadState == null && nullToAbsent + ? const Value.absent() + : Value(downloadState), + requiresAuthentication: Value(requiresAuthentication), + reopenByContact: Value(reopenByContact), + storedByContact: Value(storedByContact), + displayLimitInMilliseconds: + displayLimitInMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(displayLimitInMilliseconds), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + encryptionKey: encryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(encryptionKey), + encryptionMac: encryptionMac == null && nullToAbsent + ? const Value.absent() + : Value(encryptionMac), + encryptionNonce: encryptionNonce == null && nullToAbsent + ? const Value.absent() + : Value(encryptionNonce), + createdAt: Value(createdAt), + ); + } + + factory MediaFile.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFile( + mediaId: serializer.fromJson(json['mediaId']), + type: $MediaFilesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + uploadState: $MediaFilesTable.$converteruploadStaten + .fromJson(serializer.fromJson(json['uploadState'])), + downloadState: $MediaFilesTable.$converterdownloadStaten + .fromJson(serializer.fromJson(json['downloadState'])), + requiresAuthentication: + serializer.fromJson(json['requiresAuthentication']), + reopenByContact: serializer.fromJson(json['reopenByContact']), + storedByContact: serializer.fromJson(json['storedByContact']), + displayLimitInMilliseconds: + serializer.fromJson(json['displayLimitInMilliseconds']), + downloadToken: serializer.fromJson(json['downloadToken']), + encryptionKey: serializer.fromJson(json['encryptionKey']), + encryptionMac: serializer.fromJson(json['encryptionMac']), + encryptionNonce: serializer.fromJson(json['encryptionNonce']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'mediaId': serializer.toJson(mediaId), + 'type': serializer + .toJson($MediaFilesTable.$convertertype.toJson(type)), + 'uploadState': serializer.toJson( + $MediaFilesTable.$converteruploadStaten.toJson(uploadState)), + 'downloadState': serializer.toJson( + $MediaFilesTable.$converterdownloadStaten.toJson(downloadState)), + 'requiresAuthentication': serializer.toJson(requiresAuthentication), + 'reopenByContact': serializer.toJson(reopenByContact), + 'storedByContact': serializer.toJson(storedByContact), + 'displayLimitInMilliseconds': + serializer.toJson(displayLimitInMilliseconds), + 'downloadToken': serializer.toJson(downloadToken), + 'encryptionKey': serializer.toJson(encryptionKey), + 'encryptionMac': serializer.toJson(encryptionMac), + 'encryptionNonce': serializer.toJson(encryptionNonce), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MediaFile copyWith( + {String? mediaId, + MediaType? type, + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + bool? requiresAuthentication, + bool? reopenByContact, + bool? storedByContact, + Value displayLimitInMilliseconds = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + DateTime? createdAt}) => + MediaFile( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState.present ? uploadState.value : this.uploadState, + downloadState: + downloadState.present ? downloadState.value : this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + reopenByContact: reopenByContact ?? this.reopenByContact, + storedByContact: storedByContact ?? this.storedByContact, + displayLimitInMilliseconds: displayLimitInMilliseconds.present + ? displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, + encryptionKey: + encryptionKey.present ? encryptionKey.value : this.encryptionKey, + encryptionMac: + encryptionMac.present ? encryptionMac.value : this.encryptionMac, + encryptionNonce: encryptionNonce.present + ? encryptionNonce.value + : this.encryptionNonce, + createdAt: createdAt ?? this.createdAt, + ); + MediaFile copyWithCompanion(MediaFilesCompanion data) { + return MediaFile( + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + type: data.type.present ? data.type.value : this.type, + uploadState: + data.uploadState.present ? data.uploadState.value : this.uploadState, + downloadState: data.downloadState.present + ? data.downloadState.value + : this.downloadState, + requiresAuthentication: data.requiresAuthentication.present + ? data.requiresAuthentication.value + : this.requiresAuthentication, + reopenByContact: data.reopenByContact.present + ? data.reopenByContact.value + : this.reopenByContact, + storedByContact: data.storedByContact.present + ? data.storedByContact.value + : this.storedByContact, + displayLimitInMilliseconds: data.displayLimitInMilliseconds.present + ? data.displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + encryptionKey: data.encryptionKey.present + ? data.encryptionKey.value + : this.encryptionKey, + encryptionMac: data.encryptionMac.present + ? data.encryptionMac.value + : this.encryptionMac, + encryptionNonce: data.encryptionNonce.present + ? data.encryptionNonce.value + : this.encryptionNonce, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFile(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('reopenByContact: $reopenByContact, ') + ..write('storedByContact: $storedByContact, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + reopenByContact, + storedByContact, + displayLimitInMilliseconds, + $driftBlobEquality.hash(downloadToken), + $driftBlobEquality.hash(encryptionKey), + $driftBlobEquality.hash(encryptionMac), + $driftBlobEquality.hash(encryptionNonce), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFile && + other.mediaId == this.mediaId && + other.type == this.type && + other.uploadState == this.uploadState && + other.downloadState == this.downloadState && + other.requiresAuthentication == this.requiresAuthentication && + other.reopenByContact == this.reopenByContact && + other.storedByContact == this.storedByContact && + other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && + $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && + $driftBlobEquality.equals( + other.encryptionNonce, this.encryptionNonce) && + other.createdAt == this.createdAt); +} + +class MediaFilesCompanion extends UpdateCompanion { + final Value mediaId; + final Value type; + final Value uploadState; + final Value downloadState; + final Value requiresAuthentication; + final Value reopenByContact; + final Value storedByContact; + final Value displayLimitInMilliseconds; + final Value downloadToken; + final Value encryptionKey; + final Value encryptionMac; + final Value encryptionNonce; + final Value createdAt; + final Value rowid; + const MediaFilesCompanion({ + this.mediaId = const Value.absent(), + this.type = const Value.absent(), + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.reopenByContact = const Value.absent(), + this.storedByContact = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + this.mediaId = const Value.absent(), + required MediaType type, + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + required bool requiresAuthentication, + this.reopenByContact = const Value.absent(), + this.storedByContact = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : type = Value(type), + requiresAuthentication = Value(requiresAuthentication); + static Insertable custom({ + Expression? mediaId, + Expression? type, + Expression? uploadState, + Expression? downloadState, + Expression? requiresAuthentication, + Expression? reopenByContact, + Expression? storedByContact, + Expression? displayLimitInMilliseconds, + Expression? downloadToken, + Expression? encryptionKey, + Expression? encryptionMac, + Expression? encryptionNonce, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (mediaId != null) 'media_id': mediaId, + if (type != null) 'type': type, + if (uploadState != null) 'upload_state': uploadState, + if (downloadState != null) 'download_state': downloadState, + if (requiresAuthentication != null) + 'requires_authentication': requiresAuthentication, + if (reopenByContact != null) 'reopen_by_contact': reopenByContact, + if (storedByContact != null) 'stored_by_contact': storedByContact, + if (displayLimitInMilliseconds != null) + 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (downloadToken != null) 'download_token': downloadToken, + if (encryptionKey != null) 'encryption_key': encryptionKey, + if (encryptionMac != null) 'encryption_mac': encryptionMac, + if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith( + {Value? mediaId, + Value? type, + Value? uploadState, + Value? downloadState, + Value? requiresAuthentication, + Value? reopenByContact, + Value? storedByContact, + Value? displayLimitInMilliseconds, + Value? downloadToken, + Value? encryptionKey, + Value? encryptionMac, + Value? encryptionNonce, + Value? createdAt, + Value? rowid}) { + return MediaFilesCompanion( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState ?? this.uploadState, + downloadState: downloadState ?? this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + reopenByContact: reopenByContact ?? this.reopenByContact, + storedByContact: storedByContact ?? this.storedByContact, + displayLimitInMilliseconds: + displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + downloadToken: downloadToken ?? this.downloadToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + encryptionMac: encryptionMac ?? this.encryptionMac, + encryptionNonce: encryptionNonce ?? this.encryptionNonce, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (type.present) { + map['type'] = + Variable($MediaFilesTable.$convertertype.toSql(type.value)); + } + if (uploadState.present) { + map['upload_state'] = Variable( + $MediaFilesTable.$converteruploadStaten.toSql(uploadState.value)); + } + if (downloadState.present) { + map['download_state'] = Variable( + $MediaFilesTable.$converterdownloadStaten.toSql(downloadState.value)); + } + if (requiresAuthentication.present) { + map['requires_authentication'] = + Variable(requiresAuthentication.value); + } + if (reopenByContact.present) { + map['reopen_by_contact'] = Variable(reopenByContact.value); + } + if (storedByContact.present) { + map['stored_by_contact'] = Variable(storedByContact.value); + } + if (displayLimitInMilliseconds.present) { + map['display_limit_in_milliseconds'] = + Variable(displayLimitInMilliseconds.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (encryptionKey.present) { + map['encryption_key'] = Variable(encryptionKey.value); + } + if (encryptionMac.present) { + map['encryption_mac'] = Variable(encryptionMac.value); + } + if (encryptionNonce.present) { + map['encryption_nonce'] = Variable(encryptionNonce.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('reopenByContact: $reopenByContact, ') + ..write('storedByContact: $storedByContact, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessagesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderIdMeta = + const VerificationMeta('senderId'); + @override + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + static const VerificationMeta _contentMeta = + const VerificationMeta('content'); + @override + late final GeneratedColumn content = GeneratedColumn( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _mediaIdMeta = + const VerificationMeta('mediaId'); + @override + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES media_files (media_id)')); + static const VerificationMeta _quotesMessageIdMeta = + const VerificationMeta('quotesMessageId'); + @override + late final GeneratedColumn quotesMessageId = GeneratedColumn( + 'quotes_message_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id)')); + static const VerificationMeta _isDeletedFromSenderMeta = + const VerificationMeta('isDeletedFromSender'); + @override + late final GeneratedColumn isDeletedFromSender = GeneratedColumn( + 'is_deleted_from_sender', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_deleted_from_sender" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _isEditedMeta = + const VerificationMeta('isEdited'); + @override + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_edited" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _acknowledgeByUserMeta = + const VerificationMeta('acknowledgeByUser'); + @override + late final GeneratedColumn acknowledgeByUser = GeneratedColumn( + 'acknowledge_by_user', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("acknowledge_by_user" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _acknowledgeByServerMeta = + const VerificationMeta('acknowledgeByServer'); + @override + late final GeneratedColumn acknowledgeByServer = GeneratedColumn( + 'acknowledge_by_server', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("acknowledge_by_server" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _openedByCounterMeta = + const VerificationMeta('openedByCounter'); + @override + late final GeneratedColumn openedByCounter = GeneratedColumn( + 'opened_by_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _openedAtMeta = + const VerificationMeta('openedAt'); + @override + late final GeneratedColumn openedAt = GeneratedColumn( + 'opened_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _modifiedAtMeta = + const VerificationMeta('modifiedAt'); + @override + late final GeneratedColumn modifiedAt = GeneratedColumn( + 'modified_at', aliasedName, true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupId, + messageId, + senderId, + content, + mediaId, + quotesMessageId, + isDeletedFromSender, + isEdited, + acknowledgeByUser, + acknowledgeByServer, + openedByCounter, + openedAt, + createdAt, + modifiedAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'messages'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('sender_id')) { + context.handle(_senderIdMeta, + senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta)); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta)); + } + if (data.containsKey('media_id')) { + context.handle(_mediaIdMeta, + mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); + } + if (data.containsKey('quotes_message_id')) { + context.handle( + _quotesMessageIdMeta, + quotesMessageId.isAcceptableOrUnknown( + data['quotes_message_id']!, _quotesMessageIdMeta)); + } + if (data.containsKey('is_deleted_from_sender')) { + context.handle( + _isDeletedFromSenderMeta, + isDeletedFromSender.isAcceptableOrUnknown( + data['is_deleted_from_sender']!, _isDeletedFromSenderMeta)); + } + if (data.containsKey('is_edited')) { + context.handle(_isEditedMeta, + isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta)); + } + if (data.containsKey('acknowledge_by_user')) { + context.handle( + _acknowledgeByUserMeta, + acknowledgeByUser.isAcceptableOrUnknown( + data['acknowledge_by_user']!, _acknowledgeByUserMeta)); + } + if (data.containsKey('acknowledge_by_server')) { + context.handle( + _acknowledgeByServerMeta, + acknowledgeByServer.isAcceptableOrUnknown( + data['acknowledge_by_server']!, _acknowledgeByServerMeta)); + } + if (data.containsKey('opened_by_counter')) { + context.handle( + _openedByCounterMeta, + openedByCounter.isAcceptableOrUnknown( + data['opened_by_counter']!, _openedByCounterMeta)); + } + if (data.containsKey('opened_at')) { + context.handle(_openedAtMeta, + openedAt.isAcceptableOrUnknown(data['opened_at']!, _openedAtMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('modified_at')) { + context.handle( + _modifiedAtMeta, + modifiedAt.isAcceptableOrUnknown( + data['modified_at']!, _modifiedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId}; + @override + Message map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Message( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + quotesMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), + isDeletedFromSender: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, + isEdited: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_edited'])!, + acknowledgeByUser: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}acknowledge_by_user'])!, + acknowledgeByServer: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}acknowledge_by_server'])!, + openedByCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}opened_by_counter'])!, + openedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + modifiedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), + ); + } + + @override + $MessagesTable createAlias(String alias) { + return $MessagesTable(attachedDatabase, alias); + } +} + +class Message extends DataClass implements Insertable { + final String groupId; + final String messageId; + final int? senderId; + final String? content; + final String? mediaId; + final String? quotesMessageId; + final bool isDeletedFromSender; + final bool isEdited; + final bool acknowledgeByUser; + final bool acknowledgeByServer; + final int openedByCounter; + final DateTime? openedAt; + final DateTime createdAt; + final DateTime? modifiedAt; + const Message( + {required this.groupId, + required this.messageId, + this.senderId, + this.content, + this.mediaId, + this.quotesMessageId, + required this.isDeletedFromSender, + required this.isEdited, + required this.acknowledgeByUser, + required this.acknowledgeByServer, + required this.openedByCounter, + this.openedAt, + required this.createdAt, + this.modifiedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + if (!nullToAbsent || mediaId != null) { + map['media_id'] = Variable(mediaId); + } + if (!nullToAbsent || quotesMessageId != null) { + map['quotes_message_id'] = Variable(quotesMessageId); + } + map['is_deleted_from_sender'] = Variable(isDeletedFromSender); + map['is_edited'] = Variable(isEdited); + map['acknowledge_by_user'] = Variable(acknowledgeByUser); + map['acknowledge_by_server'] = Variable(acknowledgeByServer); + map['opened_by_counter'] = Variable(openedByCounter); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable(openedAt); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable(modifiedAt); + } + return map; + } + + MessagesCompanion toCompanion(bool nullToAbsent) { + return MessagesCompanion( + groupId: Value(groupId), + messageId: Value(messageId), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + mediaId: mediaId == null && nullToAbsent + ? const Value.absent() + : Value(mediaId), + quotesMessageId: quotesMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotesMessageId), + isDeletedFromSender: Value(isDeletedFromSender), + isEdited: Value(isEdited), + acknowledgeByUser: Value(acknowledgeByUser), + acknowledgeByServer: Value(acknowledgeByServer), + openedByCounter: Value(openedByCounter), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), + createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), + ); + } + + factory Message.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Message( + groupId: serializer.fromJson(json['groupId']), + messageId: serializer.fromJson(json['messageId']), + senderId: serializer.fromJson(json['senderId']), + content: serializer.fromJson(json['content']), + mediaId: serializer.fromJson(json['mediaId']), + quotesMessageId: serializer.fromJson(json['quotesMessageId']), + isDeletedFromSender: + serializer.fromJson(json['isDeletedFromSender']), + isEdited: serializer.fromJson(json['isEdited']), + acknowledgeByUser: serializer.fromJson(json['acknowledgeByUser']), + acknowledgeByServer: + serializer.fromJson(json['acknowledgeByServer']), + openedByCounter: serializer.fromJson(json['openedByCounter']), + openedAt: serializer.fromJson(json['openedAt']), + createdAt: serializer.fromJson(json['createdAt']), + modifiedAt: serializer.fromJson(json['modifiedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'messageId': serializer.toJson(messageId), + 'senderId': serializer.toJson(senderId), + 'content': serializer.toJson(content), + 'mediaId': serializer.toJson(mediaId), + 'quotesMessageId': serializer.toJson(quotesMessageId), + 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), + 'isEdited': serializer.toJson(isEdited), + 'acknowledgeByUser': serializer.toJson(acknowledgeByUser), + 'acknowledgeByServer': serializer.toJson(acknowledgeByServer), + 'openedByCounter': serializer.toJson(openedByCounter), + 'openedAt': serializer.toJson(openedAt), + 'createdAt': serializer.toJson(createdAt), + 'modifiedAt': serializer.toJson(modifiedAt), + }; + } + + Message copyWith( + {String? groupId, + String? messageId, + Value senderId = const Value.absent(), + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value quotesMessageId = const Value.absent(), + bool? isDeletedFromSender, + bool? isEdited, + bool? acknowledgeByUser, + bool? acknowledgeByServer, + int? openedByCounter, + Value openedAt = const Value.absent(), + DateTime? createdAt, + Value modifiedAt = const Value.absent()}) => + Message( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId.present ? senderId.value : this.senderId, + content: content.present ? content.value : this.content, + mediaId: mediaId.present ? mediaId.value : this.mediaId, + quotesMessageId: quotesMessageId.present + ? quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + isEdited: isEdited ?? this.isEdited, + acknowledgeByUser: acknowledgeByUser ?? this.acknowledgeByUser, + acknowledgeByServer: acknowledgeByServer ?? this.acknowledgeByServer, + openedByCounter: openedByCounter ?? this.openedByCounter, + openedAt: openedAt.present ? openedAt.value : this.openedAt, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ); + Message copyWithCompanion(MessagesCompanion data) { + return Message( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + content: data.content.present ? data.content.value : this.content, + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + quotesMessageId: data.quotesMessageId.present + ? data.quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: data.isDeletedFromSender.present + ? data.isDeletedFromSender.value + : this.isDeletedFromSender, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + acknowledgeByUser: data.acknowledgeByUser.present + ? data.acknowledgeByUser.value + : this.acknowledgeByUser, + acknowledgeByServer: data.acknowledgeByServer.present + ? data.acknowledgeByServer.value + : this.acknowledgeByServer, + openedByCounter: data.openedByCounter.present + ? data.openedByCounter.value + : this.openedByCounter, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: + data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Message(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('isEdited: $isEdited, ') + ..write('acknowledgeByUser: $acknowledgeByUser, ') + ..write('acknowledgeByServer: $acknowledgeByServer, ') + ..write('openedByCounter: $openedByCounter, ') + ..write('openedAt: $openedAt, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + messageId, + senderId, + content, + mediaId, + quotesMessageId, + isDeletedFromSender, + isEdited, + acknowledgeByUser, + acknowledgeByServer, + openedByCounter, + openedAt, + createdAt, + modifiedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Message && + other.groupId == this.groupId && + other.messageId == this.messageId && + other.senderId == this.senderId && + other.content == this.content && + other.mediaId == this.mediaId && + other.quotesMessageId == this.quotesMessageId && + other.isDeletedFromSender == this.isDeletedFromSender && + other.isEdited == this.isEdited && + other.acknowledgeByUser == this.acknowledgeByUser && + other.acknowledgeByServer == this.acknowledgeByServer && + other.openedByCounter == this.openedByCounter && + other.openedAt == this.openedAt && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt); +} + +class MessagesCompanion extends UpdateCompanion { + final Value groupId; + final Value messageId; + final Value senderId; + final Value content; + final Value mediaId; + final Value quotesMessageId; + final Value isDeletedFromSender; + final Value isEdited; + final Value acknowledgeByUser; + final Value acknowledgeByServer; + final Value openedByCounter; + final Value openedAt; + final Value createdAt; + final Value modifiedAt; + final Value rowid; + const MessagesCompanion({ + this.groupId = const Value.absent(), + this.messageId = const Value.absent(), + this.senderId = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.isEdited = const Value.absent(), + this.acknowledgeByUser = const Value.absent(), + this.acknowledgeByServer = const Value.absent(), + this.openedByCounter = const Value.absent(), + this.openedAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessagesCompanion.insert({ + required String groupId, + required String messageId, + this.senderId = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.isEdited = const Value.absent(), + this.acknowledgeByUser = const Value.absent(), + this.acknowledgeByServer = const Value.absent(), + this.openedByCounter = const Value.absent(), + this.openedAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + messageId = Value(messageId); + static Insertable custom({ + Expression? groupId, + Expression? messageId, + Expression? senderId, + Expression? content, + Expression? mediaId, + Expression? quotesMessageId, + Expression? isDeletedFromSender, + Expression? isEdited, + Expression? acknowledgeByUser, + Expression? acknowledgeByServer, + Expression? openedByCounter, + Expression? openedAt, + Expression? createdAt, + Expression? modifiedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (messageId != null) 'message_id': messageId, + if (senderId != null) 'sender_id': senderId, + if (content != null) 'content': content, + if (mediaId != null) 'media_id': mediaId, + if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, + if (isDeletedFromSender != null) + 'is_deleted_from_sender': isDeletedFromSender, + if (isEdited != null) 'is_edited': isEdited, + if (acknowledgeByUser != null) 'acknowledge_by_user': acknowledgeByUser, + if (acknowledgeByServer != null) + 'acknowledge_by_server': acknowledgeByServer, + if (openedByCounter != null) 'opened_by_counter': openedByCounter, + if (openedAt != null) 'opened_at': openedAt, + if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessagesCompanion copyWith( + {Value? groupId, + Value? messageId, + Value? senderId, + Value? content, + Value? mediaId, + Value? quotesMessageId, + Value? isDeletedFromSender, + Value? isEdited, + Value? acknowledgeByUser, + Value? acknowledgeByServer, + Value? openedByCounter, + Value? openedAt, + Value? createdAt, + Value? modifiedAt, + Value? rowid}) { + return MessagesCompanion( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId ?? this.senderId, + content: content ?? this.content, + mediaId: mediaId ?? this.mediaId, + quotesMessageId: quotesMessageId ?? this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + isEdited: isEdited ?? this.isEdited, + acknowledgeByUser: acknowledgeByUser ?? this.acknowledgeByUser, + acknowledgeByServer: acknowledgeByServer ?? this.acknowledgeByServer, + openedByCounter: openedByCounter ?? this.openedByCounter, + openedAt: openedAt ?? this.openedAt, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (quotesMessageId.present) { + map['quotes_message_id'] = Variable(quotesMessageId.value); + } + if (isDeletedFromSender.present) { + map['is_deleted_from_sender'] = Variable(isDeletedFromSender.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + if (acknowledgeByUser.present) { + map['acknowledge_by_user'] = Variable(acknowledgeByUser.value); + } + if (acknowledgeByServer.present) { + map['acknowledge_by_server'] = Variable(acknowledgeByServer.value); + } + if (openedByCounter.present) { + map['opened_by_counter'] = Variable(openedByCounter.value); + } + if (openedAt.present) { + map['opened_at'] = Variable(openedAt.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (modifiedAt.present) { + map['modified_at'] = Variable(modifiedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessagesCompanion(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('isEdited: $isEdited, ') + ..write('acknowledgeByUser: $acknowledgeByUser, ') + ..write('acknowledgeByServer: $acknowledgeByServer, ') + ..write('openedByCounter: $openedByCounter, ') + ..write('openedAt: $openedAt, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MessageHistoriesTable extends MessageHistories + with TableInfo<$MessageHistoriesTable, MessageHistory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessageHistoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _contentMeta = + const VerificationMeta('content'); + @override + late final GeneratedColumn content = GeneratedColumn( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [messageId, content, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_histories'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId, createdAt}; + @override + MessageHistory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageHistory( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $MessageHistoriesTable createAlias(String alias) { + return $MessageHistoriesTable(attachedDatabase, alias); + } +} + +class MessageHistory extends DataClass implements Insertable { + final String messageId; + final String? content; + final DateTime createdAt; + const MessageHistory( + {required this.messageId, this.content, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MessageHistoriesCompanion toCompanion(bool nullToAbsent) { + return MessageHistoriesCompanion( + messageId: Value(messageId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + createdAt: Value(createdAt), + ); + } + + factory MessageHistory.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageHistory( + messageId: serializer.fromJson(json['messageId']), + content: serializer.fromJson(json['content']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'content': serializer.toJson(content), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MessageHistory copyWith( + {String? messageId, + Value content = const Value.absent(), + DateTime? createdAt}) => + MessageHistory( + messageId: messageId ?? this.messageId, + content: content.present ? content.value : this.content, + createdAt: createdAt ?? this.createdAt, + ); + MessageHistory copyWithCompanion(MessageHistoriesCompanion data) { + return MessageHistory( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + content: data.content.present ? data.content.value : this.content, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageHistory(') + ..write('messageId: $messageId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, content, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageHistory && + other.messageId == this.messageId && + other.content == this.content && + other.createdAt == this.createdAt); +} + +class MessageHistoriesCompanion extends UpdateCompanion { + final Value messageId; + final Value content; + final Value createdAt; + final Value rowid; + const MessageHistoriesCompanion({ + this.messageId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageHistoriesCompanion.insert({ + required String messageId, + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId); + static Insertable custom({ + Expression? messageId, + Expression? content, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (content != null) 'content': content, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageHistoriesCompanion copyWith( + {Value? messageId, + Value? content, + Value? createdAt, + Value? rowid}) { + return MessageHistoriesCompanion( + messageId: messageId ?? this.messageId, + content: content ?? this.content, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesCompanion(') + ..write('messageId: $messageId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ReactionsTable extends Reactions + with TableInfo<$ReactionsTable, Reaction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReactionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _emojiMeta = const VerificationMeta('emoji'); + @override + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderIdMeta = + const VerificationMeta('senderId'); + @override + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [messageId, emoji, senderId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'reactions'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('emoji')) { + context.handle( + _emojiMeta, emoji.isAcceptableOrUnknown(data['emoji']!, _emojiMeta)); + } else if (isInserting) { + context.missing(_emojiMeta); + } + if (data.containsKey('sender_id')) { + context.handle(_senderIdMeta, + senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId, senderId, createdAt}; + @override + Reaction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Reaction( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + emoji: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}emoji'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReactionsTable createAlias(String alias) { + return $ReactionsTable(attachedDatabase, alias); + } +} + +class Reaction extends DataClass implements Insertable { + final String messageId; + final String emoji; + final int? senderId; + final DateTime createdAt; + const Reaction( + {required this.messageId, + required this.emoji, + this.senderId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['emoji'] = Variable(emoji); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReactionsCompanion toCompanion(bool nullToAbsent) { + return ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + createdAt: Value(createdAt), + ); + } + + factory Reaction.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Reaction( + messageId: serializer.fromJson(json['messageId']), + emoji: serializer.fromJson(json['emoji']), + senderId: serializer.fromJson(json['senderId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'emoji': serializer.toJson(emoji), + 'senderId': serializer.toJson(senderId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Reaction copyWith( + {String? messageId, + String? emoji, + Value senderId = const Value.absent(), + DateTime? createdAt}) => + Reaction( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId.present ? senderId.value : this.senderId, + createdAt: createdAt ?? this.createdAt, + ); + Reaction copyWithCompanion(ReactionsCompanion data) { + return Reaction( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Reaction(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, emoji, senderId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Reaction && + other.messageId == this.messageId && + other.emoji == this.emoji && + other.senderId == this.senderId && + other.createdAt == this.createdAt); +} + +class ReactionsCompanion extends UpdateCompanion { + final Value messageId; + final Value emoji; + final Value senderId; + final Value createdAt; + final Value rowid; + const ReactionsCompanion({ + this.messageId = const Value.absent(), + this.emoji = const Value.absent(), + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReactionsCompanion.insert({ + required String messageId, + required String emoji, + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + emoji = Value(emoji); + static Insertable custom({ + Expression? messageId, + Expression? emoji, + Expression? senderId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (emoji != null) 'emoji': emoji, + if (senderId != null) 'sender_id': senderId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReactionsCompanion copyWith( + {Value? messageId, + Value? emoji, + Value? senderId, + Value? createdAt, + Value? rowid}) { + return ReactionsCompanion( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId ?? this.senderId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v4()); + static const VerificationMeta _isGroupAdminMeta = + const VerificationMeta('isGroupAdmin'); + @override + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_admin" IN (0, 1))')); + static const VerificationMeta _isGroupOfTwoMeta = + const VerificationMeta('isGroupOfTwo'); + @override + late final GeneratedColumn isGroupOfTwo = GeneratedColumn( + 'is_group_of_two', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_of_two" IN (0, 1))')); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _lastMessageExchangeMeta = + const VerificationMeta('lastMessageExchange'); + @override + late final GeneratedColumn lastMessageExchange = + GeneratedColumn('last_message_exchange', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isGroupOfTwo, + pinned, + lastMessageExchange, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } + if (data.containsKey('is_group_admin')) { + context.handle( + _isGroupAdminMeta, + isGroupAdmin.isAcceptableOrUnknown( + data['is_group_admin']!, _isGroupAdminMeta)); + } else if (isInserting) { + context.missing(_isGroupAdminMeta); + } + if (data.containsKey('is_group_of_two')) { + context.handle( + _isGroupOfTwoMeta, + isGroupOfTwo.isAcceptableOrUnknown( + data['is_group_of_two']!, _isGroupOfTwoMeta)); + } else if (isInserting) { + context.missing(_isGroupOfTwoMeta); + } + if (data.containsKey('pinned')) { + context.handle(_pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + } + if (data.containsKey('last_message_exchange')) { + context.handle( + _lastMessageExchangeMeta, + lastMessageExchange.isAcceptableOrUnknown( + data['last_message_exchange']!, _lastMessageExchangeMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupId}; + @override + Group map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Group( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + isGroupAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, + isGroupOfTwo: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_of_two'])!, + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_exchange'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $GroupsTable createAlias(String alias) { + return $GroupsTable(attachedDatabase, alias); + } +} + +class Group extends DataClass implements Insertable { + final String groupId; + final bool isGroupAdmin; + final bool isGroupOfTwo; + final bool pinned; + final DateTime lastMessageExchange; + final DateTime createdAt; + const Group( + {required this.groupId, + required this.isGroupAdmin, + required this.isGroupOfTwo, + required this.pinned, + required this.lastMessageExchange, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_group_of_two'] = Variable(isGroupOfTwo); + map['pinned'] = Variable(pinned); + map['last_message_exchange'] = Variable(lastMessageExchange); + map['created_at'] = Variable(createdAt); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isGroupOfTwo: Value(isGroupOfTwo), + pinned: Value(pinned), + lastMessageExchange: Value(lastMessageExchange), + createdAt: Value(createdAt), + ); + } + + factory Group.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Group( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), + pinned: serializer.fromJson(json['pinned']), + lastMessageExchange: + serializer.fromJson(json['lastMessageExchange']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), + 'pinned': serializer.toJson(pinned), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Group copyWith( + {String? groupId, + bool? isGroupAdmin, + bool? isGroupOfTwo, + bool? pinned, + DateTime? lastMessageExchange, + DateTime? createdAt}) => + Group( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, + pinned: pinned ?? this.pinned, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + createdAt: createdAt ?? this.createdAt, + ); + Group copyWithCompanion(GroupsCompanion data) { + return Group( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isGroupOfTwo: data.isGroupOfTwo.present + ? data.isGroupOfTwo.value + : this.isGroupOfTwo, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Group(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isGroupOfTwo: $isGroupOfTwo, ') + ..write('pinned: $pinned, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, + lastMessageExchange, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Group && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isGroupOfTwo == this.isGroupOfTwo && + other.pinned == this.pinned && + other.lastMessageExchange == this.lastMessageExchange && + other.createdAt == this.createdAt); +} + +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isGroupOfTwo; + final Value pinned; + final Value lastMessageExchange; + final Value createdAt; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isGroupOfTwo = const Value.absent(), + this.pinned = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + this.groupId = const Value.absent(), + required bool isGroupAdmin, + required bool isGroupOfTwo, + this.pinned = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : isGroupAdmin = Value(isGroupAdmin), + isGroupOfTwo = Value(isGroupOfTwo); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isGroupOfTwo, + Expression? pinned, + Expression? lastMessageExchange, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, + if (pinned != null) 'pinned': pinned, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith( + {Value? groupId, + Value? isGroupAdmin, + Value? isGroupOfTwo, + Value? pinned, + Value? lastMessageExchange, + Value? createdAt, + Value? rowid}) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, + pinned: pinned ?? this.pinned, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); + } + if (isGroupOfTwo.present) { + map['is_group_of_two'] = Variable(isGroupOfTwo.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = + Variable(lastMessageExchange.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isGroupOfTwo: $isGroupOfTwo, ') + ..write('pinned: $pinned, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupMembersTable extends GroupMembers + with TableInfo<$GroupMembersTable, GroupMember> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupMembersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + @override + late final GeneratedColumnWithTypeConverter + memberState = GeneratedColumn('member_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter( + $GroupMembersTable.$convertermemberStaten); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [groupId, contactId, memberState, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_members'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupId, contactId}; + @override + GroupMember map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMember( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + memberState: $GroupMembersTable.$convertermemberStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}member_state'])), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $GroupMembersTable createAlias(String alias) { + return $GroupMembersTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertermemberState = + const EnumNameConverter(MemberState.values); + static JsonTypeConverter2 + $convertermemberStaten = + JsonTypeConverter2.asNullable($convertermemberState); +} + +class GroupMember extends DataClass implements Insertable { + final String groupId; + final int contactId; + final MemberState? memberState; + final DateTime createdAt; + const GroupMember( + {required this.groupId, + required this.contactId, + this.memberState, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || memberState != null) { + map['member_state'] = Variable( + $GroupMembersTable.$convertermemberStaten.toSql(memberState)); + } + map['created_at'] = Variable(createdAt); + return map; + } + + GroupMembersCompanion toCompanion(bool nullToAbsent) { + return GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(contactId), + memberState: memberState == null && nullToAbsent + ? const Value.absent() + : Value(memberState), + createdAt: Value(createdAt), + ); + } + + factory GroupMember.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMember( + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + memberState: $GroupMembersTable.$convertermemberStaten + .fromJson(serializer.fromJson(json['memberState'])), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'memberState': serializer.toJson( + $GroupMembersTable.$convertermemberStaten.toJson(memberState)), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GroupMember copyWith( + {String? groupId, + int? contactId, + Value memberState = const Value.absent(), + DateTime? createdAt}) => + GroupMember( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState.present ? memberState.value : this.memberState, + createdAt: createdAt ?? this.createdAt, + ); + GroupMember copyWithCompanion(GroupMembersCompanion data) { + return GroupMember( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + memberState: + data.memberState.present ? data.memberState.value : this.memberState, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMember(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, contactId, memberState, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMember && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.memberState == this.memberState && + other.createdAt == this.createdAt); +} + +class GroupMembersCompanion extends UpdateCompanion { + final Value groupId; + final Value contactId; + final Value memberState; + final Value createdAt; + final Value rowid; + const GroupMembersCompanion({ + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.memberState = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMembersCompanion.insert({ + required String groupId, + required int contactId, + this.memberState = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + contactId = Value(contactId); + static Insertable custom({ + Expression? groupId, + Expression? contactId, + Expression? memberState, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (memberState != null) 'member_state': memberState, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMembersCompanion copyWith( + {Value? groupId, + Value? contactId, + Value? memberState, + Value? createdAt, + Value? rowid}) { + return GroupMembersCompanion( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState ?? this.memberState, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (memberState.present) { + map['member_state'] = Variable( + $GroupMembersTable.$convertermemberStaten.toSql(memberState.value)); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMembersCompanion(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReceiptsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _receiptIdMeta = + const VerificationMeta('receiptId'); + @override + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v4()); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _messageMeta = + const VerificationMeta('message'); + @override + late final GeneratedColumn message = GeneratedColumn( + 'message', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _contactWillSendsReceiptMeta = + const VerificationMeta('contactWillSendsReceipt'); + @override + late final GeneratedColumn contactWillSendsReceipt = + GeneratedColumn('contact_will_sends_receipt', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("contact_will_sends_receipt" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _retryCountMeta = + const VerificationMeta('retryCount'); + @override + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _lastRetryMeta = + const VerificationMeta('lastRetry'); + @override + late final GeneratedColumn lastRetry = GeneratedColumn( + 'last_retry', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + receiptId, + contactId, + messageId, + message, + contactWillSendsReceipt, + retryCount, + lastRetry, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('receipt_id')) { + context.handle(_receiptIdMeta, + receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta)); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } + if (data.containsKey('message')) { + context.handle(_messageMeta, + message.isAcceptableOrUnknown(data['message']!, _messageMeta)); + } else if (isInserting) { + context.missing(_messageMeta); + } + if (data.containsKey('contact_will_sends_receipt')) { + context.handle( + _contactWillSendsReceiptMeta, + contactWillSendsReceipt.isAcceptableOrUnknown( + data['contact_will_sends_receipt']!, + _contactWillSendsReceiptMeta)); + } + if (data.containsKey('retry_count')) { + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown( + data['retry_count']!, _retryCountMeta)); + } + if (data.containsKey('last_retry')) { + context.handle(_lastRetryMeta, + lastRetry.isAcceptableOrUnknown(data['last_retry']!, _lastRetryMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {receiptId}; + @override + Receipt map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Receipt( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), + message: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}message'])!, + contactWillSendsReceipt: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}contact_will_sends_receipt'])!, + retryCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, + lastRetry: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_retry']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReceiptsTable createAlias(String alias) { + return $ReceiptsTable(attachedDatabase, alias); + } +} + +class Receipt extends DataClass implements Insertable { + final String receiptId; + final int contactId; + final String? messageId; + final Uint8List message; + final bool contactWillSendsReceipt; + final int retryCount; + final DateTime? lastRetry; + final DateTime createdAt; + const Receipt( + {required this.receiptId, + required this.contactId, + this.messageId, + required this.message, + required this.contactWillSendsReceipt, + required this.retryCount, + this.lastRetry, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + map['message'] = Variable(message); + map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt); + map['retry_count'] = Variable(retryCount); + if (!nullToAbsent || lastRetry != null) { + map['last_retry'] = Variable(lastRetry); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + message: Value(message), + contactWillSendsReceipt: Value(contactWillSendsReceipt), + retryCount: Value(retryCount), + lastRetry: lastRetry == null && nullToAbsent + ? const Value.absent() + : Value(lastRetry), + createdAt: Value(createdAt), + ); + } + + factory Receipt.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Receipt( + receiptId: serializer.fromJson(json['receiptId']), + contactId: serializer.fromJson(json['contactId']), + messageId: serializer.fromJson(json['messageId']), + message: serializer.fromJson(json['message']), + contactWillSendsReceipt: + serializer.fromJson(json['contactWillSendsReceipt']), + retryCount: serializer.fromJson(json['retryCount']), + lastRetry: serializer.fromJson(json['lastRetry']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'contactId': serializer.toJson(contactId), + 'messageId': serializer.toJson(messageId), + 'message': serializer.toJson(message), + 'contactWillSendsReceipt': + serializer.toJson(contactWillSendsReceipt), + 'retryCount': serializer.toJson(retryCount), + 'lastRetry': serializer.toJson(lastRetry), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Receipt copyWith( + {String? receiptId, + int? contactId, + Value messageId = const Value.absent(), + Uint8List? message, + bool? contactWillSendsReceipt, + int? retryCount, + Value lastRetry = const Value.absent(), + DateTime? createdAt}) => + Receipt( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, + createdAt: createdAt ?? this.createdAt, + ); + Receipt copyWithCompanion(ReceiptsCompanion data) { + return Receipt( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + message: data.message.present ? data.message.value : this.message, + contactWillSendsReceipt: data.contactWillSendsReceipt.present + ? data.contactWillSendsReceipt.value + : this.contactWillSendsReceipt, + retryCount: + data.retryCount.present ? data.retryCount.value : this.retryCount, + lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Receipt(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + receiptId, + contactId, + messageId, + $driftBlobEquality.hash(message), + contactWillSendsReceipt, + retryCount, + lastRetry, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Receipt && + other.receiptId == this.receiptId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals(other.message, this.message) && + other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.retryCount == this.retryCount && + other.lastRetry == this.lastRetry && + other.createdAt == this.createdAt); +} + +class ReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value contactId; + final Value messageId; + final Value message; + final Value contactWillSendsReceipt; + final Value retryCount; + final Value lastRetry; + final Value createdAt; + final Value rowid; + const ReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.message = const Value.absent(), + this.contactWillSendsReceipt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptsCompanion.insert({ + this.receiptId = const Value.absent(), + required int contactId, + this.messageId = const Value.absent(), + required Uint8List message, + this.contactWillSendsReceipt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : contactId = Value(contactId), + message = Value(message); + static Insertable custom({ + Expression? receiptId, + Expression? contactId, + Expression? messageId, + Expression? message, + Expression? contactWillSendsReceipt, + Expression? retryCount, + Expression? lastRetry, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (message != null) 'message': message, + if (contactWillSendsReceipt != null) + 'contact_will_sends_receipt': contactWillSendsReceipt, + if (retryCount != null) 'retry_count': retryCount, + if (lastRetry != null) 'last_retry': lastRetry, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptsCompanion copyWith( + {Value? receiptId, + Value? contactId, + Value? messageId, + Value? message, + Value? contactWillSendsReceipt, + Value? retryCount, + Value? lastRetry, + Value? createdAt, + Value? rowid}) { + return ReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry ?? this.lastRetry, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (message.present) { + map['message'] = Variable(message.value); + } + if (contactWillSendsReceipt.present) { + map['contact_will_sends_receipt'] = + Variable(contactWillSendsReceipt.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + if (lastRetry.present) { + map['last_retry'] = Variable(lastRetry.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores + with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalIdentityKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _deviceIdMeta = + const VerificationMeta('deviceId'); + @override + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _identityKeyMeta = + const VerificationMeta('identityKey'); + @override + late final GeneratedColumn identityKey = + GeneratedColumn('identity_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [deviceId, name, identityKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_identity_key_stores'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('device_id')) { + context.handle(_deviceIdMeta, + deviceId.isAcceptableOrUnknown(data['device_id']!, _deviceIdMeta)); + } else if (isInserting) { + context.missing(_deviceIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('identity_key')) { + context.handle( + _identityKeyMeta, + identityKey.isAcceptableOrUnknown( + data['identity_key']!, _identityKeyMeta)); + } else if (isInserting) { + context.missing(_identityKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalIdentityKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalIdentityKeyStore( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + identityKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}identity_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalIdentityKeyStoresTable createAlias(String alias) { + return $SignalIdentityKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalIdentityKeyStore extends DataClass + implements Insertable { + final int deviceId; + final String name; + final Uint8List identityKey; + final DateTime createdAt; + const SignalIdentityKeyStore( + {required this.deviceId, + required this.name, + required this.identityKey, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['identity_key'] = Variable(identityKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalIdentityKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalIdentityKeyStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + identityKey: Value(identityKey), + createdAt: Value(createdAt), + ); + } + + factory SignalIdentityKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalIdentityKeyStore( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + identityKey: serializer.fromJson(json['identityKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'identityKey': serializer.toJson(identityKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalIdentityKeyStore copyWith( + {int? deviceId, + String? name, + Uint8List? identityKey, + DateTime? createdAt}) => + SignalIdentityKeyStore( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalIdentityKeyStore copyWithCompanion( + SignalIdentityKeyStoresCompanion data) { + return SignalIdentityKeyStore( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + identityKey: + data.identityKey.present ? data.identityKey.value : this.identityKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStore(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(identityKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalIdentityKeyStore && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.identityKey, this.identityKey) && + other.createdAt == this.createdAt); +} + +class SignalIdentityKeyStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value identityKey; + final Value createdAt; + final Value rowid; + const SignalIdentityKeyStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.identityKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalIdentityKeyStoresCompanion.insert({ + required int deviceId, + required String name, + required Uint8List identityKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + identityKey = Value(identityKey); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? identityKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (identityKey != null) 'identity_key': identityKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalIdentityKeyStoresCompanion copyWith( + {Value? deviceId, + Value? name, + Value? identityKey, + Value? createdAt, + Value? rowid}) { + return SignalIdentityKeyStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (identityKey.present) { + map['identity_key'] = Variable(identityKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalPreKeyStoresTable extends SignalPreKeyStores + with TableInfo<$SignalPreKeyStoresTable, SignalPreKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalPreKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _preKeyIdMeta = + const VerificationMeta('preKeyId'); + @override + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _preKeyMeta = const VerificationMeta('preKey'); + @override + late final GeneratedColumn preKey = GeneratedColumn( + 'pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_pre_key_stores'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('pre_key_id')) { + context.handle(_preKeyIdMeta, + preKeyId.isAcceptableOrUnknown(data['pre_key_id']!, _preKeyIdMeta)); + } + if (data.containsKey('pre_key')) { + context.handle(_preKeyMeta, + preKey.isAcceptableOrUnknown(data['pre_key']!, _preKeyMeta)); + } else if (isInserting) { + context.missing(_preKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {preKeyId}; + @override + SignalPreKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalPreKeyStore( + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalPreKeyStoresTable createAlias(String alias) { + return $SignalPreKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalPreKeyStore extends DataClass + implements Insertable { + final int preKeyId; + final Uint8List preKey; + final DateTime createdAt; + const SignalPreKeyStore( + {required this.preKeyId, required this.preKey, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalPreKeyStoresCompanion( + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalPreKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalPreKeyStore( + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalPreKeyStore copyWith( + {int? preKeyId, Uint8List? preKey, DateTime? createdAt}) => + SignalPreKeyStore( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalPreKeyStore copyWithCompanion(SignalPreKeyStoresCompanion data) { + return SignalPreKeyStore( + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStore(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalPreKeyStore && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalPreKeyStoresCompanion extends UpdateCompanion { + final Value preKeyId; + final Value preKey; + final Value createdAt; + const SignalPreKeyStoresCompanion({ + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalPreKeyStoresCompanion.insert({ + this.preKeyId = const Value.absent(), + required Uint8List preKey, + this.createdAt = const Value.absent(), + }) : preKey = Value(preKey); + static Insertable custom({ + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalPreKeyStoresCompanion copyWith( + {Value? preKeyId, + Value? preKey, + Value? createdAt}) { + return SignalPreKeyStoresCompanion( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresCompanion(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $SignalSenderKeyStoresTable extends SignalSenderKeyStores + with TableInfo<$SignalSenderKeyStoresTable, SignalSenderKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalSenderKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _senderKeyNameMeta = + const VerificationMeta('senderKeyName'); + @override + late final GeneratedColumn senderKeyName = GeneratedColumn( + 'sender_key_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderKeyMeta = + const VerificationMeta('senderKey'); + @override + late final GeneratedColumn senderKey = GeneratedColumn( + 'sender_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [senderKeyName, senderKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_sender_key_stores'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('sender_key_name')) { + context.handle( + _senderKeyNameMeta, + senderKeyName.isAcceptableOrUnknown( + data['sender_key_name']!, _senderKeyNameMeta)); + } else if (isInserting) { + context.missing(_senderKeyNameMeta); + } + if (data.containsKey('sender_key')) { + context.handle(_senderKeyMeta, + senderKey.isAcceptableOrUnknown(data['sender_key']!, _senderKeyMeta)); + } else if (isInserting) { + context.missing(_senderKeyMeta); + } + return context; + } + + @override + Set get $primaryKey => {senderKeyName}; + @override + SignalSenderKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSenderKeyStore( + senderKeyName: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}sender_key_name'])!, + senderKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}sender_key'])!, + ); + } + + @override + $SignalSenderKeyStoresTable createAlias(String alias) { + return $SignalSenderKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalSenderKeyStore extends DataClass + implements Insertable { + final String senderKeyName; + final Uint8List senderKey; + const SignalSenderKeyStore( + {required this.senderKeyName, required this.senderKey}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['sender_key_name'] = Variable(senderKeyName); + map['sender_key'] = Variable(senderKey); + return map; + } + + SignalSenderKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSenderKeyStoresCompanion( + senderKeyName: Value(senderKeyName), + senderKey: Value(senderKey), + ); + } + + factory SignalSenderKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSenderKeyStore( + senderKeyName: serializer.fromJson(json['senderKeyName']), + senderKey: serializer.fromJson(json['senderKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'senderKeyName': serializer.toJson(senderKeyName), + 'senderKey': serializer.toJson(senderKey), + }; + } + + SignalSenderKeyStore copyWith( + {String? senderKeyName, Uint8List? senderKey}) => + SignalSenderKeyStore( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + ); + SignalSenderKeyStore copyWithCompanion(SignalSenderKeyStoresCompanion data) { + return SignalSenderKeyStore( + senderKeyName: data.senderKeyName.present + ? data.senderKeyName.value + : this.senderKeyName, + senderKey: data.senderKey.present ? data.senderKey.value : this.senderKey, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStore(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(senderKeyName, $driftBlobEquality.hash(senderKey)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSenderKeyStore && + other.senderKeyName == this.senderKeyName && + $driftBlobEquality.equals(other.senderKey, this.senderKey)); +} + +class SignalSenderKeyStoresCompanion + extends UpdateCompanion { + final Value senderKeyName; + final Value senderKey; + final Value rowid; + const SignalSenderKeyStoresCompanion({ + this.senderKeyName = const Value.absent(), + this.senderKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSenderKeyStoresCompanion.insert({ + required String senderKeyName, + required Uint8List senderKey, + this.rowid = const Value.absent(), + }) : senderKeyName = Value(senderKeyName), + senderKey = Value(senderKey); + static Insertable custom({ + Expression? senderKeyName, + Expression? senderKey, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (senderKeyName != null) 'sender_key_name': senderKeyName, + if (senderKey != null) 'sender_key': senderKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSenderKeyStoresCompanion copyWith( + {Value? senderKeyName, + Value? senderKey, + Value? rowid}) { + return SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (senderKeyName.present) { + map['sender_key_name'] = Variable(senderKeyName.value); + } + if (senderKey.present) { + map['sender_key'] = Variable(senderKey.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresCompanion(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalSessionStoresTable extends SignalSessionStores + with TableInfo<$SignalSessionStoresTable, SignalSessionStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalSessionStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _deviceIdMeta = + const VerificationMeta('deviceId'); + @override + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sessionRecordMeta = + const VerificationMeta('sessionRecord'); + @override + late final GeneratedColumn sessionRecord = + GeneratedColumn('session_record', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [deviceId, name, sessionRecord, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_session_stores'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('device_id')) { + context.handle(_deviceIdMeta, + deviceId.isAcceptableOrUnknown(data['device_id']!, _deviceIdMeta)); + } else if (isInserting) { + context.missing(_deviceIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('session_record')) { + context.handle( + _sessionRecordMeta, + sessionRecord.isAcceptableOrUnknown( + data['session_record']!, _sessionRecordMeta)); + } else if (isInserting) { + context.missing(_sessionRecordMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalSessionStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSessionStore( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + sessionRecord: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}session_record'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalSessionStoresTable createAlias(String alias) { + return $SignalSessionStoresTable(attachedDatabase, alias); + } +} + +class SignalSessionStore extends DataClass + implements Insertable { + final int deviceId; + final String name; + final Uint8List sessionRecord; + final DateTime createdAt; + const SignalSessionStore( + {required this.deviceId, + required this.name, + required this.sessionRecord, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['session_record'] = Variable(sessionRecord); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSessionStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSessionStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + sessionRecord: Value(sessionRecord), + createdAt: Value(createdAt), + ); + } + + factory SignalSessionStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSessionStore( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + sessionRecord: serializer.fromJson(json['sessionRecord']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'sessionRecord': serializer.toJson(sessionRecord), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSessionStore copyWith( + {int? deviceId, + String? name, + Uint8List? sessionRecord, + DateTime? createdAt}) => + SignalSessionStore( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + ); + SignalSessionStore copyWithCompanion(SignalSessionStoresCompanion data) { + return SignalSessionStore( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + sessionRecord: data.sessionRecord.present + ? data.sessionRecord.value + : this.sessionRecord, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSessionStore(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(sessionRecord), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSessionStore && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.sessionRecord, this.sessionRecord) && + other.createdAt == this.createdAt); +} + +class SignalSessionStoresCompanion extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value sessionRecord; + final Value createdAt; + final Value rowid; + const SignalSessionStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.sessionRecord = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSessionStoresCompanion.insert({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + sessionRecord = Value(sessionRecord); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? sessionRecord, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (sessionRecord != null) 'session_record': sessionRecord, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSessionStoresCompanion copyWith( + {Value? deviceId, + Value? name, + Value? sessionRecord, + Value? createdAt, + Value? rowid}) { + return SignalSessionStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (sessionRecord.present) { + map['session_record'] = Variable(sessionRecord.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalContactPreKeysTable extends SignalContactPreKeys + with TableInfo<$SignalContactPreKeysTable, SignalContactPreKey> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalContactPreKeysTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _preKeyIdMeta = + const VerificationMeta('preKeyId'); + @override + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _preKeyMeta = const VerificationMeta('preKey'); + @override + late final GeneratedColumn preKey = GeneratedColumn( + 'pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [contactId, preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_pre_keys'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('pre_key_id')) { + context.handle(_preKeyIdMeta, + preKeyId.isAcceptableOrUnknown(data['pre_key_id']!, _preKeyIdMeta)); + } else if (isInserting) { + context.missing(_preKeyIdMeta); + } + if (data.containsKey('pre_key')) { + context.handle(_preKeyMeta, + preKey.isAcceptableOrUnknown(data['pre_key']!, _preKeyMeta)); + } else if (isInserting) { + context.missing(_preKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {contactId, preKeyId}; + @override + SignalContactPreKey map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactPreKey( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalContactPreKeysTable createAlias(String alias) { + return $SignalContactPreKeysTable(attachedDatabase, alias); + } +} + +class SignalContactPreKey extends DataClass + implements Insertable { + final int contactId; + final int preKeyId; + final Uint8List preKey; + final DateTime createdAt; + const SignalContactPreKey( + {required this.contactId, + required this.preKeyId, + required this.preKey, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['contact_id'] = Variable(contactId); + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalContactPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactPreKeysCompanion( + contactId: Value(contactId), + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalContactPreKey.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactPreKey( + contactId: serializer.fromJson(json['contactId']), + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'contactId': serializer.toJson(contactId), + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalContactPreKey copyWith( + {int? contactId, + int? preKeyId, + Uint8List? preKey, + DateTime? createdAt}) => + SignalContactPreKey( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactPreKey copyWithCompanion(SignalContactPreKeysCompanion data) { + return SignalContactPreKey( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKey(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactPreKey && + other.contactId == this.contactId && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalContactPreKeysCompanion + extends UpdateCompanion { + final Value contactId; + final Value preKeyId; + final Value preKey; + final Value createdAt; + final Value rowid; + const SignalContactPreKeysCompanion({ + this.contactId = const Value.absent(), + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalContactPreKeysCompanion.insert({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : contactId = Value(contactId), + preKeyId = Value(preKeyId), + preKey = Value(preKey); + static Insertable custom({ + Expression? contactId, + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalContactPreKeysCompanion copyWith( + {Value? contactId, + Value? preKeyId, + Value? preKey, + Value? createdAt, + Value? rowid}) { + return SignalContactPreKeysCompanion( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalContactSignedPreKeysTable extends SignalContactSignedPreKeys + with + TableInfo<$SignalContactSignedPreKeysTable, SignalContactSignedPreKey> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalContactSignedPreKeysTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _signedPreKeyIdMeta = + const VerificationMeta('signedPreKeyId'); + @override + late final GeneratedColumn signedPreKeyId = GeneratedColumn( + 'signed_pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _signedPreKeyMeta = + const VerificationMeta('signedPreKey'); + @override + late final GeneratedColumn signedPreKey = + GeneratedColumn('signed_pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _signedPreKeySignatureMeta = + const VerificationMeta('signedPreKeySignature'); + @override + late final GeneratedColumn signedPreKeySignature = + GeneratedColumn('signed_pre_key_signature', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + contactId, + signedPreKeyId, + signedPreKey, + signedPreKeySignature, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_signed_pre_keys'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } + if (data.containsKey('signed_pre_key_id')) { + context.handle( + _signedPreKeyIdMeta, + signedPreKeyId.isAcceptableOrUnknown( + data['signed_pre_key_id']!, _signedPreKeyIdMeta)); + } else if (isInserting) { + context.missing(_signedPreKeyIdMeta); + } + if (data.containsKey('signed_pre_key')) { + context.handle( + _signedPreKeyMeta, + signedPreKey.isAcceptableOrUnknown( + data['signed_pre_key']!, _signedPreKeyMeta)); + } else if (isInserting) { + context.missing(_signedPreKeyMeta); + } + if (data.containsKey('signed_pre_key_signature')) { + context.handle( + _signedPreKeySignatureMeta, + signedPreKeySignature.isAcceptableOrUnknown( + data['signed_pre_key_signature']!, _signedPreKeySignatureMeta)); + } else if (isInserting) { + context.missing(_signedPreKeySignatureMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {contactId}; + @override + SignalContactSignedPreKey map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactSignedPreKey( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + signedPreKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}signed_pre_key_id'])!, + signedPreKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}signed_pre_key'])!, + signedPreKeySignature: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}signed_pre_key_signature'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalContactSignedPreKeysTable createAlias(String alias) { + return $SignalContactSignedPreKeysTable(attachedDatabase, alias); + } +} + +class SignalContactSignedPreKey extends DataClass + implements Insertable { + final int contactId; + final int signedPreKeyId; + final Uint8List signedPreKey; + final Uint8List signedPreKeySignature; + final DateTime createdAt; + const SignalContactSignedPreKey( + {required this.contactId, + required this.signedPreKeyId, + required this.signedPreKey, + required this.signedPreKeySignature, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['contact_id'] = Variable(contactId); + map['signed_pre_key_id'] = Variable(signedPreKeyId); + map['signed_pre_key'] = Variable(signedPreKey); + map['signed_pre_key_signature'] = + Variable(signedPreKeySignature); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalContactSignedPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactSignedPreKeysCompanion( + contactId: Value(contactId), + signedPreKeyId: Value(signedPreKeyId), + signedPreKey: Value(signedPreKey), + signedPreKeySignature: Value(signedPreKeySignature), + createdAt: Value(createdAt), + ); + } + + factory SignalContactSignedPreKey.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactSignedPreKey( + contactId: serializer.fromJson(json['contactId']), + signedPreKeyId: serializer.fromJson(json['signedPreKeyId']), + signedPreKey: serializer.fromJson(json['signedPreKey']), + signedPreKeySignature: + serializer.fromJson(json['signedPreKeySignature']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'contactId': serializer.toJson(contactId), + 'signedPreKeyId': serializer.toJson(signedPreKeyId), + 'signedPreKey': serializer.toJson(signedPreKey), + 'signedPreKeySignature': + serializer.toJson(signedPreKeySignature), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalContactSignedPreKey copyWith( + {int? contactId, + int? signedPreKeyId, + Uint8List? signedPreKey, + Uint8List? signedPreKeySignature, + DateTime? createdAt}) => + SignalContactSignedPreKey( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactSignedPreKey copyWithCompanion( + SignalContactSignedPreKeysCompanion data) { + return SignalContactSignedPreKey( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + signedPreKeyId: data.signedPreKeyId.present + ? data.signedPreKeyId.value + : this.signedPreKeyId, + signedPreKey: data.signedPreKey.present + ? data.signedPreKey.value + : this.signedPreKey, + signedPreKeySignature: data.signedPreKeySignature.present + ? data.signedPreKeySignature.value + : this.signedPreKeySignature, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKey(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, + signedPreKeyId, + $driftBlobEquality.hash(signedPreKey), + $driftBlobEquality.hash(signedPreKeySignature), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactSignedPreKey && + other.contactId == this.contactId && + other.signedPreKeyId == this.signedPreKeyId && + $driftBlobEquality.equals(other.signedPreKey, this.signedPreKey) && + $driftBlobEquality.equals( + other.signedPreKeySignature, this.signedPreKeySignature) && + other.createdAt == this.createdAt); +} + +class SignalContactSignedPreKeysCompanion + extends UpdateCompanion { + final Value contactId; + final Value signedPreKeyId; + final Value signedPreKey; + final Value signedPreKeySignature; + final Value createdAt; + const SignalContactSignedPreKeysCompanion({ + this.contactId = const Value.absent(), + this.signedPreKeyId = const Value.absent(), + this.signedPreKey = const Value.absent(), + this.signedPreKeySignature = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalContactSignedPreKeysCompanion.insert({ + this.contactId = const Value.absent(), + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + this.createdAt = const Value.absent(), + }) : signedPreKeyId = Value(signedPreKeyId), + signedPreKey = Value(signedPreKey), + signedPreKeySignature = Value(signedPreKeySignature); + static Insertable custom({ + Expression? contactId, + Expression? signedPreKeyId, + Expression? signedPreKey, + Expression? signedPreKeySignature, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (signedPreKeyId != null) 'signed_pre_key_id': signedPreKeyId, + if (signedPreKey != null) 'signed_pre_key': signedPreKey, + if (signedPreKeySignature != null) + 'signed_pre_key_signature': signedPreKeySignature, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalContactSignedPreKeysCompanion copyWith( + {Value? contactId, + Value? signedPreKeyId, + Value? signedPreKey, + Value? signedPreKeySignature, + Value? createdAt}) { + return SignalContactSignedPreKeysCompanion( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (signedPreKeyId.present) { + map['signed_pre_key_id'] = Variable(signedPreKeyId.value); + } + if (signedPreKey.present) { + map['signed_pre_key'] = Variable(signedPreKey.value); + } + if (signedPreKeySignature.present) { + map['signed_pre_key_signature'] = + Variable(signedPreKeySignature.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +abstract class _$TwonlyDB extends GeneratedDatabase { + _$TwonlyDB(QueryExecutor e) : super(e); + $TwonlyDBManager get managers => $TwonlyDBManager(this); + late final $ContactsTable contacts = $ContactsTable(this); + late final $MediaFilesTable mediaFiles = $MediaFilesTable(this); + late final $MessagesTable messages = $MessagesTable(this); + late final $MessageHistoriesTable messageHistories = + $MessageHistoriesTable(this); + late final $ReactionsTable reactions = $ReactionsTable(this); + late final $GroupsTable groups = $GroupsTable(this); + late final $GroupMembersTable groupMembers = $GroupMembersTable(this); + late final $ReceiptsTable receipts = $ReceiptsTable(this); + late final $SignalIdentityKeyStoresTable signalIdentityKeyStores = + $SignalIdentityKeyStoresTable(this); + late final $SignalPreKeyStoresTable signalPreKeyStores = + $SignalPreKeyStoresTable(this); + late final $SignalSenderKeyStoresTable signalSenderKeyStores = + $SignalSenderKeyStoresTable(this); + late final $SignalSessionStoresTable signalSessionStores = + $SignalSessionStoresTable(this); + late final $SignalContactPreKeysTable signalContactPreKeys = + $SignalContactPreKeysTable(this); + late final $SignalContactSignedPreKeysTable signalContactSignedPreKeys = + $SignalContactSignedPreKeysTable(this); + late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); + late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); + late final SignalDao signalDao = SignalDao(this as TwonlyDB); + late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB); + late final GroupsDao groupsDao = GroupsDao(this as TwonlyDB); + late final ReactionsDao reactionsDao = ReactionsDao(this as TwonlyDB); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + contacts, + mediaFiles, + messages, + messageHistories, + reactions, + groups, + groupMembers, + receipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalContactPreKeys, + signalContactSignedPreKeys + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_histories', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('receipts', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('receipts', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('signal_contact_pre_keys', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('signal_contact_signed_pre_keys', + kind: UpdateKind.delete), + ], + ), + ], + ); +} + +typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ + Value userId, + required String username, + Value displayName, + Value nickName, + Value avatarSvg, + Value senderProfileCounter, + Value accepted, + Value requested, + Value hidden, + Value blocked, + Value verified, + Value archived, + Value deleted, + Value alsoBestFriend, + Value deleteMessagesAfterXMinutes, + Value createdAt, + Value totalMediaCounter, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, +}); +typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ + Value userId, + Value username, + Value displayName, + Value nickName, + Value avatarSvg, + Value senderProfileCounter, + Value accepted, + Value requested, + Value hidden, + Value blocked, + Value verified, + Value archived, + Value deleted, + Value alsoBestFriend, + Value deleteMessagesAfterXMinutes, + Value createdAt, + Value totalMediaCounter, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, +}); + +final class $$ContactsTableReferences + extends BaseReferences<_$TwonlyDB, $ContactsTable, Contact> { + $$ContactsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.messages.senderId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.senderId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReactionsTable, List> + _reactionsRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.reactions, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.reactions.senderId)); + + $$ReactionsTableProcessedTableManager get reactionsRefs { + final manager = $$ReactionsTableTableManager($_db, $_db.reactions).filter( + (f) => f.senderId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$GroupMembersTable, List> + _groupMembersRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupMembers, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.groupMembers.contactId)); + + $$GroupMembersTableProcessedTableManager get groupMembersRefs { + final manager = $$GroupMembersTableTableManager($_db, $_db.groupMembers) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupMembersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReceiptsTable, List> _receiptsRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.receipts, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.receipts.contactId)); + + $$ReceiptsTableProcessedTableManager get receiptsRefs { + final manager = $$ReceiptsTableTableManager($_db, $_db.receipts).filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_receiptsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$SignalContactPreKeysTable, + List> _signalContactPreKeysRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.signalContactPreKeys, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.signalContactPreKeys.contactId)); + + $$SignalContactPreKeysTableProcessedTableManager + get signalContactPreKeysRefs { + final manager = $$SignalContactPreKeysTableTableManager( + $_db, $_db.signalContactPreKeys) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = + $_typedResult.readTableOrNull(_signalContactPreKeysRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$SignalContactSignedPreKeysTable, + List> _signalContactSignedPreKeysRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.signalContactSignedPreKeys, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.signalContactSignedPreKeys.contactId)); + + $$SignalContactSignedPreKeysTableProcessedTableManager + get signalContactSignedPreKeysRefs { + final manager = $$SignalContactSignedPreKeysTableTableManager( + $_db, $_db.signalContactSignedPreKeys) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult + .readTableOrNull(_signalContactSignedPreKeysRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$ContactsTableFilterComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnFilters(column)); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get nickName => $composableBuilder( + column: $table.nickName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get avatarSvg => $composableBuilder( + column: $table.avatarSvg, builder: (column) => ColumnFilters(column)); + + ColumnFilters get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get accepted => $composableBuilder( + column: $table.accepted, builder: (column) => ColumnFilters(column)); + + ColumnFilters get requested => $composableBuilder( + column: $table.requested, builder: (column) => ColumnFilters(column)); + + ColumnFilters get hidden => $composableBuilder( + column: $table.hidden, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnFilters(column)); + + ColumnFilters get verified => $composableBuilder( + column: $table.verified, builder: (column) => ColumnFilters(column)); + + ColumnFilters get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnFilters(column)); + + ColumnFilters get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get deleteMessagesAfterXMinutes => $composableBuilder( + column: $table.deleteMessagesAfterXMinutes, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => ColumnFilters(column)); + + ColumnFilters get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableFilterComposer f) f) { + final $$ReactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableFilterComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableFilterComposer f) f) { + final $$GroupMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableFilterComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableFilterComposer f) f) { + final $$ReceiptsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableFilterComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactPreKeysRefs( + Expression Function($$SignalContactPreKeysTableFilterComposer f) + f) { + final $$SignalContactPreKeysTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactPreKeysTableFilterComposer( + $db: $db, + $table: $db.signalContactPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactSignedPreKeysRefs( + Expression Function( + $$SignalContactSignedPreKeysTableFilterComposer f) + f) { + final $$SignalContactSignedPreKeysTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactSignedPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactSignedPreKeysTableFilterComposer( + $db: $db, + $table: $db.signalContactSignedPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$ContactsTableOrderingComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get nickName => $composableBuilder( + column: $table.nickName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get avatarSvg => $composableBuilder( + column: $table.avatarSvg, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get accepted => $composableBuilder( + column: $table.accepted, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get requested => $composableBuilder( + column: $table.requested, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get hidden => $composableBuilder( + column: $table.hidden, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get verified => $composableBuilder( + column: $table.verified, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deleteMessagesAfterXMinutes => $composableBuilder( + column: $table.deleteMessagesAfterXMinutes, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get flameCounter => $composableBuilder( + column: $table.flameCounter, + builder: (column) => ColumnOrderings(column)); +} + +class $$ContactsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get username => + $composableBuilder(column: $table.username, builder: (column) => column); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => column); + + GeneratedColumn get nickName => + $composableBuilder(column: $table.nickName, builder: (column) => column); + + GeneratedColumn get avatarSvg => + $composableBuilder(column: $table.avatarSvg, builder: (column) => column); + + GeneratedColumn get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, builder: (column) => column); + + GeneratedColumn get accepted => + $composableBuilder(column: $table.accepted, builder: (column) => column); + + GeneratedColumn get requested => + $composableBuilder(column: $table.requested, builder: (column) => column); + + GeneratedColumn get hidden => + $composableBuilder(column: $table.hidden, builder: (column) => column); + + GeneratedColumn get blocked => + $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get verified => + $composableBuilder(column: $table.verified, builder: (column) => column); + + GeneratedColumn get archived => + $composableBuilder(column: $table.archived, builder: (column) => column); + + GeneratedColumn get deleted => + $composableBuilder(column: $table.deleted, builder: (column) => column); + + GeneratedColumn get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, builder: (column) => column); + + GeneratedColumn get deleteMessagesAfterXMinutes => $composableBuilder( + column: $table.deleteMessagesAfterXMinutes, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, builder: (column) => column); + + GeneratedColumn get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, builder: (column) => column); + + GeneratedColumn get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, builder: (column) => column); + + GeneratedColumn get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, builder: (column) => column); + + GeneratedColumn get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => column); + + GeneratedColumn get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableAnnotationComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableAnnotationComposer a) f) { + final $$GroupMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableAnnotationComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableAnnotationComposer a) f) { + final $$ReceiptsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableAnnotationComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactPreKeysRefs( + Expression Function($$SignalContactPreKeysTableAnnotationComposer a) + f) { + final $$SignalContactPreKeysTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactPreKeysTableAnnotationComposer( + $db: $db, + $table: $db.signalContactPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactSignedPreKeysRefs( + Expression Function( + $$SignalContactSignedPreKeysTableAnnotationComposer a) + f) { + final $$SignalContactSignedPreKeysTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactSignedPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactSignedPreKeysTableAnnotationComposer( + $db: $db, + $table: $db.signalContactSignedPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$ContactsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ContactsTable, + Contact, + $$ContactsTableFilterComposer, + $$ContactsTableOrderingComposer, + $$ContactsTableAnnotationComposer, + $$ContactsTableCreateCompanionBuilder, + $$ContactsTableUpdateCompanionBuilder, + (Contact, $$ContactsTableReferences), + Contact, + PrefetchHooks Function( + {bool messagesRefs, + bool reactionsRefs, + bool groupMembersRefs, + bool receiptsRefs, + bool signalContactPreKeysRefs, + bool signalContactSignedPreKeysRefs})> { + $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ContactsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ContactsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ContactsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value userId = const Value.absent(), + Value username = const Value.absent(), + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + Value senderProfileCounter = const Value.absent(), + Value accepted = const Value.absent(), + Value requested = const Value.absent(), + Value hidden = const Value.absent(), + Value blocked = const Value.absent(), + Value verified = const Value.absent(), + Value archived = const Value.absent(), + Value deleted = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterXMinutes = const Value.absent(), + Value createdAt = const Value.absent(), + Value totalMediaCounter = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + }) => + ContactsCompanion( + userId: userId, + username: username, + displayName: displayName, + nickName: nickName, + avatarSvg: avatarSvg, + senderProfileCounter: senderProfileCounter, + accepted: accepted, + requested: requested, + hidden: hidden, + blocked: blocked, + verified: verified, + archived: archived, + deleted: deleted, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, + createdAt: createdAt, + totalMediaCounter: totalMediaCounter, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + ), + createCompanionCallback: ({ + Value userId = const Value.absent(), + required String username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + Value senderProfileCounter = const Value.absent(), + Value accepted = const Value.absent(), + Value requested = const Value.absent(), + Value hidden = const Value.absent(), + Value blocked = const Value.absent(), + Value verified = const Value.absent(), + Value archived = const Value.absent(), + Value deleted = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterXMinutes = const Value.absent(), + Value createdAt = const Value.absent(), + Value totalMediaCounter = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + }) => + ContactsCompanion.insert( + userId: userId, + username: username, + displayName: displayName, + nickName: nickName, + avatarSvg: avatarSvg, + senderProfileCounter: senderProfileCounter, + accepted: accepted, + requested: requested, + hidden: hidden, + blocked: blocked, + verified: verified, + archived: archived, + deleted: deleted, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, + createdAt: createdAt, + totalMediaCounter: totalMediaCounter, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$ContactsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {messagesRefs = false, + reactionsRefs = false, + groupMembersRefs = false, + receiptsRefs = false, + signalContactPreKeysRefs = false, + signalContactSignedPreKeysRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (reactionsRefs) db.reactions, + if (groupMembersRefs) db.groupMembers, + if (receiptsRefs) db.receipts, + if (signalContactPreKeysRefs) db.signalContactPreKeys, + if (signalContactSignedPreKeysRefs) + db.signalContactSignedPreKeys + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.senderId == item.userId), + typedResults: items), + if (reactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._reactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .reactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.senderId == item.userId), + typedResults: items), + if (groupMembersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._groupMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .groupMembersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (receiptsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._receiptsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .receiptsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (signalContactPreKeysRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._signalContactPreKeysRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .signalContactPreKeysRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (signalContactSignedPreKeysRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._signalContactSignedPreKeysRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .signalContactSignedPreKeysRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ContactsTable, + Contact, + $$ContactsTableFilterComposer, + $$ContactsTableOrderingComposer, + $$ContactsTableAnnotationComposer, + $$ContactsTableCreateCompanionBuilder, + $$ContactsTableUpdateCompanionBuilder, + (Contact, $$ContactsTableReferences), + Contact, + PrefetchHooks Function( + {bool messagesRefs, + bool reactionsRefs, + bool groupMembersRefs, + bool receiptsRefs, + bool signalContactPreKeysRefs, + bool signalContactSignedPreKeysRefs})>; +typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ + Value mediaId, + required MediaType type, + Value uploadState, + Value downloadState, + required bool requiresAuthentication, + Value reopenByContact, + Value storedByContact, + Value displayLimitInMilliseconds, + Value downloadToken, + Value encryptionKey, + Value encryptionMac, + Value encryptionNonce, + Value createdAt, + Value rowid, +}); +typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ + Value mediaId, + Value type, + Value uploadState, + Value downloadState, + Value requiresAuthentication, + Value reopenByContact, + Value storedByContact, + Value displayLimitInMilliseconds, + Value downloadToken, + Value encryptionKey, + Value encryptionMac, + Value encryptionNonce, + Value createdAt, + Value rowid, +}); + +final class $$MediaFilesTableReferences + extends BaseReferences<_$TwonlyDB, $MediaFilesTable, MediaFile> { + $$MediaFilesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.mediaFiles.mediaId, db.messages.mediaId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.mediaId.mediaId.sqlEquals($_itemColumn('media_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$MediaFilesTableFilterComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get mediaId => $composableBuilder( + column: $table.mediaId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get uploadState => $composableBuilder( + column: $table.uploadState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get downloadState => $composableBuilder( + column: $table.downloadState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get reopenByContact => $composableBuilder( + column: $table.reopenByContact, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get storedByContact => $composableBuilder( + column: $table.storedByContact, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionKey => $composableBuilder( + column: $table.encryptionKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionMac => $composableBuilder( + column: $table.encryptionMac, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MediaFilesTableOrderingComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get mediaId => $composableBuilder( + column: $table.mediaId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get uploadState => $composableBuilder( + column: $table.uploadState, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get downloadState => $composableBuilder( + column: $table.downloadState, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get reopenByContact => $composableBuilder( + column: $table.reopenByContact, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get storedByContact => $composableBuilder( + column: $table.storedByContact, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get downloadToken => $composableBuilder( + column: $table.downloadToken, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionKey => $composableBuilder( + column: $table.encryptionKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionMac => $composableBuilder( + column: $table.encryptionMac, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$MediaFilesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get mediaId => + $composableBuilder(column: $table.mediaId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumnWithTypeConverter get uploadState => + $composableBuilder( + column: $table.uploadState, builder: (column) => column); + + GeneratedColumnWithTypeConverter get downloadState => + $composableBuilder( + column: $table.downloadState, builder: (column) => column); + + GeneratedColumn get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, builder: (column) => column); + + GeneratedColumn get reopenByContact => $composableBuilder( + column: $table.reopenByContact, builder: (column) => column); + + GeneratedColumn get storedByContact => $composableBuilder( + column: $table.storedByContact, builder: (column) => column); + + GeneratedColumn get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, builder: (column) => column); + + GeneratedColumn get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => column); + + GeneratedColumn get encryptionKey => $composableBuilder( + column: $table.encryptionKey, builder: (column) => column); + + GeneratedColumn get encryptionMac => $composableBuilder( + column: $table.encryptionMac, builder: (column) => column); + + GeneratedColumn get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MediaFilesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, $$MediaFilesTableReferences), + MediaFile, + PrefetchHooks Function({bool messagesRefs})> { + $$MediaFilesTableTableManager(_$TwonlyDB db, $MediaFilesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MediaFilesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MediaFilesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MediaFilesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value mediaId = const Value.absent(), + Value type = const Value.absent(), + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + Value requiresAuthentication = const Value.absent(), + Value reopenByContact = const Value.absent(), + Value storedByContact = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MediaFilesCompanion( + mediaId: mediaId, + type: type, + uploadState: uploadState, + downloadState: downloadState, + requiresAuthentication: requiresAuthentication, + reopenByContact: reopenByContact, + storedByContact: storedByContact, + displayLimitInMilliseconds: displayLimitInMilliseconds, + downloadToken: downloadToken, + encryptionKey: encryptionKey, + encryptionMac: encryptionMac, + encryptionNonce: encryptionNonce, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + Value mediaId = const Value.absent(), + required MediaType type, + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + required bool requiresAuthentication, + Value reopenByContact = const Value.absent(), + Value storedByContact = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MediaFilesCompanion.insert( + mediaId: mediaId, + type: type, + uploadState: uploadState, + downloadState: downloadState, + requiresAuthentication: requiresAuthentication, + reopenByContact: reopenByContact, + storedByContact: storedByContact, + displayLimitInMilliseconds: displayLimitInMilliseconds, + downloadToken: downloadToken, + encryptionKey: encryptionKey, + encryptionMac: encryptionMac, + encryptionNonce: encryptionNonce, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MediaFilesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messagesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (messagesRefs) db.messages], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MediaFilesTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$MediaFilesTableReferences(db, table, p0) + .messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.mediaId == item.mediaId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, $$MediaFilesTableReferences), + MediaFile, + PrefetchHooks Function({bool messagesRefs})>; +typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ + required String groupId, + required String messageId, + Value senderId, + Value content, + Value mediaId, + Value quotesMessageId, + Value isDeletedFromSender, + Value isEdited, + Value acknowledgeByUser, + Value acknowledgeByServer, + Value openedByCounter, + Value openedAt, + Value createdAt, + Value modifiedAt, + Value rowid, +}); +typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ + Value groupId, + Value messageId, + Value senderId, + Value content, + Value mediaId, + Value quotesMessageId, + Value isDeletedFromSender, + Value isEdited, + Value acknowledgeByUser, + Value acknowledgeByServer, + Value openedByCounter, + Value openedAt, + Value createdAt, + Value modifiedAt, + Value rowid, +}); + +final class $$MessagesTableReferences + extends BaseReferences<_$TwonlyDB, $MessagesTable, Message> { + $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _senderIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.messages.senderId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get senderId { + final $_column = $_itemColumn('sender_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_senderIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MediaFilesTable _mediaIdTable(_$TwonlyDB db) => + db.mediaFiles.createAlias( + $_aliasNameGenerator(db.messages.mediaId, db.mediaFiles.mediaId)); + + $$MediaFilesTableProcessedTableManager? get mediaId { + final $_column = $_itemColumn('media_id'); + if ($_column == null) return null; + final manager = $$MediaFilesTableTableManager($_db, $_db.mediaFiles) + .filter((f) => f.mediaId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_mediaIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MessagesTable _quotesMessageIdTable(_$TwonlyDB db) => + db.messages.createAlias($_aliasNameGenerator( + db.messages.quotesMessageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager? get quotesMessageId { + final $_column = $_itemColumn('quotes_message_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_quotesMessageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static MultiTypedResultKey<$MessageHistoriesTable, List> + _messageHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageHistories, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.messageHistories.messageId)); + + $$MessageHistoriesTableProcessedTableManager get messageHistoriesRefs { + final manager = + $$MessageHistoriesTableTableManager($_db, $_db.messageHistories).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = + $_typedResult.readTableOrNull(_messageHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReactionsTable, List> + _reactionsRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.reactions, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.reactions.messageId)); + + $$ReactionsTableProcessedTableManager get reactionsRefs { + final manager = $$ReactionsTableTableManager($_db, $_db.reactions).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReceiptsTable, List> _receiptsRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.receipts, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.receipts.messageId)); + + $$ReceiptsTableProcessedTableManager get receiptsRefs { + final manager = $$ReceiptsTableTableManager($_db, $_db.receipts).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_receiptsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$MessagesTableFilterComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get isEdited => $composableBuilder( + column: $table.isEdited, builder: (column) => ColumnFilters(column)); + + ColumnFilters get acknowledgeByUser => $composableBuilder( + column: $table.acknowledgeByUser, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get acknowledgeByServer => $composableBuilder( + column: $table.acknowledgeByServer, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get openedByCounter => $composableBuilder( + column: $table.openedByCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get senderId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableFilterComposer get mediaId { + final $$MediaFilesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableFilterComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableFilterComposer get quotesMessageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.quotesMessageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + Expression messageHistoriesRefs( + Expression Function($$MessageHistoriesTableFilterComposer f) f) { + final $$MessageHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableFilterComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableFilterComposer f) f) { + final $$ReactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableFilterComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableFilterComposer f) f) { + final $$ReceiptsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableFilterComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MessagesTableOrderingComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isEdited => $composableBuilder( + column: $table.isEdited, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get acknowledgeByUser => $composableBuilder( + column: $table.acknowledgeByUser, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get acknowledgeByServer => $composableBuilder( + column: $table.acknowledgeByServer, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get openedByCounter => $composableBuilder( + column: $table.openedByCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get senderId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableOrderingComposer get mediaId { + final $$MediaFilesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableOrderingComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableOrderingComposer get quotesMessageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.quotesMessageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessagesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumn get messageId => + $composableBuilder(column: $table.messageId, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, builder: (column) => column); + + GeneratedColumn get isEdited => + $composableBuilder(column: $table.isEdited, builder: (column) => column); + + GeneratedColumn get acknowledgeByUser => $composableBuilder( + column: $table.acknowledgeByUser, builder: (column) => column); + + GeneratedColumn get acknowledgeByServer => $composableBuilder( + column: $table.acknowledgeByServer, builder: (column) => column); + + GeneratedColumn get openedByCounter => $composableBuilder( + column: $table.openedByCounter, builder: (column) => column); + + GeneratedColumn get openedAt => + $composableBuilder(column: $table.openedAt, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get senderId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableAnnotationComposer get mediaId { + final $$MediaFilesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableAnnotationComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableAnnotationComposer get quotesMessageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.quotesMessageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + Expression messageHistoriesRefs( + Expression Function($$MessageHistoriesTableAnnotationComposer a) f) { + final $$MessageHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableAnnotationComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableAnnotationComposer a) f) { + final $$ReceiptsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableAnnotationComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MessagesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessagesTable, + Message, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (Message, $$MessagesTableReferences), + Message, + PrefetchHooks Function( + {bool senderId, + bool mediaId, + bool quotesMessageId, + bool messageHistoriesRefs, + bool reactionsRefs, + bool receiptsRefs})> { + $$MessagesTableTableManager(_$TwonlyDB db, $MessagesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessagesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value messageId = const Value.absent(), + Value senderId = const Value.absent(), + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value quotesMessageId = const Value.absent(), + Value isDeletedFromSender = const Value.absent(), + Value isEdited = const Value.absent(), + Value acknowledgeByUser = const Value.absent(), + Value acknowledgeByServer = const Value.absent(), + Value openedByCounter = const Value.absent(), + Value openedAt = const Value.absent(), + Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessagesCompanion( + groupId: groupId, + messageId: messageId, + senderId: senderId, + content: content, + mediaId: mediaId, + quotesMessageId: quotesMessageId, + isDeletedFromSender: isDeletedFromSender, + isEdited: isEdited, + acknowledgeByUser: acknowledgeByUser, + acknowledgeByServer: acknowledgeByServer, + openedByCounter: openedByCounter, + openedAt: openedAt, + createdAt: createdAt, + modifiedAt: modifiedAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupId, + required String messageId, + Value senderId = const Value.absent(), + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value quotesMessageId = const Value.absent(), + Value isDeletedFromSender = const Value.absent(), + Value isEdited = const Value.absent(), + Value acknowledgeByUser = const Value.absent(), + Value acknowledgeByServer = const Value.absent(), + Value openedByCounter = const Value.absent(), + Value openedAt = const Value.absent(), + Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessagesCompanion.insert( + groupId: groupId, + messageId: messageId, + senderId: senderId, + content: content, + mediaId: mediaId, + quotesMessageId: quotesMessageId, + isDeletedFromSender: isDeletedFromSender, + isEdited: isEdited, + acknowledgeByUser: acknowledgeByUser, + acknowledgeByServer: acknowledgeByServer, + openedByCounter: openedByCounter, + openedAt: openedAt, + createdAt: createdAt, + modifiedAt: modifiedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$MessagesTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {senderId = false, + mediaId = false, + quotesMessageId = false, + messageHistoriesRefs = false, + reactionsRefs = false, + receiptsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messageHistoriesRefs) db.messageHistories, + if (reactionsRefs) db.reactions, + if (receiptsRefs) db.receipts + ], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (senderId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.senderId, + referencedTable: + $$MessagesTableReferences._senderIdTable(db), + referencedColumn: + $$MessagesTableReferences._senderIdTable(db).userId, + ) as T; + } + if (mediaId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.mediaId, + referencedTable: + $$MessagesTableReferences._mediaIdTable(db), + referencedColumn: + $$MessagesTableReferences._mediaIdTable(db).mediaId, + ) as T; + } + if (quotesMessageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.quotesMessageId, + referencedTable: + $$MessagesTableReferences._quotesMessageIdTable(db), + referencedColumn: $$MessagesTableReferences + ._quotesMessageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (messageHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences + ._messageHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .messageHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items), + if (reactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MessagesTableReferences._reactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .reactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items), + if (receiptsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MessagesTableReferences._receiptsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .receiptsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessagesTable, + Message, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (Message, $$MessagesTableReferences), + Message, + PrefetchHooks Function( + {bool senderId, + bool mediaId, + bool quotesMessageId, + bool messageHistoriesRefs, + bool reactionsRefs, + bool receiptsRefs})>; +typedef $$MessageHistoriesTableCreateCompanionBuilder + = MessageHistoriesCompanion Function({ + required String messageId, + Value content, + Value createdAt, + Value rowid, +}); +typedef $$MessageHistoriesTableUpdateCompanionBuilder + = MessageHistoriesCompanion Function({ + Value messageId, + Value content, + Value createdAt, + Value rowid, +}); + +final class $$MessageHistoriesTableReferences + extends BaseReferences<_$TwonlyDB, $MessageHistoriesTable, MessageHistory> { + $$MessageHistoriesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias($_aliasNameGenerator( + db.messageHistories.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MessageHistoriesTableFilterComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableOrderingComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessageHistoriesTable, + MessageHistory, + $$MessageHistoriesTableFilterComposer, + $$MessageHistoriesTableOrderingComposer, + $$MessageHistoriesTableAnnotationComposer, + $$MessageHistoriesTableCreateCompanionBuilder, + $$MessageHistoriesTableUpdateCompanionBuilder, + (MessageHistory, $$MessageHistoriesTableReferences), + MessageHistory, + PrefetchHooks Function({bool messageId})> { + $$MessageHistoriesTableTableManager( + _$TwonlyDB db, $MessageHistoriesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessageHistoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessageHistoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessageHistoriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value messageId = const Value.absent(), + Value content = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessageHistoriesCompanion( + messageId: messageId, + content: content, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String messageId, + Value content = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessageHistoriesCompanion.insert( + messageId: messageId, + content: content, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MessageHistoriesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$MessageHistoriesTableReferences._messageIdTable(db), + referencedColumn: $$MessageHistoriesTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MessageHistoriesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessageHistoriesTable, + MessageHistory, + $$MessageHistoriesTableFilterComposer, + $$MessageHistoriesTableOrderingComposer, + $$MessageHistoriesTableAnnotationComposer, + $$MessageHistoriesTableCreateCompanionBuilder, + $$MessageHistoriesTableUpdateCompanionBuilder, + (MessageHistory, $$MessageHistoriesTableReferences), + MessageHistory, + PrefetchHooks Function({bool messageId})>; +typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({ + required String messageId, + required String emoji, + Value senderId, + Value createdAt, + Value rowid, +}); +typedef $$ReactionsTableUpdateCompanionBuilder = ReactionsCompanion Function({ + Value messageId, + Value emoji, + Value senderId, + Value createdAt, + Value rowid, +}); + +final class $$ReactionsTableReferences + extends BaseReferences<_$TwonlyDB, $ReactionsTable, Reaction> { + $$ReactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias( + $_aliasNameGenerator(db.reactions.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _senderIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.reactions.senderId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get senderId { + final $_column = $_itemColumn('sender_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_senderIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$ReactionsTableFilterComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get emoji => $composableBuilder( + column: $table.emoji, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get senderId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get emoji => $composableBuilder( + column: $table.emoji, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get senderId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get emoji => + $composableBuilder(column: $table.emoji, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get senderId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReactionsTable, + Reaction, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (Reaction, $$ReactionsTableReferences), + Reaction, + PrefetchHooks Function({bool messageId, bool senderId})> { + $$ReactionsTableTableManager(_$TwonlyDB db, $ReactionsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReactionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReactionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReactionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value messageId = const Value.absent(), + Value emoji = const Value.absent(), + Value senderId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReactionsCompanion( + messageId: messageId, + emoji: emoji, + senderId: senderId, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String messageId, + required String emoji, + Value senderId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReactionsCompanion.insert( + messageId: messageId, + emoji: emoji, + senderId: senderId, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$ReactionsTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false, senderId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$ReactionsTableReferences._messageIdTable(db), + referencedColumn: $$ReactionsTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + if (senderId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.senderId, + referencedTable: + $$ReactionsTableReferences._senderIdTable(db), + referencedColumn: + $$ReactionsTableReferences._senderIdTable(db).userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$ReactionsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReactionsTable, + Reaction, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (Reaction, $$ReactionsTableReferences), + Reaction, + PrefetchHooks Function({bool messageId, bool senderId})>; +typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ + Value groupId, + required bool isGroupAdmin, + required bool isGroupOfTwo, + Value pinned, + Value lastMessageExchange, + Value createdAt, + Value rowid, +}); +typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ + Value groupId, + Value isGroupAdmin, + Value isGroupOfTwo, + Value pinned, + Value lastMessageExchange, + Value createdAt, + Value rowid, +}); + +class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, builder: (column) => ColumnFilters(column)); + + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$GroupsTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumn get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => column); + + GeneratedColumn get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, builder: (column) => column); + + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); + + GeneratedColumn get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$GroupsTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, BaseReferences<_$TwonlyDB, $GroupsTable, Group>), + Group, + PrefetchHooks Function()> { + $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value isGroupAdmin = const Value.absent(), + Value isGroupOfTwo = const Value.absent(), + Value pinned = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isGroupOfTwo: isGroupOfTwo, + pinned: pinned, + lastMessageExchange: lastMessageExchange, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + Value groupId = const Value.absent(), + required bool isGroupAdmin, + required bool isGroupOfTwo, + Value pinned = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion.insert( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isGroupOfTwo: isGroupOfTwo, + pinned: pinned, + lastMessageExchange: lastMessageExchange, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, BaseReferences<_$TwonlyDB, $GroupsTable, Group>), + Group, + PrefetchHooks Function()>; +typedef $$GroupMembersTableCreateCompanionBuilder = GroupMembersCompanion + Function({ + required String groupId, + required int contactId, + Value memberState, + Value createdAt, + Value rowid, +}); +typedef $$GroupMembersTableUpdateCompanionBuilder = GroupMembersCompanion + Function({ + Value groupId, + Value contactId, + Value memberState, + Value createdAt, + Value rowid, +}); + +final class $$GroupMembersTableReferences + extends BaseReferences<_$TwonlyDB, $GroupMembersTable, GroupMember> { + $$GroupMembersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.groupMembers.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$GroupMembersTableFilterComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get memberState => $composableBuilder( + column: $table.memberState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableOrderingComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get memberState => $composableBuilder( + column: $table.memberState, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get memberState => + $composableBuilder( + column: $table.memberState, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupMembersTable, + GroupMember, + $$GroupMembersTableFilterComposer, + $$GroupMembersTableOrderingComposer, + $$GroupMembersTableAnnotationComposer, + $$GroupMembersTableCreateCompanionBuilder, + $$GroupMembersTableUpdateCompanionBuilder, + (GroupMember, $$GroupMembersTableReferences), + GroupMember, + PrefetchHooks Function({bool contactId})> { + $$GroupMembersTableTableManager(_$TwonlyDB db, $GroupMembersTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupMembersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupMembersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupMembersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value contactId = const Value.absent(), + Value memberState = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupMembersCompanion( + groupId: groupId, + contactId: contactId, + memberState: memberState, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupId, + required int contactId, + Value memberState = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupMembersCompanion.insert( + groupId: groupId, + contactId: contactId, + memberState: memberState, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$GroupMembersTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$GroupMembersTableReferences._contactIdTable(db), + referencedColumn: $$GroupMembersTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$GroupMembersTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupMembersTable, + GroupMember, + $$GroupMembersTableFilterComposer, + $$GroupMembersTableOrderingComposer, + $$GroupMembersTableAnnotationComposer, + $$GroupMembersTableCreateCompanionBuilder, + $$GroupMembersTableUpdateCompanionBuilder, + (GroupMember, $$GroupMembersTableReferences), + GroupMember, + PrefetchHooks Function({bool contactId})>; +typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({ + Value receiptId, + required int contactId, + Value messageId, + required Uint8List message, + Value contactWillSendsReceipt, + Value retryCount, + Value lastRetry, + Value createdAt, + Value rowid, +}); +typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({ + Value receiptId, + Value contactId, + Value messageId, + Value message, + Value contactWillSendsReceipt, + Value retryCount, + Value lastRetry, + Value createdAt, + Value rowid, +}); + +final class $$ReceiptsTableReferences + extends BaseReferences<_$TwonlyDB, $ReceiptsTable, Receipt> { + $$ReceiptsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.receipts.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias( + $_aliasNameGenerator(db.receipts.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$ReceiptsTableFilterComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get message => $composableBuilder( + column: $table.message, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastRetry => $composableBuilder( + column: $table.lastRetry, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get message => $composableBuilder( + column: $table.message, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastRetry => $composableBuilder( + column: $table.lastRetry, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get receiptId => + $composableBuilder(column: $table.receiptId, builder: (column) => column); + + GeneratedColumn get message => + $composableBuilder(column: $table.message, builder: (column) => column); + + GeneratedColumn get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, builder: (column) => column); + + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => column); + + GeneratedColumn get lastRetry => + $composableBuilder(column: $table.lastRetry, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReceiptsTable, + Receipt, + $$ReceiptsTableFilterComposer, + $$ReceiptsTableOrderingComposer, + $$ReceiptsTableAnnotationComposer, + $$ReceiptsTableCreateCompanionBuilder, + $$ReceiptsTableUpdateCompanionBuilder, + (Receipt, $$ReceiptsTableReferences), + Receipt, + PrefetchHooks Function({bool contactId, bool messageId})> { + $$ReceiptsTableTableManager(_$TwonlyDB db, $ReceiptsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReceiptsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReceiptsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReceiptsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value receiptId = const Value.absent(), + Value contactId = const Value.absent(), + Value messageId = const Value.absent(), + Value message = const Value.absent(), + Value contactWillSendsReceipt = const Value.absent(), + Value retryCount = const Value.absent(), + Value lastRetry = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceiptsCompanion( + receiptId: receiptId, + contactId: contactId, + messageId: messageId, + message: message, + contactWillSendsReceipt: contactWillSendsReceipt, + retryCount: retryCount, + lastRetry: lastRetry, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + Value receiptId = const Value.absent(), + required int contactId, + Value messageId = const Value.absent(), + required Uint8List message, + Value contactWillSendsReceipt = const Value.absent(), + Value retryCount = const Value.absent(), + Value lastRetry = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceiptsCompanion.insert( + receiptId: receiptId, + contactId: contactId, + messageId: messageId, + message: message, + contactWillSendsReceipt: contactWillSendsReceipt, + retryCount: retryCount, + lastRetry: lastRetry, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$ReceiptsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({contactId = false, messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$ReceiptsTableReferences._contactIdTable(db), + referencedColumn: + $$ReceiptsTableReferences._contactIdTable(db).userId, + ) as T; + } + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$ReceiptsTableReferences._messageIdTable(db), + referencedColumn: + $$ReceiptsTableReferences._messageIdTable(db).messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReceiptsTable, + Receipt, + $$ReceiptsTableFilterComposer, + $$ReceiptsTableOrderingComposer, + $$ReceiptsTableAnnotationComposer, + $$ReceiptsTableCreateCompanionBuilder, + $$ReceiptsTableUpdateCompanionBuilder, + (Receipt, $$ReceiptsTableReferences), + Receipt, + PrefetchHooks Function({bool contactId, bool messageId})>; +typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder + = SignalIdentityKeyStoresCompanion Function({ + required int deviceId, + required String name, + required Uint8List identityKey, + Value createdAt, + Value rowid, +}); +typedef $$SignalIdentityKeyStoresTableUpdateCompanionBuilder + = SignalIdentityKeyStoresCompanion Function({ + Value deviceId, + Value name, + Value identityKey, + Value createdAt, + Value rowid, +}); + +class $$SignalIdentityKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalIdentityKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalIdentityKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get deviceId => + $composableBuilder(column: $table.deviceId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore, + $$SignalIdentityKeyStoresTableFilterComposer, + $$SignalIdentityKeyStoresTableOrderingComposer, + $$SignalIdentityKeyStoresTableAnnotationComposer, + $$SignalIdentityKeyStoresTableCreateCompanionBuilder, + $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, + ( + SignalIdentityKeyStore, + BaseReferences<_$TwonlyDB, $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore> + ), + SignalIdentityKeyStore, + PrefetchHooks Function()> { + $$SignalIdentityKeyStoresTableTableManager( + _$TwonlyDB db, $SignalIdentityKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalIdentityKeyStoresTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalIdentityKeyStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalIdentityKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value deviceId = const Value.absent(), + Value name = const Value.absent(), + Value identityKey = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalIdentityKeyStoresCompanion( + deviceId: deviceId, + name: name, + identityKey: identityKey, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int deviceId, + required String name, + required Uint8List identityKey, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalIdentityKeyStoresCompanion.insert( + deviceId: deviceId, + name: name, + identityKey: identityKey, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalIdentityKeyStoresTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore, + $$SignalIdentityKeyStoresTableFilterComposer, + $$SignalIdentityKeyStoresTableOrderingComposer, + $$SignalIdentityKeyStoresTableAnnotationComposer, + $$SignalIdentityKeyStoresTableCreateCompanionBuilder, + $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, + ( + SignalIdentityKeyStore, + BaseReferences<_$TwonlyDB, $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore> + ), + SignalIdentityKeyStore, + PrefetchHooks Function()>; +typedef $$SignalPreKeyStoresTableCreateCompanionBuilder + = SignalPreKeyStoresCompanion Function({ + Value preKeyId, + required Uint8List preKey, + Value createdAt, +}); +typedef $$SignalPreKeyStoresTableUpdateCompanionBuilder + = SignalPreKeyStoresCompanion Function({ + Value preKeyId, + Value preKey, + Value createdAt, +}); + +class $$SignalPreKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalPreKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalPreKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get preKeyId => + $composableBuilder(column: $table.preKeyId, builder: (column) => column); + + GeneratedColumn get preKey => + $composableBuilder(column: $table.preKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalPreKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalPreKeyStoresTable, + SignalPreKeyStore, + $$SignalPreKeyStoresTableFilterComposer, + $$SignalPreKeyStoresTableOrderingComposer, + $$SignalPreKeyStoresTableAnnotationComposer, + $$SignalPreKeyStoresTableCreateCompanionBuilder, + $$SignalPreKeyStoresTableUpdateCompanionBuilder, + ( + SignalPreKeyStore, + BaseReferences<_$TwonlyDB, $SignalPreKeyStoresTable, SignalPreKeyStore> + ), + SignalPreKeyStore, + PrefetchHooks Function()> { + $$SignalPreKeyStoresTableTableManager( + _$TwonlyDB db, $SignalPreKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalPreKeyStoresTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalPreKeyStoresTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SignalPreKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value preKeyId = const Value.absent(), + Value preKey = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SignalPreKeyStoresCompanion( + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value preKeyId = const Value.absent(), + required Uint8List preKey, + Value createdAt = const Value.absent(), + }) => + SignalPreKeyStoresCompanion.insert( + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $SignalPreKeyStoresTable, + SignalPreKeyStore, + $$SignalPreKeyStoresTableFilterComposer, + $$SignalPreKeyStoresTableOrderingComposer, + $$SignalPreKeyStoresTableAnnotationComposer, + $$SignalPreKeyStoresTableCreateCompanionBuilder, + $$SignalPreKeyStoresTableUpdateCompanionBuilder, + ( + SignalPreKeyStore, + BaseReferences<_$TwonlyDB, $SignalPreKeyStoresTable, SignalPreKeyStore> + ), + SignalPreKeyStore, + PrefetchHooks Function()>; +typedef $$SignalSenderKeyStoresTableCreateCompanionBuilder + = SignalSenderKeyStoresCompanion Function({ + required String senderKeyName, + required Uint8List senderKey, + Value rowid, +}); +typedef $$SignalSenderKeyStoresTableUpdateCompanionBuilder + = SignalSenderKeyStoresCompanion Function({ + Value senderKeyName, + Value senderKey, + Value rowid, +}); + +class $$SignalSenderKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get senderKeyName => $composableBuilder( + column: $table.senderKeyName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get senderKey => $composableBuilder( + column: $table.senderKey, builder: (column) => ColumnFilters(column)); +} + +class $$SignalSenderKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get senderKeyName => $composableBuilder( + column: $table.senderKeyName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get senderKey => $composableBuilder( + column: $table.senderKey, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalSenderKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get senderKeyName => $composableBuilder( + column: $table.senderKeyName, builder: (column) => column); + + GeneratedColumn get senderKey => + $composableBuilder(column: $table.senderKey, builder: (column) => column); +} + +class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalSenderKeyStoresTable, + SignalSenderKeyStore, + $$SignalSenderKeyStoresTableFilterComposer, + $$SignalSenderKeyStoresTableOrderingComposer, + $$SignalSenderKeyStoresTableAnnotationComposer, + $$SignalSenderKeyStoresTableCreateCompanionBuilder, + $$SignalSenderKeyStoresTableUpdateCompanionBuilder, + ( + SignalSenderKeyStore, + BaseReferences<_$TwonlyDB, $SignalSenderKeyStoresTable, + SignalSenderKeyStore> + ), + SignalSenderKeyStore, + PrefetchHooks Function()> { + $$SignalSenderKeyStoresTableTableManager( + _$TwonlyDB db, $SignalSenderKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalSenderKeyStoresTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalSenderKeyStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalSenderKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value senderKeyName = const Value.absent(), + Value senderKey = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName, + senderKey: senderKey, + rowid: rowid, + ), + createCompanionCallback: ({ + required String senderKeyName, + required Uint8List senderKey, + Value rowid = const Value.absent(), + }) => + SignalSenderKeyStoresCompanion.insert( + senderKeyName: senderKeyName, + senderKey: senderKey, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalSenderKeyStoresTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalSenderKeyStoresTable, + SignalSenderKeyStore, + $$SignalSenderKeyStoresTableFilterComposer, + $$SignalSenderKeyStoresTableOrderingComposer, + $$SignalSenderKeyStoresTableAnnotationComposer, + $$SignalSenderKeyStoresTableCreateCompanionBuilder, + $$SignalSenderKeyStoresTableUpdateCompanionBuilder, + ( + SignalSenderKeyStore, + BaseReferences<_$TwonlyDB, $SignalSenderKeyStoresTable, + SignalSenderKeyStore> + ), + SignalSenderKeyStore, + PrefetchHooks Function()>; +typedef $$SignalSessionStoresTableCreateCompanionBuilder + = SignalSessionStoresCompanion Function({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + Value createdAt, + Value rowid, +}); +typedef $$SignalSessionStoresTableUpdateCompanionBuilder + = SignalSessionStoresCompanion Function({ + Value deviceId, + Value name, + Value sessionRecord, + Value createdAt, + Value rowid, +}); + +class $$SignalSessionStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get sessionRecord => $composableBuilder( + column: $table.sessionRecord, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalSessionStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get sessionRecord => $composableBuilder( + column: $table.sessionRecord, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalSessionStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get deviceId => + $composableBuilder(column: $table.deviceId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get sessionRecord => $composableBuilder( + column: $table.sessionRecord, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalSessionStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalSessionStoresTable, + SignalSessionStore, + $$SignalSessionStoresTableFilterComposer, + $$SignalSessionStoresTableOrderingComposer, + $$SignalSessionStoresTableAnnotationComposer, + $$SignalSessionStoresTableCreateCompanionBuilder, + $$SignalSessionStoresTableUpdateCompanionBuilder, + ( + SignalSessionStore, + BaseReferences<_$TwonlyDB, $SignalSessionStoresTable, SignalSessionStore> + ), + SignalSessionStore, + PrefetchHooks Function()> { + $$SignalSessionStoresTableTableManager( + _$TwonlyDB db, $SignalSessionStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalSessionStoresTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalSessionStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalSessionStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value deviceId = const Value.absent(), + Value name = const Value.absent(), + Value sessionRecord = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSessionStoresCompanion( + deviceId: deviceId, + name: name, + sessionRecord: sessionRecord, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSessionStoresCompanion.insert( + deviceId: deviceId, + name: name, + sessionRecord: sessionRecord, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $SignalSessionStoresTable, + SignalSessionStore, + $$SignalSessionStoresTableFilterComposer, + $$SignalSessionStoresTableOrderingComposer, + $$SignalSessionStoresTableAnnotationComposer, + $$SignalSessionStoresTableCreateCompanionBuilder, + $$SignalSessionStoresTableUpdateCompanionBuilder, + ( + SignalSessionStore, + BaseReferences<_$TwonlyDB, $SignalSessionStoresTable, SignalSessionStore> + ), + SignalSessionStore, + PrefetchHooks Function()>; +typedef $$SignalContactPreKeysTableCreateCompanionBuilder + = SignalContactPreKeysCompanion Function({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + Value createdAt, + Value rowid, +}); +typedef $$SignalContactPreKeysTableUpdateCompanionBuilder + = SignalContactPreKeysCompanion Function({ + Value contactId, + Value preKeyId, + Value preKey, + Value createdAt, + Value rowid, +}); + +final class $$SignalContactPreKeysTableReferences extends BaseReferences< + _$TwonlyDB, $SignalContactPreKeysTable, SignalContactPreKey> { + $$SignalContactPreKeysTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.signalContactPreKeys.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$SignalContactPreKeysTableFilterComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get preKeyId => + $composableBuilder(column: $table.preKeyId, builder: (column) => column); + + GeneratedColumn get preKey => + $composableBuilder(column: $table.preKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalContactPreKeysTable, + SignalContactPreKey, + $$SignalContactPreKeysTableFilterComposer, + $$SignalContactPreKeysTableOrderingComposer, + $$SignalContactPreKeysTableAnnotationComposer, + $$SignalContactPreKeysTableCreateCompanionBuilder, + $$SignalContactPreKeysTableUpdateCompanionBuilder, + (SignalContactPreKey, $$SignalContactPreKeysTableReferences), + SignalContactPreKey, + PrefetchHooks Function({bool contactId})> { + $$SignalContactPreKeysTableTableManager( + _$TwonlyDB db, $SignalContactPreKeysTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalContactPreKeysTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalContactPreKeysTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalContactPreKeysTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value contactId = const Value.absent(), + Value preKeyId = const Value.absent(), + Value preKey = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalContactPreKeysCompanion( + contactId: contactId, + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalContactPreKeysCompanion.insert( + contactId: contactId, + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$SignalContactPreKeysTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: $$SignalContactPreKeysTableReferences + ._contactIdTable(db), + referencedColumn: $$SignalContactPreKeysTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$SignalContactPreKeysTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalContactPreKeysTable, + SignalContactPreKey, + $$SignalContactPreKeysTableFilterComposer, + $$SignalContactPreKeysTableOrderingComposer, + $$SignalContactPreKeysTableAnnotationComposer, + $$SignalContactPreKeysTableCreateCompanionBuilder, + $$SignalContactPreKeysTableUpdateCompanionBuilder, + (SignalContactPreKey, $$SignalContactPreKeysTableReferences), + SignalContactPreKey, + PrefetchHooks Function({bool contactId})>; +typedef $$SignalContactSignedPreKeysTableCreateCompanionBuilder + = SignalContactSignedPreKeysCompanion Function({ + Value contactId, + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + Value createdAt, +}); +typedef $$SignalContactSignedPreKeysTableUpdateCompanionBuilder + = SignalContactSignedPreKeysCompanion Function({ + Value contactId, + Value signedPreKeyId, + Value signedPreKey, + Value signedPreKeySignature, + Value createdAt, +}); + +final class $$SignalContactSignedPreKeysTableReferences extends BaseReferences< + _$TwonlyDB, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> { + $$SignalContactSignedPreKeysTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.signalContactSignedPreKeys.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$SignalContactSignedPreKeysTableFilterComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get signedPreKey => $composableBuilder( + column: $table.signedPreKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get signedPreKey => $composableBuilder( + column: $table.signedPreKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, builder: (column) => column); + + GeneratedColumn get signedPreKey => $composableBuilder( + column: $table.signedPreKey, builder: (column) => column); + + GeneratedColumn get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalContactSignedPreKeysTable, + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableFilterComposer, + $$SignalContactSignedPreKeysTableOrderingComposer, + $$SignalContactSignedPreKeysTableAnnotationComposer, + $$SignalContactSignedPreKeysTableCreateCompanionBuilder, + $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, + (SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableReferences), + SignalContactSignedPreKey, + PrefetchHooks Function({bool contactId})> { + $$SignalContactSignedPreKeysTableTableManager( + _$TwonlyDB db, $SignalContactSignedPreKeysTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalContactSignedPreKeysTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalContactSignedPreKeysTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalContactSignedPreKeysTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value contactId = const Value.absent(), + Value signedPreKeyId = const Value.absent(), + Value signedPreKey = const Value.absent(), + Value signedPreKeySignature = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SignalContactSignedPreKeysCompanion( + contactId: contactId, + signedPreKeyId: signedPreKeyId, + signedPreKey: signedPreKey, + signedPreKeySignature: signedPreKeySignature, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value contactId = const Value.absent(), + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + Value createdAt = const Value.absent(), + }) => + SignalContactSignedPreKeysCompanion.insert( + contactId: contactId, + signedPreKeyId: signedPreKeyId, + signedPreKey: signedPreKey, + signedPreKeySignature: signedPreKeySignature, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$SignalContactSignedPreKeysTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: $$SignalContactSignedPreKeysTableReferences + ._contactIdTable(db), + referencedColumn: + $$SignalContactSignedPreKeysTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$SignalContactSignedPreKeysTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalContactSignedPreKeysTable, + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableFilterComposer, + $$SignalContactSignedPreKeysTableOrderingComposer, + $$SignalContactSignedPreKeysTableAnnotationComposer, + $$SignalContactSignedPreKeysTableCreateCompanionBuilder, + $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, + ( + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableReferences + ), + SignalContactSignedPreKey, + PrefetchHooks Function({bool contactId})>; + +class $TwonlyDBManager { + final _$TwonlyDB _db; + $TwonlyDBManager(this._db); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db, _db.contacts); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db, _db.mediaFiles); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db, _db.messages); + $$MessageHistoriesTableTableManager get messageHistories => + $$MessageHistoriesTableTableManager(_db, _db.messageHistories); + $$ReactionsTableTableManager get reactions => + $$ReactionsTableTableManager(_db, _db.reactions); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db, _db.groups); + $$GroupMembersTableTableManager get groupMembers => + $$GroupMembersTableTableManager(_db, _db.groupMembers); + $$ReceiptsTableTableManager get receipts => + $$ReceiptsTableTableManager(_db, _db.receipts); + $$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores => + $$SignalIdentityKeyStoresTableTableManager( + _db, _db.signalIdentityKeyStores); + $$SignalPreKeyStoresTableTableManager get signalPreKeyStores => + $$SignalPreKeyStoresTableTableManager(_db, _db.signalPreKeyStores); + $$SignalSenderKeyStoresTableTableManager get signalSenderKeyStores => + $$SignalSenderKeyStoresTableTableManager(_db, _db.signalSenderKeyStores); + $$SignalSessionStoresTableTableManager get signalSessionStores => + $$SignalSessionStoresTableTableManager(_db, _db.signalSessionStores); + $$SignalContactPreKeysTableTableManager get signalContactPreKeys => + $$SignalContactPreKeysTableTableManager(_db, _db.signalContactPreKeys); + $$SignalContactSignedPreKeysTableTableManager + get signalContactSignedPreKeys => + $$SignalContactSignedPreKeysTableTableManager( + _db, _db.signalContactSignedPreKeys); +} diff --git a/lib/src/database/twonly_database.dart b/lib/src/database/twonly_database_old.dart similarity index 80% rename from lib/src/database/twonly_database.dart rename to lib/src/database/twonly_database_old.dart index 65a88ec..1125a21 100644 --- a/lib/src/database/twonly_database.dart +++ b/lib/src/database/twonly_database_old.dart @@ -2,25 +2,20 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; import 'package:path_provider/path_provider.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/daos/media_uploads_dao.dart'; -import 'package:twonly/src/database/daos/message_retransmissions.dao.dart'; -import 'package:twonly/src/database/daos/messages_dao.dart'; -import 'package:twonly/src/database/daos/signal_dao.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/message_retransmissions.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_identity_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_pre_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_sender_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_session_store_table.dart'; -import 'package:twonly/src/database/twonly_database.steps.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/media_uploads_table.dart'; +import 'package:twonly/src/database/tables_old/message_retransmissions.dart'; +import 'package:twonly/src/database/tables_old/messages_table.dart'; +import 'package:twonly/src/database/tables_old/signal_contact_prekey_table.dart'; +import 'package:twonly/src/database/tables_old/signal_contact_signed_prekey_table.dart'; +import 'package:twonly/src/database/tables_old/signal_identity_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_pre_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_sender_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_session_store_table.dart'; +import 'package:twonly/src/database/twonly_database_old.steps.dart'; import 'package:twonly/src/utils/log.dart'; -part 'twonly_database.g.dart'; +part 'twonly_database_old.g.dart'; // You can then create a database class that includes this table @DriftDatabase( @@ -36,22 +31,16 @@ part 'twonly_database.g.dart'; SignalContactSignedPreKeys, MessageRetransmissions, ], - daos: [ - MessagesDao, - ContactsDao, - MediaUploadsDao, - SignalDao, - MessageRetransmissionDao, - ], + daos: [], ) -class TwonlyDatabase extends _$TwonlyDatabase { - TwonlyDatabase([QueryExecutor? e]) +class TwonlyDatabaseOld extends _$TwonlyDatabaseOld { + TwonlyDatabaseOld([QueryExecutor? e]) : super( e ?? _openConnection(), ); // ignore: matching_super_parameters - TwonlyDatabase.forTesting(DatabaseConnection super.connection); + TwonlyDatabaseOld.forTesting(DatabaseConnection super.connection); @override int get schemaVersion => 17; diff --git a/lib/src/database/twonly_database.g.dart b/lib/src/database/twonly_database_old.g.dart similarity index 98% rename from lib/src/database/twonly_database.g.dart rename to lib/src/database/twonly_database_old.g.dart index bef787d..ed59521 100644 --- a/lib/src/database/twonly_database.g.dart +++ b/lib/src/database/twonly_database_old.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'twonly_database.dart'; +part of 'twonly_database_old.dart'; // ignore_for_file: type=lint class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { @@ -4443,9 +4443,9 @@ class MessageRetransmissionsCompanion } } -abstract class _$TwonlyDatabase extends GeneratedDatabase { - _$TwonlyDatabase(QueryExecutor e) : super(e); - $TwonlyDatabaseManager get managers => $TwonlyDatabaseManager(this); +abstract class _$TwonlyDatabaseOld extends GeneratedDatabase { + _$TwonlyDatabaseOld(QueryExecutor e) : super(e); + $TwonlyDatabaseOldManager get managers => $TwonlyDatabaseOldManager(this); late final $ContactsTable contacts = $ContactsTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $MediaUploadsTable mediaUploads = $MediaUploadsTable(this); @@ -4463,13 +4463,6 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase { $SignalContactSignedPreKeysTable(this); late final $MessageRetransmissionsTable messageRetransmissions = $MessageRetransmissionsTable(this); - late final MessagesDao messagesDao = MessagesDao(this as TwonlyDatabase); - late final ContactsDao contactsDao = ContactsDao(this as TwonlyDatabase); - late final MediaUploadsDao mediaUploadsDao = - MediaUploadsDao(this as TwonlyDatabase); - late final SignalDao signalDao = SignalDao(this as TwonlyDatabase); - late final MessageRetransmissionDao messageRetransmissionDao = - MessageRetransmissionDao(this as TwonlyDatabase); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4559,11 +4552,11 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ }); final class $$ContactsTableReferences - extends BaseReferences<_$TwonlyDatabase, $ContactsTable, Contact> { + extends BaseReferences<_$TwonlyDatabaseOld, $ContactsTable, Contact> { $$ContactsTableReferences(super.$_db, super.$_table, super.$_typedResult); static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messages, aliasName: $_aliasNameGenerator(db.contacts.userId, db.messages.contactId)); @@ -4579,7 +4572,7 @@ final class $$ContactsTableReferences static MultiTypedResultKey<$MessageRetransmissionsTable, List> _messageRetransmissionsRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messageRetransmissions, aliasName: $_aliasNameGenerator( db.contacts.userId, db.messageRetransmissions.contactId)); @@ -4599,7 +4592,7 @@ final class $$ContactsTableReferences } class $$ContactsTableFilterComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableFilterComposer({ required super.$db, required super.$table, @@ -4730,7 +4723,7 @@ class $$ContactsTableFilterComposer } class $$ContactsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableOrderingComposer({ required super.$db, required super.$table, @@ -4819,7 +4812,7 @@ class $$ContactsTableOrderingComposer } class $$ContactsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableAnnotationComposer({ required super.$db, required super.$table, @@ -4942,7 +4935,7 @@ class $$ContactsTableAnnotationComposer } class $$ContactsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $ContactsTable, Contact, $$ContactsTableFilterComposer, @@ -4954,7 +4947,7 @@ class $$ContactsTableTableManager extends RootTableManager< Contact, PrefetchHooks Function( {bool messagesRefs, bool messageRetransmissionsRefs})> { - $$ContactsTableTableManager(_$TwonlyDatabase db, $ContactsTable table) + $$ContactsTableTableManager(_$TwonlyDatabaseOld db, $ContactsTable table) : super(TableManagerState( db: db, table: table, @@ -5112,7 +5105,7 @@ class $$ContactsTableTableManager extends RootTableManager< } typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $ContactsTable, Contact, $$ContactsTableFilterComposer, @@ -5166,10 +5159,10 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ }); final class $$MessagesTableReferences - extends BaseReferences<_$TwonlyDatabase, $MessagesTable, Message> { + extends BaseReferences<_$TwonlyDatabaseOld, $MessagesTable, Message> { $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ContactsTable _contactIdTable(_$TwonlyDatabase db) => + static $ContactsTable _contactIdTable(_$TwonlyDatabaseOld db) => db.contacts.createAlias( $_aliasNameGenerator(db.messages.contactId, db.contacts.userId)); @@ -5186,7 +5179,7 @@ final class $$MessagesTableReferences static MultiTypedResultKey<$MessageRetransmissionsTable, List> _messageRetransmissionsRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messageRetransmissions, aliasName: $_aliasNameGenerator( db.messages.messageId, db.messageRetransmissions.messageId)); @@ -5206,7 +5199,7 @@ final class $$MessagesTableReferences } class $$MessagesTableFilterComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableFilterComposer({ required super.$db, required super.$table, @@ -5324,7 +5317,7 @@ class $$MessagesTableFilterComposer } class $$MessagesTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableOrderingComposer({ required super.$db, required super.$table, @@ -5415,7 +5408,7 @@ class $$MessagesTableOrderingComposer } class $$MessagesTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableAnnotationComposer({ required super.$db, required super.$table, @@ -5521,7 +5514,7 @@ class $$MessagesTableAnnotationComposer } class $$MessagesTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessagesTable, Message, $$MessagesTableFilterComposer, @@ -5532,7 +5525,7 @@ class $$MessagesTableTableManager extends RootTableManager< (Message, $$MessagesTableReferences), Message, PrefetchHooks Function({bool contactId, bool messageRetransmissionsRefs})> { - $$MessagesTableTableManager(_$TwonlyDatabase db, $MessagesTable table) + $$MessagesTableTableManager(_$TwonlyDatabaseOld db, $MessagesTable table) : super(TableManagerState( db: db, table: table, @@ -5684,7 +5677,7 @@ class $$MessagesTableTableManager extends RootTableManager< } typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessagesTable, Message, $$MessagesTableFilterComposer, @@ -5713,7 +5706,7 @@ typedef $$MediaUploadsTableUpdateCompanionBuilder = MediaUploadsCompanion }); class $$MediaUploadsTableFilterComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableFilterComposer({ required super.$db, required super.$table, @@ -5748,7 +5741,7 @@ class $$MediaUploadsTableFilterComposer } class $$MediaUploadsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableOrderingComposer({ required super.$db, required super.$table, @@ -5775,7 +5768,7 @@ class $$MediaUploadsTableOrderingComposer } class $$MediaUploadsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableAnnotationComposer({ required super.$db, required super.$table, @@ -5802,7 +5795,7 @@ class $$MediaUploadsTableAnnotationComposer } class $$MediaUploadsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload, $$MediaUploadsTableFilterComposer, @@ -5812,11 +5805,12 @@ class $$MediaUploadsTableTableManager extends RootTableManager< $$MediaUploadsTableUpdateCompanionBuilder, ( MediaUpload, - BaseReferences<_$TwonlyDatabase, $MediaUploadsTable, MediaUpload> + BaseReferences<_$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload> ), MediaUpload, PrefetchHooks Function()> { - $$MediaUploadsTableTableManager(_$TwonlyDatabase db, $MediaUploadsTable table) + $$MediaUploadsTableTableManager( + _$TwonlyDatabaseOld db, $MediaUploadsTable table) : super(TableManagerState( db: db, table: table, @@ -5862,7 +5856,7 @@ class $$MediaUploadsTableTableManager extends RootTableManager< } typedef $$MediaUploadsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload, $$MediaUploadsTableFilterComposer, @@ -5872,7 +5866,7 @@ typedef $$MediaUploadsTableProcessedTableManager = ProcessedTableManager< $$MediaUploadsTableUpdateCompanionBuilder, ( MediaUpload, - BaseReferences<_$TwonlyDatabase, $MediaUploadsTable, MediaUpload> + BaseReferences<_$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload> ), MediaUpload, PrefetchHooks Function()>; @@ -5894,7 +5888,7 @@ typedef $$SignalIdentityKeyStoresTableUpdateCompanionBuilder }); class $$SignalIdentityKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -5916,7 +5910,7 @@ class $$SignalIdentityKeyStoresTableFilterComposer } class $$SignalIdentityKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -5938,7 +5932,7 @@ class $$SignalIdentityKeyStoresTableOrderingComposer } class $$SignalIdentityKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -5960,7 +5954,7 @@ class $$SignalIdentityKeyStoresTableAnnotationComposer } class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore, $$SignalIdentityKeyStoresTableFilterComposer, @@ -5970,13 +5964,13 @@ class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, ( SignalIdentityKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalIdentityKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore> ), SignalIdentityKeyStore, PrefetchHooks Function()> { $$SignalIdentityKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalIdentityKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalIdentityKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6026,7 +6020,7 @@ class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< typedef $$SignalIdentityKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore, $$SignalIdentityKeyStoresTableFilterComposer, @@ -6036,7 +6030,7 @@ typedef $$SignalIdentityKeyStoresTableProcessedTableManager $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, ( SignalIdentityKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalIdentityKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore> ), SignalIdentityKeyStore, @@ -6055,7 +6049,7 @@ typedef $$SignalPreKeyStoresTableUpdateCompanionBuilder }); class $$SignalPreKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6074,7 +6068,7 @@ class $$SignalPreKeyStoresTableFilterComposer } class $$SignalPreKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6093,7 +6087,7 @@ class $$SignalPreKeyStoresTableOrderingComposer } class $$SignalPreKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6112,7 +6106,7 @@ class $$SignalPreKeyStoresTableAnnotationComposer } class $$SignalPreKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore, $$SignalPreKeyStoresTableFilterComposer, @@ -6122,13 +6116,13 @@ class $$SignalPreKeyStoresTableTableManager extends RootTableManager< $$SignalPreKeyStoresTableUpdateCompanionBuilder, ( SignalPreKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalPreKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore> ), SignalPreKeyStore, PrefetchHooks Function()> { $$SignalPreKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalPreKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalPreKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6167,7 +6161,7 @@ class $$SignalPreKeyStoresTableTableManager extends RootTableManager< } typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore, $$SignalPreKeyStoresTableFilterComposer, @@ -6177,7 +6171,7 @@ typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< $$SignalPreKeyStoresTableUpdateCompanionBuilder, ( SignalPreKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalPreKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore> ), SignalPreKeyStore, @@ -6196,7 +6190,7 @@ typedef $$SignalSenderKeyStoresTableUpdateCompanionBuilder }); class $$SignalSenderKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6212,7 +6206,7 @@ class $$SignalSenderKeyStoresTableFilterComposer } class $$SignalSenderKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6229,7 +6223,7 @@ class $$SignalSenderKeyStoresTableOrderingComposer } class $$SignalSenderKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6245,7 +6239,7 @@ class $$SignalSenderKeyStoresTableAnnotationComposer } class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore, $$SignalSenderKeyStoresTableFilterComposer, @@ -6255,13 +6249,13 @@ class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< $$SignalSenderKeyStoresTableUpdateCompanionBuilder, ( SignalSenderKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalSenderKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore> ), SignalSenderKeyStore, PrefetchHooks Function()> { $$SignalSenderKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalSenderKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalSenderKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6303,7 +6297,7 @@ class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< typedef $$SignalSenderKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore, $$SignalSenderKeyStoresTableFilterComposer, @@ -6313,7 +6307,7 @@ typedef $$SignalSenderKeyStoresTableProcessedTableManager $$SignalSenderKeyStoresTableUpdateCompanionBuilder, ( SignalSenderKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalSenderKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore> ), SignalSenderKeyStore, @@ -6336,7 +6330,7 @@ typedef $$SignalSessionStoresTableUpdateCompanionBuilder }); class $$SignalSessionStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6358,7 +6352,7 @@ class $$SignalSessionStoresTableFilterComposer } class $$SignalSessionStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6381,7 +6375,7 @@ class $$SignalSessionStoresTableOrderingComposer } class $$SignalSessionStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6403,7 +6397,7 @@ class $$SignalSessionStoresTableAnnotationComposer } class $$SignalSessionStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore, $$SignalSessionStoresTableFilterComposer, @@ -6413,13 +6407,13 @@ class $$SignalSessionStoresTableTableManager extends RootTableManager< $$SignalSessionStoresTableUpdateCompanionBuilder, ( SignalSessionStore, - BaseReferences<_$TwonlyDatabase, $SignalSessionStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore> ), SignalSessionStore, PrefetchHooks Function()> { $$SignalSessionStoresTableTableManager( - _$TwonlyDatabase db, $SignalSessionStoresTable table) + _$TwonlyDatabaseOld db, $SignalSessionStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6467,7 +6461,7 @@ class $$SignalSessionStoresTableTableManager extends RootTableManager< } typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore, $$SignalSessionStoresTableFilterComposer, @@ -6477,7 +6471,7 @@ typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< $$SignalSessionStoresTableUpdateCompanionBuilder, ( SignalSessionStore, - BaseReferences<_$TwonlyDatabase, $SignalSessionStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore> ), SignalSessionStore, @@ -6500,7 +6494,7 @@ typedef $$SignalContactPreKeysTableUpdateCompanionBuilder }); class $$SignalContactPreKeysTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableFilterComposer({ required super.$db, required super.$table, @@ -6522,7 +6516,7 @@ class $$SignalContactPreKeysTableFilterComposer } class $$SignalContactPreKeysTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableOrderingComposer({ required super.$db, required super.$table, @@ -6544,7 +6538,7 @@ class $$SignalContactPreKeysTableOrderingComposer } class $$SignalContactPreKeysTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableAnnotationComposer({ required super.$db, required super.$table, @@ -6566,7 +6560,7 @@ class $$SignalContactPreKeysTableAnnotationComposer } class $$SignalContactPreKeysTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey, $$SignalContactPreKeysTableFilterComposer, @@ -6576,13 +6570,13 @@ class $$SignalContactPreKeysTableTableManager extends RootTableManager< $$SignalContactPreKeysTableUpdateCompanionBuilder, ( SignalContactPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey> ), SignalContactPreKey, PrefetchHooks Function()> { $$SignalContactPreKeysTableTableManager( - _$TwonlyDatabase db, $SignalContactPreKeysTable table) + _$TwonlyDatabaseOld db, $SignalContactPreKeysTable table) : super(TableManagerState( db: db, table: table, @@ -6631,7 +6625,7 @@ class $$SignalContactPreKeysTableTableManager extends RootTableManager< typedef $$SignalContactPreKeysTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey, $$SignalContactPreKeysTableFilterComposer, @@ -6641,7 +6635,7 @@ typedef $$SignalContactPreKeysTableProcessedTableManager $$SignalContactPreKeysTableUpdateCompanionBuilder, ( SignalContactPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey> ), SignalContactPreKey, @@ -6664,7 +6658,7 @@ typedef $$SignalContactSignedPreKeysTableUpdateCompanionBuilder }); class $$SignalContactSignedPreKeysTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableFilterComposer({ required super.$db, required super.$table, @@ -6691,7 +6685,7 @@ class $$SignalContactSignedPreKeysTableFilterComposer } class $$SignalContactSignedPreKeysTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableOrderingComposer({ required super.$db, required super.$table, @@ -6719,7 +6713,7 @@ class $$SignalContactSignedPreKeysTableOrderingComposer } class $$SignalContactSignedPreKeysTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableAnnotationComposer({ required super.$db, required super.$table, @@ -6744,7 +6738,7 @@ class $$SignalContactSignedPreKeysTableAnnotationComposer } class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableFilterComposer, @@ -6754,13 +6748,13 @@ class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, ( SignalContactSignedPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactSignedPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> ), SignalContactSignedPreKey, PrefetchHooks Function()> { $$SignalContactSignedPreKeysTableTableManager( - _$TwonlyDatabase db, $SignalContactSignedPreKeysTable table) + _$TwonlyDatabaseOld db, $SignalContactSignedPreKeysTable table) : super(TableManagerState( db: db, table: table, @@ -6810,7 +6804,7 @@ class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< typedef $$SignalContactSignedPreKeysTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableFilterComposer, @@ -6820,7 +6814,7 @@ typedef $$SignalContactSignedPreKeysTableProcessedTableManager $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, ( SignalContactSignedPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactSignedPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> ), SignalContactSignedPreKey, @@ -6851,11 +6845,11 @@ typedef $$MessageRetransmissionsTableUpdateCompanionBuilder }); final class $$MessageRetransmissionsTableReferences extends BaseReferences< - _$TwonlyDatabase, $MessageRetransmissionsTable, MessageRetransmission> { + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission> { $$MessageRetransmissionsTableReferences( super.$_db, super.$_table, super.$_typedResult); - static $ContactsTable _contactIdTable(_$TwonlyDatabase db) => + static $ContactsTable _contactIdTable(_$TwonlyDatabaseOld db) => db.contacts.createAlias($_aliasNameGenerator( db.messageRetransmissions.contactId, db.contacts.userId)); @@ -6870,7 +6864,7 @@ final class $$MessageRetransmissionsTableReferences extends BaseReferences< manager.$state.copyWith(prefetchedData: [item])); } - static $MessagesTable _messageIdTable(_$TwonlyDatabase db) => + static $MessagesTable _messageIdTable(_$TwonlyDatabaseOld db) => db.messages.createAlias($_aliasNameGenerator( db.messageRetransmissions.messageId, db.messages.messageId)); @@ -6887,7 +6881,7 @@ final class $$MessageRetransmissionsTableReferences extends BaseReferences< } class $$MessageRetransmissionsTableFilterComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableFilterComposer({ required super.$db, required super.$table, @@ -6961,7 +6955,7 @@ class $$MessageRetransmissionsTableFilterComposer } class $$MessageRetransmissionsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableOrderingComposer({ required super.$db, required super.$table, @@ -7036,7 +7030,7 @@ class $$MessageRetransmissionsTableOrderingComposer } class $$MessageRetransmissionsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableAnnotationComposer({ required super.$db, required super.$table, @@ -7107,7 +7101,7 @@ class $$MessageRetransmissionsTableAnnotationComposer } class $$MessageRetransmissionsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission, $$MessageRetransmissionsTableFilterComposer, @@ -7119,7 +7113,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager< MessageRetransmission, PrefetchHooks Function({bool contactId, bool messageId})> { $$MessageRetransmissionsTableTableManager( - _$TwonlyDatabase db, $MessageRetransmissionsTable table) + _$TwonlyDatabaseOld db, $MessageRetransmissionsTable table) : super(TableManagerState( db: db, table: table, @@ -7234,7 +7228,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager< typedef $$MessageRetransmissionsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission, $$MessageRetransmissionsTableFilterComposer, @@ -7246,9 +7240,9 @@ typedef $$MessageRetransmissionsTableProcessedTableManager MessageRetransmission, PrefetchHooks Function({bool contactId, bool messageId})>; -class $TwonlyDatabaseManager { - final _$TwonlyDatabase _db; - $TwonlyDatabaseManager(this._db); +class $TwonlyDatabaseOldManager { + final _$TwonlyDatabaseOld _db; + $TwonlyDatabaseOldManager(this._db); $$ContactsTableTableManager get contacts => $$ContactsTableTableManager(_db, _db.contacts); $$MessagesTableTableManager get messages => diff --git a/lib/src/database/twonly_database.steps.dart b/lib/src/database/twonly_database_old.steps.dart similarity index 100% rename from lib/src/database/twonly_database.steps.dart rename to lib/src/database/twonly_database_old.steps.dart diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message_old.dart similarity index 99% rename from lib/src/model/json/message.dart rename to lib/src/model/json/message_old.dart index 7a115fb..d783fc6 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message_old.dart @@ -1,7 +1,7 @@ // ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods import 'package:flutter/material.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables_old/messages_table.dart'; import 'package:twonly/src/utils/misc.dart'; Color getMessageColorFromType(MessageContent content, BuildContext context) { diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index 8e57d55..eafe7a8 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/api/media_upload.dart' as send; import 'package:twonly/src/services/thumbnail.service.dart'; diff --git a/lib/src/model/protobuf/backup/backup.proto b/lib/src/model/protobuf/client/backup.proto similarity index 100% rename from lib/src/model/protobuf/backup/backup.proto rename to lib/src/model/protobuf/client/backup.proto diff --git a/lib/src/model/protobuf/backup/backup.pb.dart b/lib/src/model/protobuf/client/generated/backup.pb.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pb.dart rename to lib/src/model/protobuf/client/generated/backup.pb.dart diff --git a/lib/src/model/protobuf/backup/backup.pbenum.dart b/lib/src/model/protobuf/client/generated/backup.pbenum.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbenum.dart rename to lib/src/model/protobuf/client/generated/backup.pbenum.dart diff --git a/lib/src/model/protobuf/backup/backup.pbjson.dart b/lib/src/model/protobuf/client/generated/backup.pbjson.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbjson.dart rename to lib/src/model/protobuf/client/generated/backup.pbjson.dart diff --git a/lib/src/model/protobuf/backup/backup.pbserver.dart b/lib/src/model/protobuf/client/generated/backup.pbserver.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbserver.dart rename to lib/src/model/protobuf/client/generated/backup.pbserver.dart diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart new file mode 100644 index 0000000..bc9d4bd --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -0,0 +1,1172 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'messages.pbenum.dart'; + +export 'messages.pbenum.dart'; + +class Message extends $pb.GeneratedMessage { + factory Message({ + Message_Type? type, + $core.String? receiptId, + $core.List<$core.int>? encryptedContent, + PlaintextContent? plaintextContent, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (receiptId != null) { + $result.receiptId = receiptId; + } + if (encryptedContent != null) { + $result.encryptedContent = encryptedContent; + } + if (plaintextContent != null) { + $result.plaintextContent = plaintextContent; + } + return $result; + } + Message._() : super(); + factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: Message_Type.SENDER_DELIVERY_RECEIPT, valueOf: Message_Type.valueOf, enumValues: Message_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'receiptId', protoName: 'receiptId') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedContent', $pb.PbFieldType.OY, protoName: 'encryptedContent') + ..aOM(4, _omitFieldNames ? '' : 'plaintextContent', protoName: 'plaintextContent', subBuilder: PlaintextContent.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message clone() => Message()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message copyWith(void Function(Message) updates) => super.copyWith((message) => updates(message as Message)) as Message; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message create() => Message._(); + Message createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message? _defaultInstance; + + @$pb.TagNumber(1) + Message_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(Message_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get receiptId => $_getSZ(1); + @$pb.TagNumber(2) + set receiptId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasReceiptId() => $_has(1); + @$pb.TagNumber(2) + void clearReceiptId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedContent => $_getN(2); + @$pb.TagNumber(3) + set encryptedContent($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedContent() => $_has(2); + @$pb.TagNumber(3) + void clearEncryptedContent() => clearField(3); + + @$pb.TagNumber(4) + PlaintextContent get plaintextContent => $_getN(3); + @$pb.TagNumber(4) + set plaintextContent(PlaintextContent v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasPlaintextContent() => $_has(3); + @$pb.TagNumber(4) + void clearPlaintextContent() => clearField(4); + @$pb.TagNumber(4) + PlaintextContent ensurePlaintextContent() => $_ensure(3); +} + +class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage { + factory PlaintextContent_DecryptionErrorMessage({ + PlaintextContent_DecryptionErrorMessage_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + PlaintextContent_DecryptionErrorMessage._() : super(); + factory PlaintextContent_DecryptionErrorMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent_DecryptionErrorMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent.DecryptionErrorMessage', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, valueOf: PlaintextContent_DecryptionErrorMessage_Type.valueOf, enumValues: PlaintextContent_DecryptionErrorMessage_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent_DecryptionErrorMessage clone() => PlaintextContent_DecryptionErrorMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent_DecryptionErrorMessage copyWith(void Function(PlaintextContent_DecryptionErrorMessage) updates) => super.copyWith((message) => updates(message as PlaintextContent_DecryptionErrorMessage)) as PlaintextContent_DecryptionErrorMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent_DecryptionErrorMessage create() => PlaintextContent_DecryptionErrorMessage._(); + PlaintextContent_DecryptionErrorMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent_DecryptionErrorMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent_DecryptionErrorMessage? _defaultInstance; + + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(PlaintextContent_DecryptionErrorMessage_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + +class PlaintextContent extends $pb.GeneratedMessage { + factory PlaintextContent({ + PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage, + }) { + final $result = create(); + if (decryptionErrorMessage != null) { + $result.decryptionErrorMessage = decryptionErrorMessage; + } + return $result; + } + PlaintextContent._() : super(); + factory PlaintextContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent', createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'decryptionErrorMessage', protoName: 'decryptionErrorMessage', subBuilder: PlaintextContent_DecryptionErrorMessage.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent clone() => PlaintextContent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent copyWith(void Function(PlaintextContent) updates) => super.copyWith((message) => updates(message as PlaintextContent)) as PlaintextContent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent create() => PlaintextContent._(); + PlaintextContent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent? _defaultInstance; + + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage get decryptionErrorMessage => $_getN(0); + @$pb.TagNumber(1) + set decryptionErrorMessage(PlaintextContent_DecryptionErrorMessage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDecryptionErrorMessage() => $_has(0); + @$pb.TagNumber(1) + void clearDecryptionErrorMessage() => clearField(1); + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0); +} + +class EncryptedContent_TextMessage extends $pb.GeneratedMessage { + factory EncryptedContent_TextMessage({ + $core.String? senderMessageId, + $core.String? text, + $core.String? quoteMessageId, + }) { + final $result = create(); + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (text != null) { + $result.text = text; + } + if (quoteMessageId != null) { + $result.quoteMessageId = quoteMessageId; + } + return $result; + } + EncryptedContent_TextMessage._() : super(); + factory EncryptedContent_TextMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_TextMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.TextMessage', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..aOS(2, _omitFieldNames ? '' : 'text') + ..aOS(3, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_TextMessage clone() => EncryptedContent_TextMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_TextMessage copyWith(void Function(EncryptedContent_TextMessage) updates) => super.copyWith((message) => updates(message as EncryptedContent_TextMessage)) as EncryptedContent_TextMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_TextMessage create() => EncryptedContent_TextMessage._(); + EncryptedContent_TextMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_TextMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_TextMessage? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get senderMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set senderMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasSenderMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearSenderMessageId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get text => $_getSZ(1); + @$pb.TagNumber(2) + set text($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasText() => $_has(1); + @$pb.TagNumber(2) + void clearText() => clearField(2); + + @$pb.TagNumber(3) + $core.String get quoteMessageId => $_getSZ(2); + @$pb.TagNumber(3) + set quoteMessageId($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasQuoteMessageId() => $_has(2); + @$pb.TagNumber(3) + void clearQuoteMessageId() => clearField(3); +} + +class EncryptedContent_Reaction extends $pb.GeneratedMessage { + factory EncryptedContent_Reaction({ + $core.String? targetMessageId, + $core.String? emoji, + $core.bool? remove, + }) { + final $result = create(); + if (targetMessageId != null) { + $result.targetMessageId = targetMessageId; + } + if (emoji != null) { + $result.emoji = emoji; + } + if (remove != null) { + $result.remove = remove; + } + return $result; + } + EncryptedContent_Reaction._() : super(); + factory EncryptedContent_Reaction.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_Reaction.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Reaction', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') + ..aOS(2, _omitFieldNames ? '' : 'emoji') + ..aOB(3, _omitFieldNames ? '' : 'remove') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_Reaction clone() => EncryptedContent_Reaction()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_Reaction copyWith(void Function(EncryptedContent_Reaction) updates) => super.copyWith((message) => updates(message as EncryptedContent_Reaction)) as EncryptedContent_Reaction; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_Reaction create() => EncryptedContent_Reaction._(); + EncryptedContent_Reaction createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_Reaction getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_Reaction? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get targetMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set targetMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTargetMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearTargetMessageId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get emoji => $_getSZ(1); + @$pb.TagNumber(2) + set emoji($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasEmoji() => $_has(1); + @$pb.TagNumber(2) + void clearEmoji() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get remove => $_getBF(2); + @$pb.TagNumber(3) + set remove($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasRemove() => $_has(2); + @$pb.TagNumber(3) + void clearRemove() => clearField(3); +} + +class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_MessageUpdate({ + EncryptedContent_MessageUpdate_Type? type, + $core.String? senderMessageId, + $core.String? text, + $fixnum.Int64? timestamp, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (text != null) { + $result.text = text; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + return $result; + } + EncryptedContent_MessageUpdate._() : super(); + factory EncryptedContent_MessageUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_MessageUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..aOS(3, _omitFieldNames ? '' : 'text') + ..aInt64(4, _omitFieldNames ? '' : 'timestamp') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_MessageUpdate clone() => EncryptedContent_MessageUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_MessageUpdate copyWith(void Function(EncryptedContent_MessageUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_MessageUpdate)) as EncryptedContent_MessageUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_MessageUpdate create() => EncryptedContent_MessageUpdate._(); + EncryptedContent_MessageUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_MessageUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_MessageUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_MessageUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_MessageUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get senderMessageId => $_getSZ(1); + @$pb.TagNumber(2) + set senderMessageId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasSenderMessageId() => $_has(1); + @$pb.TagNumber(2) + void clearSenderMessageId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get text => $_getSZ(2); + @$pb.TagNumber(3) + set text($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasText() => $_has(2); + @$pb.TagNumber(3) + void clearText() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get timestamp => $_getI64(3); + @$pb.TagNumber(4) + set timestamp($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasTimestamp() => $_has(3); + @$pb.TagNumber(4) + void clearTimestamp() => clearField(4); +} + +class EncryptedContent_Media extends $pb.GeneratedMessage { + factory EncryptedContent_Media({ + $core.String? senderMessageId, + EncryptedContent_Media_Type? type, + $fixnum.Int64? displayLimitInMilliseconds, + $core.bool? requiresAuthentication, + $core.List<$core.int>? downloadToken, + $core.List<$core.int>? encryptionKey, + $core.List<$core.int>? encryptionMac, + $core.List<$core.int>? encryptionNonce, + }) { + final $result = create(); + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (type != null) { + $result.type = type; + } + if (displayLimitInMilliseconds != null) { + $result.displayLimitInMilliseconds = displayLimitInMilliseconds; + } + if (requiresAuthentication != null) { + $result.requiresAuthentication = requiresAuthentication; + } + if (downloadToken != null) { + $result.downloadToken = downloadToken; + } + if (encryptionKey != null) { + $result.encryptionKey = encryptionKey; + } + if (encryptionMac != null) { + $result.encryptionMac = encryptionMac; + } + if (encryptionNonce != null) { + $result.encryptionNonce = encryptionNonce; + } + return $result; + } + EncryptedContent_Media._() : super(); + factory EncryptedContent_Media.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_Media.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Media', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..e(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.IMAGE, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values) + ..aInt64(3, _omitFieldNames ? '' : 'displayLimitInMilliseconds', protoName: 'displayLimitInMilliseconds') + ..aOB(4, _omitFieldNames ? '' : 'requiresAuthentication', protoName: 'requiresAuthentication') + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken') + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey') + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac') + ..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_Media clone() => EncryptedContent_Media()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_Media copyWith(void Function(EncryptedContent_Media) updates) => super.copyWith((message) => updates(message as EncryptedContent_Media)) as EncryptedContent_Media; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_Media create() => EncryptedContent_Media._(); + EncryptedContent_Media createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_Media getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_Media? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get senderMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set senderMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasSenderMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearSenderMessageId() => clearField(1); + + @$pb.TagNumber(2) + EncryptedContent_Media_Type get type => $_getN(1); + @$pb.TagNumber(2) + set type(EncryptedContent_Media_Type v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasType() => $_has(1); + @$pb.TagNumber(2) + void clearType() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get displayLimitInMilliseconds => $_getI64(2); + @$pb.TagNumber(3) + set displayLimitInMilliseconds($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasDisplayLimitInMilliseconds() => $_has(2); + @$pb.TagNumber(3) + void clearDisplayLimitInMilliseconds() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get requiresAuthentication => $_getBF(3); + @$pb.TagNumber(4) + set requiresAuthentication($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasRequiresAuthentication() => $_has(3); + @$pb.TagNumber(4) + void clearRequiresAuthentication() => clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get downloadToken => $_getN(4); + @$pb.TagNumber(5) + set downloadToken($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(5) + $core.bool hasDownloadToken() => $_has(4); + @$pb.TagNumber(5) + void clearDownloadToken() => clearField(5); + + @$pb.TagNumber(6) + $core.List<$core.int> get encryptionKey => $_getN(5); + @$pb.TagNumber(6) + set encryptionKey($core.List<$core.int> v) { $_setBytes(5, v); } + @$pb.TagNumber(6) + $core.bool hasEncryptionKey() => $_has(5); + @$pb.TagNumber(6) + void clearEncryptionKey() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get encryptionMac => $_getN(6); + @$pb.TagNumber(7) + set encryptionMac($core.List<$core.int> v) { $_setBytes(6, v); } + @$pb.TagNumber(7) + $core.bool hasEncryptionMac() => $_has(6); + @$pb.TagNumber(7) + void clearEncryptionMac() => clearField(7); + + @$pb.TagNumber(8) + $core.List<$core.int> get encryptionNonce => $_getN(7); + @$pb.TagNumber(8) + set encryptionNonce($core.List<$core.int> v) { $_setBytes(7, v); } + @$pb.TagNumber(8) + $core.bool hasEncryptionNonce() => $_has(7); + @$pb.TagNumber(8) + void clearEncryptionNonce() => clearField(8); +} + +class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_MediaUpdate({ + EncryptedContent_MediaUpdate_Type? type, + $core.String? targetMessageId, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (targetMessageId != null) { + $result.targetMessageId = targetMessageId; + } + return $result; + } + EncryptedContent_MediaUpdate._() : super(); + factory EncryptedContent_MediaUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_MediaUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_MediaUpdate clone() => EncryptedContent_MediaUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_MediaUpdate copyWith(void Function(EncryptedContent_MediaUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_MediaUpdate)) as EncryptedContent_MediaUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_MediaUpdate create() => EncryptedContent_MediaUpdate._(); + EncryptedContent_MediaUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_MediaUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_MediaUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_MediaUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_MediaUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get targetMessageId => $_getSZ(1); + @$pb.TagNumber(2) + set targetMessageId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasTargetMessageId() => $_has(1); + @$pb.TagNumber(2) + void clearTargetMessageId() => clearField(2); +} + +class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { + factory EncryptedContent_ContactRequest({ + EncryptedContent_ContactRequest_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + EncryptedContent_ContactRequest._() : super(); + factory EncryptedContent_ContactRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ContactRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactRequest', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactRequest_Type.REQUEST, valueOf: EncryptedContent_ContactRequest_Type.valueOf, enumValues: EncryptedContent_ContactRequest_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactRequest clone() => EncryptedContent_ContactRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactRequest copyWith(void Function(EncryptedContent_ContactRequest) updates) => super.copyWith((message) => updates(message as EncryptedContent_ContactRequest)) as EncryptedContent_ContactRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactRequest create() => EncryptedContent_ContactRequest._(); + EncryptedContent_ContactRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ContactRequest? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_ContactRequest_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_ContactRequest_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + +class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_ContactUpdate({ + EncryptedContent_ContactUpdate_Type? type, + $core.String? avatarSvg, + $core.String? displayName, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (avatarSvg != null) { + $result.avatarSvg = avatarSvg; + } + if (displayName != null) { + $result.displayName = displayName; + } + return $result; + } + EncryptedContent_ContactUpdate._() : super(); + factory EncryptedContent_ContactUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ContactUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactUpdate_Type.REQUEST, valueOf: EncryptedContent_ContactUpdate_Type.valueOf, enumValues: EncryptedContent_ContactUpdate_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'avatarSvg', protoName: 'avatarSvg') + ..aOS(3, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactUpdate clone() => EncryptedContent_ContactUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactUpdate copyWith(void Function(EncryptedContent_ContactUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_ContactUpdate)) as EncryptedContent_ContactUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactUpdate create() => EncryptedContent_ContactUpdate._(); + EncryptedContent_ContactUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ContactUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_ContactUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_ContactUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get avatarSvg => $_getSZ(1); + @$pb.TagNumber(2) + set avatarSvg($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasAvatarSvg() => $_has(1); + @$pb.TagNumber(2) + void clearAvatarSvg() => clearField(2); + + @$pb.TagNumber(3) + $core.String get displayName => $_getSZ(2); + @$pb.TagNumber(3) + set displayName($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasDisplayName() => $_has(2); + @$pb.TagNumber(3) + void clearDisplayName() => clearField(3); +} + +class EncryptedContent_PushKeys extends $pb.GeneratedMessage { + factory EncryptedContent_PushKeys({ + EncryptedContent_PushKeys_Type? type, + $fixnum.Int64? keyId, + $core.List<$core.int>? key, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (keyId != null) { + $result.keyId = keyId; + } + if (key != null) { + $result.key = key; + } + return $result; + } + EncryptedContent_PushKeys._() : super(); + factory EncryptedContent_PushKeys.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_PushKeys.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.PushKeys', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_PushKeys_Type.REQUEST, valueOf: EncryptedContent_PushKeys_Type.valueOf, enumValues: EncryptedContent_PushKeys_Type.values) + ..aInt64(2, _omitFieldNames ? '' : 'keyId', protoName: 'keyId') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_PushKeys clone() => EncryptedContent_PushKeys()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_PushKeys copyWith(void Function(EncryptedContent_PushKeys) updates) => super.copyWith((message) => updates(message as EncryptedContent_PushKeys)) as EncryptedContent_PushKeys; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_PushKeys create() => EncryptedContent_PushKeys._(); + EncryptedContent_PushKeys createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_PushKeys getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_PushKeys? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_PushKeys_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_PushKeys_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get keyId => $_getI64(1); + @$pb.TagNumber(2) + set keyId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasKeyId() => $_has(1); + @$pb.TagNumber(2) + void clearKeyId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get key => $_getN(2); + @$pb.TagNumber(3) + set key($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasKey() => $_has(2); + @$pb.TagNumber(3) + void clearKey() => clearField(3); +} + +class EncryptedContent_FlameSync extends $pb.GeneratedMessage { + factory EncryptedContent_FlameSync({ + $fixnum.Int64? flameCounter, + $fixnum.Int64? lastFlameCounterChange, + $core.bool? bestFriend, + }) { + final $result = create(); + if (flameCounter != null) { + $result.flameCounter = flameCounter; + } + if (lastFlameCounterChange != null) { + $result.lastFlameCounterChange = lastFlameCounterChange; + } + if (bestFriend != null) { + $result.bestFriend = bestFriend; + } + return $result; + } + EncryptedContent_FlameSync._() : super(); + factory EncryptedContent_FlameSync.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_FlameSync.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.FlameSync', createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter') + ..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange') + ..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_FlameSync clone() => EncryptedContent_FlameSync()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_FlameSync copyWith(void Function(EncryptedContent_FlameSync) updates) => super.copyWith((message) => updates(message as EncryptedContent_FlameSync)) as EncryptedContent_FlameSync; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_FlameSync create() => EncryptedContent_FlameSync._(); + EncryptedContent_FlameSync createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_FlameSync getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_FlameSync? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get flameCounter => $_getI64(0); + @$pb.TagNumber(1) + set flameCounter($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasFlameCounter() => $_has(0); + @$pb.TagNumber(1) + void clearFlameCounter() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get lastFlameCounterChange => $_getI64(1); + @$pb.TagNumber(2) + set lastFlameCounterChange($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasLastFlameCounterChange() => $_has(1); + @$pb.TagNumber(2) + void clearLastFlameCounterChange() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get bestFriend => $_getBF(2); + @$pb.TagNumber(3) + set bestFriend($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasBestFriend() => $_has(2); + @$pb.TagNumber(3) + void clearBestFriend() => clearField(3); +} + +class EncryptedContent extends $pb.GeneratedMessage { + factory EncryptedContent({ + $core.String? groupId, + $fixnum.Int64? senderProfileCounter, + EncryptedContent_TextMessage? textMessage, + EncryptedContent_MessageUpdate? messageUpdate, + EncryptedContent_Media? media, + EncryptedContent_MediaUpdate? mediaUpdate, + EncryptedContent_ContactUpdate? contactUpdate, + EncryptedContent_ContactRequest? contactRequest, + EncryptedContent_FlameSync? flameSync, + EncryptedContent_PushKeys? pushKeys, + EncryptedContent_Reaction? reaction, + }) { + final $result = create(); + if (groupId != null) { + $result.groupId = groupId; + } + if (senderProfileCounter != null) { + $result.senderProfileCounter = senderProfileCounter; + } + if (textMessage != null) { + $result.textMessage = textMessage; + } + if (messageUpdate != null) { + $result.messageUpdate = messageUpdate; + } + if (media != null) { + $result.media = media; + } + if (mediaUpdate != null) { + $result.mediaUpdate = mediaUpdate; + } + if (contactUpdate != null) { + $result.contactUpdate = contactUpdate; + } + if (contactRequest != null) { + $result.contactRequest = contactRequest; + } + if (flameSync != null) { + $result.flameSync = flameSync; + } + if (pushKeys != null) { + $result.pushKeys = pushKeys; + } + if (reaction != null) { + $result.reaction = reaction; + } + return $result; + } + EncryptedContent._() : super(); + factory EncryptedContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create) + ..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') + ..aInt64(3, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter') + ..aOM(4, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) + ..aOM(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create) + ..aOM(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create) + ..aOM(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.create) + ..aOM(8, _omitFieldNames ? '' : 'contactUpdate', protoName: 'contactUpdate', subBuilder: EncryptedContent_ContactUpdate.create) + ..aOM(9, _omitFieldNames ? '' : 'contactRequest', protoName: 'contactRequest', subBuilder: EncryptedContent_ContactRequest.create) + ..aOM(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create) + ..aOM(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create) + ..aOM(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent clone() => EncryptedContent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent copyWith(void Function(EncryptedContent) updates) => super.copyWith((message) => updates(message as EncryptedContent)) as EncryptedContent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent create() => EncryptedContent._(); + EncryptedContent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent? _defaultInstance; + + @$pb.TagNumber(2) + $core.String get groupId => $_getSZ(0); + @$pb.TagNumber(2) + set groupId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(2) + $core.bool hasGroupId() => $_has(0); + @$pb.TagNumber(2) + void clearGroupId() => clearField(2); + + /// / This can be added, so the receiver can check weather he is up to date with the current profile + @$pb.TagNumber(3) + $fixnum.Int64 get senderProfileCounter => $_getI64(1); + @$pb.TagNumber(3) + set senderProfileCounter($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(3) + $core.bool hasSenderProfileCounter() => $_has(1); + @$pb.TagNumber(3) + void clearSenderProfileCounter() => clearField(3); + + @$pb.TagNumber(4) + EncryptedContent_TextMessage get textMessage => $_getN(2); + @$pb.TagNumber(4) + set textMessage(EncryptedContent_TextMessage v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasTextMessage() => $_has(2); + @$pb.TagNumber(4) + void clearTextMessage() => clearField(4); + @$pb.TagNumber(4) + EncryptedContent_TextMessage ensureTextMessage() => $_ensure(2); + + @$pb.TagNumber(5) + EncryptedContent_MessageUpdate get messageUpdate => $_getN(3); + @$pb.TagNumber(5) + set messageUpdate(EncryptedContent_MessageUpdate v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasMessageUpdate() => $_has(3); + @$pb.TagNumber(5) + void clearMessageUpdate() => clearField(5); + @$pb.TagNumber(5) + EncryptedContent_MessageUpdate ensureMessageUpdate() => $_ensure(3); + + @$pb.TagNumber(6) + EncryptedContent_Media get media => $_getN(4); + @$pb.TagNumber(6) + set media(EncryptedContent_Media v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasMedia() => $_has(4); + @$pb.TagNumber(6) + void clearMedia() => clearField(6); + @$pb.TagNumber(6) + EncryptedContent_Media ensureMedia() => $_ensure(4); + + @$pb.TagNumber(7) + EncryptedContent_MediaUpdate get mediaUpdate => $_getN(5); + @$pb.TagNumber(7) + set mediaUpdate(EncryptedContent_MediaUpdate v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasMediaUpdate() => $_has(5); + @$pb.TagNumber(7) + void clearMediaUpdate() => clearField(7); + @$pb.TagNumber(7) + EncryptedContent_MediaUpdate ensureMediaUpdate() => $_ensure(5); + + @$pb.TagNumber(8) + EncryptedContent_ContactUpdate get contactUpdate => $_getN(6); + @$pb.TagNumber(8) + set contactUpdate(EncryptedContent_ContactUpdate v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasContactUpdate() => $_has(6); + @$pb.TagNumber(8) + void clearContactUpdate() => clearField(8); + @$pb.TagNumber(8) + EncryptedContent_ContactUpdate ensureContactUpdate() => $_ensure(6); + + @$pb.TagNumber(9) + EncryptedContent_ContactRequest get contactRequest => $_getN(7); + @$pb.TagNumber(9) + set contactRequest(EncryptedContent_ContactRequest v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasContactRequest() => $_has(7); + @$pb.TagNumber(9) + void clearContactRequest() => clearField(9); + @$pb.TagNumber(9) + EncryptedContent_ContactRequest ensureContactRequest() => $_ensure(7); + + @$pb.TagNumber(10) + EncryptedContent_FlameSync get flameSync => $_getN(8); + @$pb.TagNumber(10) + set flameSync(EncryptedContent_FlameSync v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasFlameSync() => $_has(8); + @$pb.TagNumber(10) + void clearFlameSync() => clearField(10); + @$pb.TagNumber(10) + EncryptedContent_FlameSync ensureFlameSync() => $_ensure(8); + + @$pb.TagNumber(11) + EncryptedContent_PushKeys get pushKeys => $_getN(9); + @$pb.TagNumber(11) + set pushKeys(EncryptedContent_PushKeys v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasPushKeys() => $_has(9); + @$pb.TagNumber(11) + void clearPushKeys() => clearField(11); + @$pb.TagNumber(11) + EncryptedContent_PushKeys ensurePushKeys() => $_ensure(9); + + @$pb.TagNumber(12) + EncryptedContent_Reaction get reaction => $_getN(10); + @$pb.TagNumber(12) + set reaction(EncryptedContent_Reaction v) { setField(12, v); } + @$pb.TagNumber(12) + $core.bool hasReaction() => $_has(10); + @$pb.TagNumber(12) + void clearReaction() => clearField(12); + @$pb.TagNumber(12) + EncryptedContent_Reaction ensureReaction() => $_ensure(10); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart new file mode 100644 index 0000000..37118a9 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -0,0 +1,149 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Message_Type extends $pb.ProtobufEnum { + static const Message_Type SENDER_DELIVERY_RECEIPT = Message_Type._(0, _omitEnumNames ? '' : 'SENDER_DELIVERY_RECEIPT'); + static const Message_Type PLAINTEXT_CONTENT = Message_Type._(1, _omitEnumNames ? '' : 'PLAINTEXT_CONTENT'); + static const Message_Type CIPHERTEXT = Message_Type._(2, _omitEnumNames ? '' : 'CIPHERTEXT'); + static const Message_Type PREKEY_BUNDLE = Message_Type._(3, _omitEnumNames ? '' : 'PREKEY_BUNDLE'); + + static const $core.List values = [ + SENDER_DELIVERY_RECEIPT, + PLAINTEXT_CONTENT, + CIPHERTEXT, + PREKEY_BUNDLE, + ]; + + static final $core.Map<$core.int, Message_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static Message_Type? valueOf($core.int value) => _byValue[value]; + + const Message_Type._($core.int v, $core.String n) : super(v, n); +} + +class PlaintextContent_DecryptionErrorMessage_Type extends $pb.ProtobufEnum { + static const PlaintextContent_DecryptionErrorMessage_Type UNKNOWN = PlaintextContent_DecryptionErrorMessage_Type._(0, _omitEnumNames ? '' : 'UNKNOWN'); + static const PlaintextContent_DecryptionErrorMessage_Type PREKEY_UNKNOWN = PlaintextContent_DecryptionErrorMessage_Type._(1, _omitEnumNames ? '' : 'PREKEY_UNKNOWN'); + + static const $core.List values = [ + UNKNOWN, + PREKEY_UNKNOWN, + ]; + + static final $core.Map<$core.int, PlaintextContent_DecryptionErrorMessage_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static PlaintextContent_DecryptionErrorMessage_Type? valueOf($core.int value) => _byValue[value]; + + const PlaintextContent_DecryptionErrorMessage_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_MessageUpdate_Type DELETE = EncryptedContent_MessageUpdate_Type._(0, _omitEnumNames ? '' : 'DELETE'); + static const EncryptedContent_MessageUpdate_Type EDIT_TEXT = EncryptedContent_MessageUpdate_Type._(1, _omitEnumNames ? '' : 'EDIT_TEXT'); + static const EncryptedContent_MessageUpdate_Type OPENED = EncryptedContent_MessageUpdate_Type._(2, _omitEnumNames ? '' : 'OPENED'); + + static const $core.List values = [ + DELETE, + EDIT_TEXT, + OPENED, + ]; + + static final $core.Map<$core.int, EncryptedContent_MessageUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_MessageUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_MessageUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_Media_Type extends $pb.ProtobufEnum { + static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'IMAGE'); + static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'VIDEO'); + static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'GIF'); + + static const $core.List values = [ + IMAGE, + VIDEO, + GIF, + ]; + + static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_Media_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_Media_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_MediaUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_MediaUpdate_Type REOPENED = EncryptedContent_MediaUpdate_Type._(0, _omitEnumNames ? '' : 'REOPENED'); + static const EncryptedContent_MediaUpdate_Type STORED = EncryptedContent_MediaUpdate_Type._(1, _omitEnumNames ? '' : 'STORED'); + static const EncryptedContent_MediaUpdate_Type DECRYPTION_ERROR = EncryptedContent_MediaUpdate_Type._(2, _omitEnumNames ? '' : 'DECRYPTION_ERROR'); + + static const $core.List values = [ + REOPENED, + STORED, + DECRYPTION_ERROR, + ]; + + static final $core.Map<$core.int, EncryptedContent_MediaUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_MediaUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_MediaUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_ContactRequest_Type extends $pb.ProtobufEnum { + static const EncryptedContent_ContactRequest_Type REQUEST = EncryptedContent_ContactRequest_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_ContactRequest_Type REJECT = EncryptedContent_ContactRequest_Type._(1, _omitEnumNames ? '' : 'REJECT'); + static const EncryptedContent_ContactRequest_Type ACCEPT = EncryptedContent_ContactRequest_Type._(2, _omitEnumNames ? '' : 'ACCEPT'); + + static const $core.List values = [ + REQUEST, + REJECT, + ACCEPT, + ]; + + static final $core.Map<$core.int, EncryptedContent_ContactRequest_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_ContactRequest_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_ContactRequest_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_ContactUpdate_Type REQUEST = EncryptedContent_ContactUpdate_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_ContactUpdate_Type UPDATE = EncryptedContent_ContactUpdate_Type._(1, _omitEnumNames ? '' : 'UPDATE'); + + static const $core.List values = [ + REQUEST, + UPDATE, + ]; + + static final $core.Map<$core.int, EncryptedContent_ContactUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_ContactUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_ContactUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_PushKeys_Type extends $pb.ProtobufEnum { + static const EncryptedContent_PushKeys_Type REQUEST = EncryptedContent_PushKeys_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_PushKeys_Type UPDATE = EncryptedContent_PushKeys_Type._(1, _omitEnumNames ? '' : 'UPDATE'); + + static const $core.List values = [ + REQUEST, + UPDATE, + ]; + + static final $core.Map<$core.int, EncryptedContent_PushKeys_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_PushKeys_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_PushKeys_Type._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart new file mode 100644 index 0000000..9da2b0c --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -0,0 +1,358 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use messageDescriptor instead') +const Message$json = { + '1': 'Message', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.Message.Type', '10': 'type'}, + {'1': 'receiptId', '3': 2, '4': 1, '5': 9, '10': 'receiptId'}, + {'1': 'encryptedContent', '3': 3, '4': 1, '5': 12, '9': 0, '10': 'encryptedContent', '17': true}, + {'1': 'plaintextContent', '3': 4, '4': 1, '5': 11, '6': '.PlaintextContent', '9': 1, '10': 'plaintextContent', '17': true}, + ], + '4': [Message_Type$json], + '8': [ + {'1': '_encryptedContent'}, + {'1': '_plaintextContent'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Type$json = { + '1': 'Type', + '2': [ + {'1': 'SENDER_DELIVERY_RECEIPT', '2': 0}, + {'1': 'PLAINTEXT_CONTENT', '2': 1}, + {'1': 'CIPHERTEXT', '2': 2}, + {'1': 'PREKEY_BUNDLE', '2': 3}, + ], +}; + +/// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( + 'CgdNZXNzYWdlEiEKBHR5cGUYASABKA4yDS5NZXNzYWdlLlR5cGVSBHR5cGUSHAoJcmVjZWlwdE' + 'lkGAIgASgJUglyZWNlaXB0SWQSLwoQZW5jcnlwdGVkQ29udGVudBgDIAEoDEgAUhBlbmNyeXB0' + 'ZWRDb250ZW50iAEBEkIKEHBsYWludGV4dENvbnRlbnQYBCABKAsyES5QbGFpbnRleHRDb250ZW' + '50SAFSEHBsYWludGV4dENvbnRlbnSIAQEiXQoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD' + 'RUlQVBAAEhUKEVBMQUlOVEVYVF9DT05URU5UEAESDgoKQ0lQSEVSVEVYVBACEhEKDVBSRUtFWV' + '9CVU5ETEUQA0ITChFfZW5jcnlwdGVkQ29udGVudEITChFfcGxhaW50ZXh0Q29udGVudA=='); + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent$json = { + '1': 'PlaintextContent', + '2': [ + {'1': 'decryptionErrorMessage', '3': 1, '4': 1, '5': 11, '6': '.PlaintextContent.DecryptionErrorMessage', '9': 0, '10': 'decryptionErrorMessage', '17': true}, + ], + '3': [PlaintextContent_DecryptionErrorMessage$json], + '8': [ + {'1': '_decryptionErrorMessage'}, + ], +}; + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_DecryptionErrorMessage$json = { + '1': 'DecryptionErrorMessage', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.PlaintextContent.DecryptionErrorMessage.Type', '10': 'type'}, + ], + '4': [PlaintextContent_DecryptionErrorMessage_Type$json], +}; + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_DecryptionErrorMessage_Type$json = { + '1': 'Type', + '2': [ + {'1': 'UNKNOWN', '2': 0}, + {'1': 'PREKEY_UNKNOWN', '2': 1}, + ], +}; + +/// Descriptor for `PlaintextContent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode( + 'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG' + 'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y' + 'TWVzc2FnZYgBARqEAQoWRGVjcnlwdGlvbkVycm9yTWVzc2FnZRJBCgR0eXBlGAEgASgOMi0uUG' + 'xhaW50ZXh0Q29udGVudC5EZWNyeXB0aW9uRXJyb3JNZXNzYWdlLlR5cGVSBHR5cGUiJwoEVHlw' + 'ZRILCgdVTktOT1dOEAASEgoOUFJFS0VZX1VOS05PV04QAUIZChdfZGVjcnlwdGlvbkVycm9yTW' + 'Vzc2FnZQ=='); + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent$json = { + '1': 'EncryptedContent', + '2': [ + {'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true}, + {'1': 'senderProfileCounter', '3': 3, '4': 1, '5': 3, '9': 1, '10': 'senderProfileCounter', '17': true}, + {'1': 'textMessage', '3': 4, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 2, '10': 'textMessage', '17': true}, + {'1': 'messageUpdate', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', '9': 3, '10': 'messageUpdate', '17': true}, + {'1': 'media', '3': 6, '4': 1, '5': 11, '6': '.EncryptedContent.Media', '9': 4, '10': 'media', '17': true}, + {'1': 'mediaUpdate', '3': 7, '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', '9': 5, '10': 'mediaUpdate', '17': true}, + {'1': 'contactUpdate', '3': 8, '4': 1, '5': 11, '6': '.EncryptedContent.ContactUpdate', '9': 6, '10': 'contactUpdate', '17': true}, + {'1': 'contactRequest', '3': 9, '4': 1, '5': 11, '6': '.EncryptedContent.ContactRequest', '9': 7, '10': 'contactRequest', '17': true}, + {'1': 'flameSync', '3': 10, '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', '9': 8, '10': 'flameSync', '17': true}, + {'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true}, + {'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true}, + ], + '3': [EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], + '8': [ + {'1': '_groupId'}, + {'1': '_senderProfileCounter'}, + {'1': '_textMessage'}, + {'1': '_messageUpdate'}, + {'1': '_media'}, + {'1': '_mediaUpdate'}, + {'1': '_contactUpdate'}, + {'1': '_contactRequest'}, + {'1': '_flameSync'}, + {'1': '_pushKeys'}, + {'1': '_reaction'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_TextMessage$json = { + '1': 'TextMessage', + '2': [ + {'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'}, + {'1': 'quoteMessageId', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true}, + ], + '8': [ + {'1': '_quoteMessageId'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Reaction$json = { + '1': 'Reaction', + '2': [ + {'1': 'targetMessageId', '3': 1, '4': 1, '5': 9, '10': 'targetMessageId'}, + {'1': 'emoji', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'emoji', '17': true}, + {'1': 'remove', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'remove', '17': true}, + ], + '8': [ + {'1': '_emoji'}, + {'1': '_remove'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MessageUpdate$json = { + '1': 'MessageUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'}, + {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'text', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'text', '17': true}, + {'1': 'timestamp', '3': 4, '4': 1, '5': 3, '9': 1, '10': 'timestamp', '17': true}, + ], + '4': [EncryptedContent_MessageUpdate_Type$json], + '8': [ + {'1': '_text'}, + {'1': '_timestamp'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MessageUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'DELETE', '2': 0}, + {'1': 'EDIT_TEXT', '2': 1}, + {'1': 'OPENED', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Media$json = { + '1': 'Media', + '2': [ + {'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.EncryptedContent.Media.Type', '10': 'type'}, + {'1': 'displayLimitInMilliseconds', '3': 3, '4': 1, '5': 3, '9': 0, '10': 'displayLimitInMilliseconds', '17': true}, + {'1': 'requiresAuthentication', '3': 4, '4': 1, '5': 8, '10': 'requiresAuthentication'}, + {'1': 'downloadToken', '3': 5, '4': 1, '5': 12, '9': 1, '10': 'downloadToken', '17': true}, + {'1': 'encryptionKey', '3': 6, '4': 1, '5': 12, '9': 2, '10': 'encryptionKey', '17': true}, + {'1': 'encryptionMac', '3': 7, '4': 1, '5': 12, '9': 3, '10': 'encryptionMac', '17': true}, + {'1': 'encryptionNonce', '3': 8, '4': 1, '5': 12, '9': 4, '10': 'encryptionNonce', '17': true}, + ], + '4': [EncryptedContent_Media_Type$json], + '8': [ + {'1': '_displayLimitInMilliseconds'}, + {'1': '_downloadToken'}, + {'1': '_encryptionKey'}, + {'1': '_encryptionMac'}, + {'1': '_encryptionNonce'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Media_Type$json = { + '1': 'Type', + '2': [ + {'1': 'IMAGE', '2': 0}, + {'1': 'VIDEO', '2': 1}, + {'1': 'GIF', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MediaUpdate$json = { + '1': 'MediaUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, + {'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'}, + ], + '4': [EncryptedContent_MediaUpdate_Type$json], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MediaUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REOPENED', '2': 0}, + {'1': 'STORED', '2': 1}, + {'1': 'DECRYPTION_ERROR', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactRequest$json = { + '1': 'ContactRequest', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactRequest.Type', '10': 'type'}, + ], + '4': [EncryptedContent_ContactRequest_Type$json], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactRequest_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'REJECT', '2': 1}, + {'1': 'ACCEPT', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactUpdate$json = { + '1': 'ContactUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactUpdate.Type', '10': 'type'}, + {'1': 'avatarSvg', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'avatarSvg', '17': true}, + {'1': 'displayName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'displayName', '17': true}, + ], + '4': [EncryptedContent_ContactUpdate_Type$json], + '8': [ + {'1': '_avatarSvg'}, + {'1': '_displayName'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'UPDATE', '2': 1}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_PushKeys$json = { + '1': 'PushKeys', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.PushKeys.Type', '10': 'type'}, + {'1': 'keyId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true}, + {'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true}, + ], + '4': [EncryptedContent_PushKeys_Type$json], + '8': [ + {'1': '_keyId'}, + {'1': '_key'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_PushKeys_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'UPDATE', '2': 1}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_FlameSync$json = { + '1': 'FlameSync', + '2': [ + {'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'}, + {'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'}, + {'1': 'bestFriend', '3': 3, '4': 1, '5': 8, '10': 'bestFriend'}, + ], +}; + +/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( + 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARI3ChRzZW' + '5kZXJQcm9maWxlQ291bnRlchgDIAEoA0gBUhRzZW5kZXJQcm9maWxlQ291bnRlcogBARJECgt0' + 'ZXh0TWVzc2FnZRgEIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIAlILdGV4dE' + '1lc3NhZ2WIAQESSgoNbWVzc2FnZVVwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVz' + 'c2FnZVVwZGF0ZUgDUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdG' + 'VkQ29udGVudC5NZWRpYUgEUgVtZWRpYYgBARJECgttZWRpYVVwZGF0ZRgHIAEoCzIdLkVuY3J5' + 'cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIBVILbWVkaWFVcGRhdGWIAQESSgoNY29udGFjdFVwZG' + 'F0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgGUg1jb250YWN0VXBk' + 'YXRliAEBEk0KDmNvbnRhY3RSZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YW' + 'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy' + 'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA' + 'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u' + 'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqLAQoLVG' + 'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' + 'dGV4dBgCIAEoCVIEdGV4dBIrCg5xdW90ZU1lc3NhZ2VJZBgDIAEoCUgAUg5xdW90ZU1lc3NhZ2' + 'VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQagQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJ' + 'ZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhkKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBn' + 'JlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFCCAoGX2Vtb2ppQgkKB19yZW1vdmUa9QEKDU1lc3Nh' + 'Z2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS' + '5UeXBlUgR0eXBlEigKD3NlbmRlck1lc3NhZ2VJZBgCIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhcK' + 'BHRleHQYAyABKAlIAFIEdGV4dIgBARIhCgl0aW1lc3RhbXAYBCABKANIAVIJdGltZXN0YW1wiA' + 'EBIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCBwoFX3Rl' + 'eHRCDAoKX3RpbWVzdGFtcBqgBAoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW' + '5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlw' + 'ZVIEdHlwZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TG' + 'ltaXRJbk1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZy' + 'ZXF1aXJlc0F1dGhlbnRpY2F0aW9uEikKDWRvd25sb2FkVG9rZW4YBSABKAxIAVINZG93bmxvYW' + 'RUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAYgASgMSAJSDWVuY3J5cHRpb25LZXmIAQESKQoN' + 'ZW5jcnlwdGlvbk1hYxgHIAEoDEgDUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb25Ob2' + '5jZRgIIAEoDEgEUg9lbmNyeXB0aW9uTm9uY2WIAQEiJQoEVHlwZRIJCgVJTUFHRRAAEgkKBVZJ' + 'REVPEAESBwoDR0lGEAJCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhAKDl9kb3dubG' + '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0' + 'aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW' + '50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJn' + 'ZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUF' + 'RJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVk' + 'Q29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEg' + 'oKBlJFSkVDVBABEgoKBkFDQ0VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4y' + 'JC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdm' + 'cYAiABKAlIAFIJYXZhdGFyU3ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlO' + 'YW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDg' + 'oMX2Rpc3BsYXlOYW1lGqQBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29u' + 'dGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2' + 'tleRgDIAEoDEgBUgNrZXmIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoG' + 'X2tleUlkQgYKBF9rZXkahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW' + '1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3Vu' + 'dGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCFw' + 'oVX3NlbmRlclByb2ZpbGVDb3VudGVyQg4KDF90ZXh0TWVzc2FnZUIQCg5fbWVzc2FnZVVwZGF0' + 'ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YW' + 'N0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb24='); + diff --git a/lib/src/model/protobuf/client/generated/messages.pbserver.dart b/lib/src/model/protobuf/client/generated/messages.pbserver.dart new file mode 100644 index 0000000..956b01d --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'messages.pb.dart'; + diff --git a/lib/src/model/protobuf/push_notification/push_notification.pb.dart b/lib/src/model/protobuf/client/generated/push_notification.pb.dart similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.pb.dart rename to lib/src/model/protobuf/client/generated/push_notification.pb.dart diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.pbenum.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbenum.dart diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.pbjson.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbjson.dart diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbserver.dart b/lib/src/model/protobuf/client/generated/push_notification.pbserver.dart similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.pbserver.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbserver.dart diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto new file mode 100644 index 0000000..39b3105 --- /dev/null +++ b/lib/src/model/protobuf/client/messages.proto @@ -0,0 +1,135 @@ +syntax = "proto3"; + +message Message { + enum Type { + SENDER_DELIVERY_RECEIPT = 0; + PLAINTEXT_CONTENT = 1; + CIPHERTEXT = 2; + PREKEY_BUNDLE = 3; + } + Type type = 1; + string receiptId = 2; + optional bytes encryptedContent = 3; + optional PlaintextContent plaintextContent = 4; +} + +message PlaintextContent { + optional DecryptionErrorMessage decryptionErrorMessage = 1; + + message DecryptionErrorMessage { + enum Type { + UNKNOWN = 0; + PREKEY_UNKNOWN = 1; + } + Type type = 1; + } +} + + +message EncryptedContent { + + optional string groupId = 2; + + /// This can be added, so the receiver can check weather he is up to date with the current profile + optional int64 senderProfileCounter = 3; + + optional TextMessage textMessage = 4; + optional MessageUpdate messageUpdate = 5; + optional Media media = 6; + optional MediaUpdate mediaUpdate = 7; + optional ContactUpdate contactUpdate = 8; + optional ContactRequest contactRequest = 9; + optional FlameSync flameSync = 10; + optional PushKeys pushKeys = 11; + optional Reaction reaction = 12; + + message TextMessage { + string senderMessageId = 1; + string text = 2; + optional string quoteMessageId = 3; + } + + message Reaction { + string targetMessageId = 1; + optional string emoji = 2; + optional bool remove = 3; + } + + message MessageUpdate { + enum Type { + DELETE = 0; + EDIT_TEXT = 1; + OPENED = 2; + } + Type type = 1; + string senderMessageId = 2; + optional string text = 3; + optional int64 timestamp = 4; + } + + message Media { + enum Type { + IMAGE = 0; + VIDEO = 1; + GIF = 2; + } + + string senderMessageId = 1; + Type type = 2; + optional int64 displayLimitInMilliseconds = 3; + bool requiresAuthentication = 4; + + optional bytes downloadToken = 5; + optional bytes encryptionKey = 6; + optional bytes encryptionMac = 7; + optional bytes encryptionNonce = 8; + } + + message MediaUpdate { + enum Type { + REOPENED = 0; + STORED = 1; + DECRYPTION_ERROR = 2; + } + Type type = 1; + string targetMessageId = 2; + } + + message ContactRequest { + enum Type { + REQUEST = 0; + REJECT = 1; + ACCEPT = 2; + } + Type type = 1; + } + + message ContactUpdate { + enum Type { + REQUEST = 0; + UPDATE = 1; + } + + Type type = 1; + optional string avatarSvg = 2; + optional string displayName = 3; + } + + message PushKeys { + enum Type { + REQUEST = 0; + UPDATE = 1; + } + + Type type = 1; + optional int64 keyId = 2; + optional bytes key = 3; + } + + message FlameSync { + int64 flameCounter = 1; + int64 lastFlameCounterChange = 2; + bool bestFriend = 3; + } + +} \ No newline at end of file diff --git a/lib/src/model/protobuf/push_notification/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.proto rename to lib/src/model/protobuf/client/push_notification.proto diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 461d180..8a54cff 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -17,7 +17,7 @@ import 'package:mutex/mutex.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart index 4148d9d..0c2708d 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/media_download.dart @@ -13,8 +13,8 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/log.dart'; diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/media_upload.dart index 96c35a4..364eb1f 100644 --- a/lib/src/services/api/media_upload.dart +++ b/lib/src/services/api/media_upload.dart @@ -19,8 +19,8 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/tables/media_uploads_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index c939546..08cbb6c 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -8,9 +8,11 @@ import 'package:fixnum/fixnum.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/server_messages.dart' show messageGetsAck; @@ -36,12 +38,15 @@ Future tryTransmitMessages() async { }); } -Future sendRetransmitMessage(int retransId) async { +Future tryToSendCompleteMessage(String receiptId) async { try { final retrans = await twonlyDB.messageRetransmissionDao .getRetransmissionById(retransId) .getSingleOrNull(); + /// SET THE Message().receiptID !!!!!!! + /// ALSO THE encryptedContent is NOT YET ENCRYPTED! + if (retrans == null) { Log.error('$retransId not found in database'); return; @@ -156,97 +161,74 @@ Future sendRetransmitMessage(int retransId) async { } } -// encrypts and stores the message and then sends it in the background -Future encryptAndSendMessageAsync( - int? messageId, - int userId, - MessageJson msg, { - PushNotification? pushNotification, -}) async { - Uint8List? pushData; - if (pushNotification != null) { - pushData = await getPushData(userId, pushNotification); - } - - final retransId = - await twonlyDB.messageRetransmissionDao.insertRetransmission( - MessageRetransmissionsCompanion( - contactId: Value(userId), - messageId: Value(messageId), - plaintextContent: Value(Uint8List(0)), - pushData: Value(pushData), - ), - ); - - if (retransId == null) { - Log.error('Could not insert the message into the retransmission database'); - return; - } - - msg.retransId = retransId; - - final plaintextContent = - Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); - - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - plaintextContent: Value(plaintextContent), - ), - ); - - // this can now be done in the background... - unawaited(sendRetransmitMessage(retransId)); -} - -Future sendTextMessage( - int target, - TextMessageContent content, - PushNotification? pushNotification, +Future sendCipherText( + int contactId, + pb.EncryptedContent encryptedContent, ) async { - final messageSendAt = DateTime.now(); - DateTime? openedAt; + final response = pb.Message() + ..type = pb.Message_Type.CIPHERTEXT + ..encryptedContent = encryptedContent.writeToBuffer(); - if (pushNotification != null && pushNotification.hasReactionContent()) { - openedAt = DateTime.now(); - } - - final messageId = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(target), - kind: const Value(MessageKind.textMessage), - sendAt: Value(messageSendAt), - responseToOtherMessageId: Value(content.responseToMessageId), - responseToMessageId: Value(content.responseToOtherMessageId), - downloadState: const Value(DownloadState.downloaded), - openedAt: Value(openedAt), - contentJson: Value( - jsonEncode(content.toJson()), - ), + final receipt = await twonlyDB.receiptsDao.insertReceipt( + ReceiptsCompanion( + contactId: Value(contactId), + message: Value(response.writeToBuffer()), ), ); - if (messageId == null) return; - - if (pushNotification != null && !pushNotification.hasReactionContent()) { - pushNotification.messageId = Int64(messageId); + if (receipt != null) { + await tryToSendCompleteMessage(receipt.receiptId); } - - final msg = MessageJson( - kind: MessageKind.textMessage, - messageSenderId: messageId, - content: content, - timestamp: messageSendAt, - ); - - await encryptAndSendMessageAsync( - messageId, - target, - msg, - pushNotification: pushNotification, - ); } +// Future sendTextMessage( +// int target, +// TextMessageContent content, +// PushNotification? pushNotification, +// ) async { +// final messageSendAt = DateTime.now(); +// DateTime? openedAt; + +// if (pushNotification != null && pushNotification.hasReactionContent()) { +// openedAt = DateTime.now(); +// } + +// final messageId = await twonlyDB.messagesDao.insertMessage( +// MessagesCompanion( +// contactId: Value(target), +// kind: const Value(MessageKind.textMessage), +// sendAt: Value(messageSendAt), +// responseToOtherMessageId: Value(content.responseToMessageId), +// responseToMessageId: Value(content.responseToOtherMessageId), +// downloadState: const Value(DownloadState.downloaded), +// openedAt: Value(openedAt), +// contentJson: Value( +// jsonEncode(content.toJson()), +// ), +// ), +// ); + +// if (messageId == null) return; + +// if (pushNotification != null && !pushNotification.hasReactionContent()) { +// pushNotification.messageId = Int64(messageId); +// } + +// final msg = MessageJson( +// kind: MessageKind.textMessage, +// messageSenderId: messageId, +// content: content, +// timestamp: messageSendAt, +// ); + +// await encryptAndSendMessageAsync( +// messageId, +// target, +// msg, +// pushNotification: pushNotification, +// ); +// } + Future notifyContactAboutOpeningMessage( int fromUserId, List messageOtherIds, @@ -269,33 +251,25 @@ Future notifyContactAboutOpeningMessage( await updateLastMessageId(fromUserId, biggestMessageId); } -Future notifyContactsAboutProfileChange() async { - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); - +Future notifyContactsAboutProfileChange({int? onlyToContact}) async { final user = await getUser(); if (user == null) return; if (user.avatarSvg == null) return; + final encryptedContent = pb.EncryptedContent() + ..contactUpdate = (pb.EncryptedContent_ContactUpdate() + ..type = pb.EncryptedContent_ContactUpdate_Type.UPDATE + ..avatarSvg = user.avatarSvg! + ..displayName = user.displayName); + + if (onlyToContact != null) { + await sendCipherText(onlyToContact, encryptedContent); + return; + } + + final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); + for (final contact in contacts) { - if (contact.myAvatarCounter < user.avatarCounter) { - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( - myAvatarCounter: Value(user.avatarCounter), - ), - ); - await encryptAndSendMessageAsync( - null, - contact.userId, - MessageJson( - kind: MessageKind.profileChange, - content: ProfileContent( - avatarSvg: user.avatarSvg!, - displayName: user.displayName, - ), - timestamp: DateTime.now(), - ), - ); - } + await sendCipherText(contact.userId, encryptedContent); } } diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index ba89de0..953924c 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,42 +1,31 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/services/api/server_messages/contact.server_messages.dart'; +import 'package:twonly/src/services/api/server_messages/media.server_messages.dart'; +import 'package:twonly/src/services/api/server_messages/messages.server_messages.dart'; +import 'package:twonly/src/services/api/server_messages/prekeys.server_messages.dart'; +import 'package:twonly/src/services/api/server_messages/pushkeys.server_messages.dart'; +import 'package:twonly/src/services/api/server_messages/reaction.server_message.dart'; +import 'package:twonly/src/services/api/server_messages/text_message.server_messages.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/services/signal/identity.signal.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/animate_icon.dart'; final lockHandleServerMessage = Mutex(); Future handleServerMessage(server.ServerToClient msg) async { - // return lockHandleServerMessage.protect(() async { - client.Response? response; + /// 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()) { @@ -44,13 +33,12 @@ Future handleServerMessage(server.ServerToClient msg) async { } else if (msg.v0.hasNewMessage()) { final body = Uint8List.fromList(msg.v0.newMessage.body); final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - response = await handleNewMessage(fromUserId, body); + await handleNewMessage(fromUserId, body); } else { - Log.error('Got a unknown message from the server: $msg'); - response = client.Response()..error = ErrorCode.InternalError; + Log.error('Unknown server message: $msg'); } } catch (e) { - response = client.Response()..error = ErrorCode.InternalError; + Log.error(e); } final v0 = client.V0() @@ -58,435 +46,159 @@ Future handleServerMessage(server.ServerToClient msg) async { ..response = response; await apiService.sendResponse(ClientToServer()..v0 = v0); - // }); } -DateTime lastSignalDecryptMessage = - DateTime.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); -bool messageGetsAck(MessageKind kind) { - return kind != MessageKind.pushKey && kind != MessageKind.ack; -} +Future handleNewMessage(int fromUserId, Uint8List body) async { + final message = Message.fromBuffer(body); + final receiptId = message.receiptId; -Future handleNewMessage(int fromUserId, Uint8List body) async { - final message = await signalDecryptMessage(fromUserId, body); - if (message == null) { - final encryptedHash = (await Sha256().hash(body)).bytes; - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.signalDecryptError, - content: SignalDecryptErrorContent(encryptedHash: encryptedHash), - timestamp: DateTime.now(), - ), - ); + switch (message.type) { + case Message_Type.SENDER_DELIVERY_RECEIPT: + Log.info('Got delivery receipt for $receiptId!'); + await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); - Log.error('Could not decrypt others message!'); - - // Message is not valid, so server can delete it - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - - client.Response? result; - - Log.info('Got: ${message.kind} from $fromUserId'); - - switch (message.kind) { - case MessageKind.ack: - final content = message.content; - if (content is AckContent) { - if (content.messageIdToAck != null) { - const update = MessagesCompanion( - acknowledgeByUser: Value(true), - errorWhileSending: Value(false), - ); - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - content.messageIdToAck!, - update, - ); - } - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(content.retransIdToAck); - } - case MessageKind.signalDecryptError: - Log.error( - 'Got signal decrypt error from other user! Sending it again.', - ); - - final content = message.content; - if (content is SignalDecryptErrorContent) { - final hash = Uint8List.fromList(content.encryptedHash); - await twonlyDB.messageRetransmissionDao.resetAckStatusFor( - fromUserId, - hash, + case Message_Type.PLAINTEXT_CONTENT: + if (message.hasPlaintextContent() && + message.plaintextContent.hasDecryptionErrorMessage()) { + Log.info( + 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', ); - final message = await twonlyDB.messageRetransmissionDao - .getRetransmissionFromHash(fromUserId, hash); - if (message != null) { - unawaited(sendRetransmitMessage(message.retransmissionId)); + await tryToSendCompleteMessage(receiptId); + } + + case Message_Type.CIPHERTEXT: + case Message_Type.PREKEY_BUNDLE: + if (message.hasEncryptedContent()) { + final encryptedContentRaw = + Uint8List.fromList(message.encryptedContent); + + final responsePlaintextContent = await handleEncryptedMessage( + fromUserId, + encryptedContentRaw, + message.type, + ); + Message response; + if (responsePlaintextContent != null) { + response = Message() + ..receiptId = receiptId + ..type = Message_Type.PLAINTEXT_CONTENT + ..plaintextContent = responsePlaintextContent; + Log.error('Sending decryption error ($receiptId)'); } else { - Log.error('Could not find message to retransmit!'); + response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT; } - } - - case MessageKind.contactRequest: - await handleContactRequest(fromUserId, message); - - case MessageKind.flameSync: - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact != null && contact.lastFlameCounterChange != null) { - final content = message.content; - if (content is FlameSyncContent) { - var updates = ContactsCompanion( - alsoBestFriend: Value(content.bestFriend), - ); - if (isToday(contact.lastFlameCounterChange!) && - isToday(content.lastFlameCounterChange)) { - if (content.flameCounter > contact.flameCounter) { - updates = ContactsCompanion( - flameCounter: Value(content.flameCounter), - ); - } - } - await twonlyDB.contactsDao.updateContact(fromUserId, updates); - } - } - - case MessageKind.receiveMediaError: - if (message.messageReceiverId != null) { - final openedMessage = await twonlyDB.messagesDao - .getMessageByIdAndContactId(fromUserId, message.messageReceiverId!) - .getSingleOrNull(); - - if (openedMessage != null) { - /// message found - - /// checks if - /// 1. this was a media upload - /// 2. the media was not already retransmitted - /// 3. the media was send in the last two days - if (openedMessage.mediaUploadId != null && - openedMessage.mediaRetransmissionState == - MediaRetransmitting.none && - openedMessage.sendAt - .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { - // reset the media upload state to pending, - // this will cause the media to be re-encrypted again - await twonlyDB.mediaUploadsDao.updateMediaUpload( - openedMessage.mediaUploadId!, - const MediaUploadsCompanion( - state: Value( - UploadState.pending, - ), - ), - ); - // reset the message upload so the upload will be done again - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - downloadState: Value(DownloadState.pending), - mediaRetransmissionState: - Value(MediaRetransmitting.retransmitted), - ), - ); - unawaited(retryMediaUpload(false)); - } else { - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } - } - } - - case MessageKind.opened: - if (message.messageReceiverId != null) { - final update = MessagesCompanion( - openedAt: Value(message.timestamp), - errorWhileSending: const Value(false), + await twonlyDB.receiptsDao.insertReceipt( + ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(fromUserId), + message: Value(response.writeToBuffer()), + contactWillSendsReceipt: const Value(false), + ), ); - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - update, - ); - final openedMessage = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageReceiverId!) - .getSingleOrNull(); - if (openedMessage != null && - openedMessage.kind == MessageKind.textMessage) { - await twonlyDB.messagesDao.openedAllNonMediaMessagesFromOtherUser( - fromUserId, - ); - } - } - - case MessageKind.rejectRequest: - await deleteContact(fromUserId); - - case MessageKind.acceptRequest: - const update = ContactsCompanion(accepted: Value(true)); - await twonlyDB.contactsDao.updateContact(fromUserId, update); - unawaited(notifyContactsAboutProfileChange()); - - case MessageKind.profileChange: - final content = message.content; - if (content is ProfileContent) { - final update = ContactsCompanion( - avatarSvg: Value(content.avatarSvg), - displayName: Value(content.displayName), - ); - await twonlyDB.contactsDao.updateContact(fromUserId, update); - } - unawaited(createPushAvatars()); - - case MessageKind.requestPushKey: - if (lastPushKeyRequest - .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { - lastPushKeyRequest = DateTime.now(); - unawaited(setupNotificationWithUsers(forceContact: fromUserId)); - } - - case MessageKind.pushKey: - if (message.content != null) { - final pushKey = message.content!; - if (pushKey is PushKeyContent) { - await handleNewPushKey(fromUserId, pushKey); - } - } - - // ignore: no_default_cases - default: - if (message.messageSenderId == null) { - Log.error('Messageid not defined $message'); - } else if ([ - MessageKind.textMessage, - MessageKind.media, - MessageKind.storedMediaFile, - MessageKind.reopenedMedia, - ].contains(message.kind)) { - result = await handleMediaOrTextMessage(fromUserId, message); - } else { - Log.error('Got unknown MessageKind $message'); + await tryToSendCompleteMessage(receiptId); } } - - if (messageGetsAck(message.kind) && message.retransId != null) { - Log.info('Sending ACK for ${message.kind}'); - - /// ACK every message - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.ack, - content: AckContent( - messageIdToAck: message.messageSenderId, - retransIdToAck: message.retransId!, - ), - timestamp: DateTime.now(), - ), - ); - } - - if (result != null) { - return result; - } - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; } -Future handleMediaOrTextMessage( +Future handleEncryptedMessage( int fromUserId, - MessageJson message, + Uint8List encryptedContentRaw, + Message_Type messageType, ) async { - if (message.kind == MessageKind.storedMediaFile) { - if (message.messageReceiverId != null) { - /// stored media file just updates the message - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - mediaStored: Value(true), - errorWhileSending: Value(false), - ), - ); - final msg = await twonlyDB.messagesDao - .getMessageByIdAndContactId( - fromUserId, - message.messageReceiverId!, - ) - .getSingleOrNull(); - if (msg != null && msg.mediaUploadId != null) { - final filePath = await getMediaFilePath(msg.mediaUploadId, 'send'); - if (filePath.contains('mp4')) { - unawaited(createThumbnailsForVideo(File(filePath))); - } else { - unawaited(createThumbnailsForImage(File(filePath))); - } - } - } - } else if (message.content != null) { - final content = message.content!; - // when a message is received doubled ignore it... + final (content, decryptionErrorType) = await signalDecryptMessage( + fromUserId, + encryptedContentRaw, + messageType as int, + ); - final openedMessage = await twonlyDB.messagesDao - .getMessageByOtherMessageId(fromUserId, message.messageSenderId!) - .getSingleOrNull(); - - if (openedMessage != null) { - if (openedMessage.errorWhileSending) { - await twonlyDB.messagesDao - .deleteMessagesByMessageId(openedMessage.messageId); - } else { - Log.error( - 'Got a duplicated message from other user: ${message.messageSenderId!}', - ); - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - } - - int? responseToMessageId; - int? responseToOtherMessageId; - int? messageId; - - var acknowledgeByUser = false; - DateTime? openedAt; - - if (message.kind == MessageKind.reopenedMedia) { - acknowledgeByUser = true; - openedAt = DateTime.now(); - } - - if (content is TextMessageContent) { - responseToMessageId = content.responseToMessageId; - responseToOtherMessageId = content.responseToOtherMessageId; - - if (responseToMessageId != null || responseToOtherMessageId != null) { - // reactions are shown in the notification directly... - if (isEmoji(content.text)) { - openedAt = DateTime.now(); - } - } - } - if (content is ReopenedMediaFileContent) { - responseToMessageId = content.messageId; - } - - if (responseToMessageId != null) { - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - responseToMessageId, - MessagesCompanion( - errorWhileSending: const Value(false), - openedAt: Value( - DateTime.now(), - ), // when a user reacted to the media file, it should be marked as opened - ), - ); - } - - final contentJson = jsonEncode(content.toJson()); - final update = MessagesCompanion( - contactId: Value(fromUserId), - kind: Value(message.kind), - messageOtherId: Value(message.messageSenderId), - contentJson: Value(contentJson), - acknowledgeByServer: const Value(true), - acknowledgeByUser: Value(acknowledgeByUser), - responseToMessageId: Value(responseToMessageId), - responseToOtherMessageId: Value(responseToOtherMessageId), - openedAt: Value(openedAt), - downloadState: Value( - message.kind == MessageKind.media - ? DownloadState.pending - : DownloadState.downloaded, - ), - sendAt: Value(message.timestamp), - ); - - messageId = await twonlyDB.messagesDao.insertMessage( - update, - ); - - if (messageId == null) { - Log.error('could not insert message into db'); - return client.Response()..error = ErrorCode.InternalError; - } - - Log.info('Inserted a new message with id: $messageId'); - - if (message.kind == MessageKind.media) { - await twonlyDB.contactsDao.incFlameCounter( - fromUserId, - true, - message.timestamp, - ); - - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (msg != null) { - unawaited(startDownloadMedia(msg, false)); - } - } - } else { - Log.error('Content is not defined $message'); + if (content == null) { + return PlaintextContent() + ..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() + ..type = decryptionErrorType!); + } + + final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); + + if (content.hasContactRequest()) { + await handleContactRequest(fromUserId, content.contactRequest); + return null; + } + + if (content.hasContactUpdate()) { + await handleContactUpdate( + fromUserId, + content.contactUpdate, + senderProfileCounter, + ); + return null; + } + + if (content.hasFlameSync()) { + await handleFlameSync(fromUserId, content.flameSync); + return null; + } + + if (content.hasPushKeys()) { + await handlePushKey(fromUserId, content.pushKeys); + return null; + } + + if (!content.hasGroupId()) { + return null; + } + + /// Verify that the user is (still) in that group... + if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { + Log.error('User $fromUserId tried to access group ${content.groupId}.'); + return null; + } + + if (content.hasMessageUpdate()) { + await handleMessageUpdate( + fromUserId, + content.groupId, + content.messageUpdate, + ); + return null; + } + + if (content.hasTextMessage()) { + await handleTextMessage( + fromUserId, + content.groupId, + content.textMessage, + ); + return null; + } + + if (content.hasReaction()) { + await handleReaction( + fromUserId, + content.groupId, + content.reaction, + ); + return null; + } + + if (content.hasMedia()) { + await handleMedia( + fromUserId, + content.groupId, + content.media, + ); + return null; + } + + if (content.hasMediaUpdate()) { + await handleMediaUpdate( + fromUserId, + content.groupId, + content.mediaUpdate, + ); + return null; } - // unarchive contact when receiving a new message - await twonlyDB.contactsDao.updateContact( - fromUserId, - const ContactsCompanion( - archived: Value(false), - ), - ); return null; } - -Future handleRequestNewPreKey() async { - final localPreKeys = await signalGetPreKeys(); - - final prekeysList = []; - for (var i = 0; i < localPreKeys.length; i++) { - prekeysList.add( - client.Response_PreKey() - ..id = Int64(localPreKeys[i].id) - ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(), - ); - } - final prekeys = client.Response_Prekeys(prekeys: prekeysList); - final ok = client.Response_Ok()..prekeys = prekeys; - return client.Response()..ok = ok; -} - -Future handleContactRequest( - int fromUserId, - MessageJson message, -) async { - // request the username by the server so an attacker can not - // forge the displayed username in the contact request - final username = await apiService.getUsername(fromUserId); - if (username.isSuccess) { - final name = username.value.userdata.username as Uint8List; - await twonlyDB.contactsDao.insertContact( - ContactsCompanion( - username: Value(utf8.decode(name)), - userId: Value(fromUserId), - requested: const Value(true), - ), - ); - } - await setupNotificationWithUsers(); -} diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart new file mode 100644 index 0000000..aff4c79 --- /dev/null +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -0,0 +1,121 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart' hide Message; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future handleContactRequest( + int fromUserId, + EncryptedContent_ContactRequest contactRequest, +) async { + switch (contactRequest.type) { + case EncryptedContent_ContactRequest_Type.REQUEST: + // Request the username by the server so an attacker can not + // forge the displayed username in the contact request + final username = await apiService.getUsername(fromUserId); + if (username.isSuccess) { + // ignore: avoid_dynamic_calls + final name = username.value.userdata.username as Uint8List; + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + username: Value(utf8.decode(name)), + userId: Value(fromUserId), + requested: const Value(true), + ), + ); + } + await setupNotificationWithUsers(); + case EncryptedContent_ContactRequest_Type.ACCEPT: + await twonlyDB.contactsDao.updateContact( + fromUserId, + const ContactsCompanion( + requested: Value(false), + accepted: Value(true), + ), + ); + case EncryptedContent_ContactRequest_Type.REJECT: + await twonlyDB.contactsDao.deleteContactByUserId(fromUserId); + } +} + +Future handleContactUpdate( + int fromUserId, + EncryptedContent_ContactUpdate contactUpdate, + int? senderProfileCounter) async { + switch (contactUpdate.type) { + case EncryptedContent_ContactUpdate_Type.REQUEST: + await notifyContactsAboutProfileChange(onlyToContact: fromUserId); + + case EncryptedContent_ContactUpdate_Type.UPDATE: + if (contactUpdate.hasAvatarSvg() && + contactUpdate.hasDisplayName() && + senderProfileCounter != null) { + await twonlyDB.contactsDao.updateContact( + fromUserId, + ContactsCompanion( + avatarSvg: Value(contactUpdate.avatarSvg), + displayName: Value(contactUpdate.displayName), + senderProfileCounter: Value(senderProfileCounter), + ), + ); + unawaited(createPushAvatars()); + } + } +} + +Future handleFlameSync( + int contactId, + EncryptedContent_FlameSync flameSync, +) async { + final contact = await twonlyDB.contactsDao + .getContactByUserId(contactId) + .getSingleOrNull(); + + if (contact == null || contact.lastFlameCounterChange != null) return; + + var updates = ContactsCompanion( + alsoBestFriend: Value(flameSync.bestFriend), + ); + if (isToday(contact.lastFlameCounterChange!) && + isToday(fromTimestamp(flameSync.lastFlameCounterChange))) { + if (flameSync.flameCounter > contact.flameCounter) { + updates = ContactsCompanion( + flameCounter: Value(flameSync.flameCounter.toInt()), + ); + } + } + await twonlyDB.contactsDao.updateContact(contactId, updates); +} + +Future checkForProfileUpdate( + int fromUserId, + EncryptedContent content, +) async { + int? senderProfileCounter; + + if (content.hasSenderProfileCounter() && !content.hasContactUpdate()) { + senderProfileCounter = content.senderProfileCounter.toInt(); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.senderProfileCounter < senderProfileCounter) { + await sendCipherText( + fromUserId, + EncryptedContent() + ..contactUpdate = (EncryptedContent_ContactUpdate() + ..type = EncryptedContent_ContactUpdate_Type.REQUEST), + ); + } + } + } + + return senderProfileCounter; +} diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart new file mode 100644 index 0000000..7b2583b --- /dev/null +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -0,0 +1,92 @@ + +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; + +Future handleMedia(int fromUserId, String groupId, EncryptedContent_Media media) async { +TODO +} + +Future handleMediaUpdate(int fromUserId, String groupId, EncryptedContent_MediaUpdate mediaUpdate) async { +TODO + + + // switch (message.kind) { + // case MessageKind.receiveMediaError: + // if (message.messageReceiverId != null) { + // final openedMessage = await twonlyDB.messagesDao + // .getMessageByIdAndContactId(fromUserId, message.messageReceiverId!) + // .getSingleOrNull(); + + // if (openedMessage != null) { + // /// message found + + // /// checks if + // /// 1. this was a media upload + // /// 2. the media was not already retransmitted + // /// 3. the media was send in the last two days + // if (openedMessage.mediaUploadId != null && + // openedMessage.mediaRetransmissionState == + // MediaRetransmitting.none && + // openedMessage.sendAt + // .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + // // reset the media upload state to pending, + // // this will cause the media to be re-encrypted again + // await twonlyDB.mediaUploadsDao.updateMediaUpload( + // openedMessage.mediaUploadId!, + // const MediaUploadsCompanion( + // state: Value( + // UploadState.pending, + // ), + // ), + // ); + // // reset the message upload so the upload will be done again + // await twonlyDB.messagesDao.updateMessageByOtherUser( + // fromUserId, + // message.messageReceiverId!, + // const MessagesCompanion( + // downloadState: Value(DownloadState.pending), + // mediaRetransmissionState: + // Value(MediaRetransmitting.retransmitted), + // ), + // ); + // unawaited(retryMediaUpload(false)); + // } else { + // await twonlyDB.messagesDao.updateMessageByOtherUser( + // fromUserId, + // message.messageReceiverId!, + // const MessagesCompanion( + // errorWhileSending: Value(true), + // ), + // ); + // } + // } + // } + + + if (message.kind == MessageKind.storedMediaFile) { + if (message.messageReceiverId != null) { + /// stored media file just updates the message + await twonlyDB.messagesDao.updateMessageByOtherUser( + fromUserId, + message.messageReceiverId!, + const MessagesCompanion( + mediaStored: Value(true), + errorWhileSending: Value(false), + ), + ); + final msg = await twonlyDB.messagesDao + .getMessageByIdAndContactId( + fromUserId, + message.messageReceiverId!, + ) + .getSingleOrNull(); + if (msg != null && msg.mediaUploadId != null) { + final filePath = await getMediaFilePath(msg.mediaUploadId, 'send'); + if (filePath.contains('mp4')) { + unawaited(createThumbnailsForVideo(File(filePath))); + } else { + unawaited(createThumbnailsForImage(File(filePath))); + } + } + } + } else if (message.content != null) {} +} diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/server_messages/messages.server_messages.dart new file mode 100644 index 0000000..6a65873 --- /dev/null +++ b/lib/src/services/api/server_messages/messages.server_messages.dart @@ -0,0 +1,35 @@ +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleMessageUpdate( + int contactId, + String groupId, + EncryptedContent_MessageUpdate messageUpdate, +) async { + switch (messageUpdate.type) { + case EncryptedContent_MessageUpdate_Type.OPENED: + Log.info('Opened message ${messageUpdate.senderMessageId}'); + await twonlyDB.messagesDao.handleMessageOpened( + groupId, + messageUpdate.senderMessageId, + fromTimestamp(messageUpdate.timestamp), + ); + case EncryptedContent_MessageUpdate_Type.DELETE: + Log.info('Delete message ${messageUpdate.senderMessageId}'); + await twonlyDB.messagesDao.handleMessageDeletion( + contactId, + messageUpdate.senderMessageId, + fromTimestamp(messageUpdate.timestamp), + ); + case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: + Log.info('Edit message ${messageUpdate.senderMessageId}'); + await twonlyDB.messagesDao.handleTextEdit( + contactId, + messageUpdate.senderMessageId, + messageUpdate.text, + fromTimestamp(messageUpdate.timestamp), + ); + } +} diff --git a/lib/src/services/api/server_messages/prekeys.server_messages.dart b/lib/src/services/api/server_messages/prekeys.server_messages.dart new file mode 100644 index 0000000..4f2ed1c --- /dev/null +++ b/lib/src/services/api/server_messages/prekeys.server_messages.dart @@ -0,0 +1,20 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' + as client; +import 'package:twonly/src/services/signal/identity.signal.dart'; + +Future handleRequestNewPreKey() async { + final localPreKeys = await signalGetPreKeys(); + + final prekeysList = []; + for (var i = 0; i < localPreKeys.length; i++) { + prekeysList.add( + client.Response_PreKey() + ..id = Int64(localPreKeys[i].id) + ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(), + ); + } + final prekeys = client.Response_Prekeys(prekeys: prekeysList); + final ok = client.Response_Ok()..prekeys = prekeys; + return client.Response()..ok = ok; +} diff --git a/lib/src/services/api/server_messages/pushkeys.server_messages.dart b/lib/src/services/api/server_messages/pushkeys.server_messages.dart new file mode 100644 index 0000000..4e755ff --- /dev/null +++ b/lib/src/services/api/server_messages/pushkeys.server_messages.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; + +DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); + +Future handlePushKey( + int contactId, + EncryptedContent_PushKeys pushKeys, +) async { + switch (pushKeys.type) { + case EncryptedContent_PushKeys_Type.REQUEST: + if (lastPushKeyRequest + .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { + lastPushKeyRequest = DateTime.now(); + unawaited(setupNotificationWithUsers(forceContact: contactId)); + } + + case EncryptedContent_PushKeys_Type.UPDATE: + await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); + } +} diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/server_messages/reaction.server_message.dart new file mode 100644 index 0000000..733457c --- /dev/null +++ b/lib/src/services/api/server_messages/reaction.server_message.dart @@ -0,0 +1,25 @@ +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; + +Future handleReaction( + int fromUserId, + String groupId, + EncryptedContent_Reaction reaction, +) async { + if (reaction.hasRemove()) { + if (reaction.remove) { + await twonlyDB.reactionsDao + .updateReaction(fromUserId, reaction.targetMessageId, groupId, null); + } + return; + } + if (reaction.hasEmoji()) { + await twonlyDB.reactionsDao.updateReaction( + fromUserId, + reaction.targetMessageId, + groupId, + reaction.emoji, + ); + return; + } +} diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart new file mode 100644 index 0000000..df9140c --- /dev/null +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -0,0 +1,125 @@ +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; + +Future handleTextMessage( + int fromUserId, + String groupId, + EncryptedContent_TextMessage textMessage, +) async { + TODO + // final content = message.content!; + // // when a message is received doubled ignore it... + + // final openedMessage = await twonlyDB.messagesDao + // .getMessageByOtherMessageId(fromUserId, message.messageSenderId!) + // .getSingleOrNull(); + + // if (openedMessage != null) { + // if (openedMessage.errorWhileSending) { + // await twonlyDB.messagesDao + // .deleteMessagesByMessageId(openedMessage.messageId); + // } else { + // Log.error( + // 'Got a duplicated message from other user: ${message.messageSenderId!}', + // ); + // final ok = client.Response_Ok()..none = true; + // return client.Response()..ok = ok; + // } + // } + + // int? responseToMessageId; + // int? responseToOtherMessageId; + // int? messageId; + + // var acknowledgeByUser = false; + // DateTime? openedAt; + + // if (message.kind == MessageKind.reopenedMedia) { + // acknowledgeByUser = true; + // openedAt = DateTime.now(); + // } + + // if (content is TextMessageContent) { + // responseToMessageId = content.responseToMessageId; + // responseToOtherMessageId = content.responseToOtherMessageId; + + // if (responseToMessageId != null || responseToOtherMessageId != null) { + // // reactions are shown in the notification directly... + // if (isEmoji(content.text)) { + // openedAt = DateTime.now(); + // } + // } + // } + // if (content is ReopenedMediaFileContent) { + // responseToMessageId = content.messageId; + // } + + // if (responseToMessageId != null) { + // await twonlyDB.messagesDao.updateMessageByOtherUser( + // fromUserId, + // responseToMessageId, + // MessagesCompanion( + // errorWhileSending: const Value(false), + // openedAt: Value( + // DateTime.now(), + // ), // when a user reacted to the media file, it should be marked as opened + // ), + // ); + // } + + // final contentJson = jsonEncode(content.toJson()); + // final update = MessagesCompanion( + // contactId: Value(fromUserId), + // kind: Value(message.kind), + // messageOtherId: Value(message.messageSenderId), + // contentJson: Value(contentJson), + // acknowledgeByServer: const Value(true), + // acknowledgeByUser: Value(acknowledgeByUser), + // responseToMessageId: Value(responseToMessageId), + // responseToOtherMessageId: Value(responseToOtherMessageId), + // openedAt: Value(openedAt), + // downloadState: Value( + // message.kind == MessageKind.media + // ? DownloadState.pending + // : DownloadState.downloaded, + // ), + // sendAt: Value(message.timestamp), + // ); + + // messageId = await twonlyDB.messagesDao.insertMessage( + // update, + // ); + + // if (messageId == null) { + // Log.error('could not insert message into db'); + // return client.Response()..error = ErrorCode.InternalError; + // } + + // Log.info('Inserted a new message with id: $messageId'); + + // if (message.kind == MessageKind.media) { + // await twonlyDB.contactsDao.incFlameCounter( + // fromUserId, + // true, + // message.timestamp, + // ); + + // final msg = await twonlyDB.messagesDao + // .getMessageByMessageId(messageId) + // .getSingleOrNull(); + // if (msg != null) { + // unawaited(startDownloadMedia(msg, false)); + // } + // } + // } else { + // Log.error('Content is not defined $message'); + // } + + // // unarchive contact when receiving a new message + // await twonlyDB.contactsDao.updateContact( + // fromUserId, + // const ContactsCompanion( + // archived: Value(false), + // ), + // ); + // return null; +} diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 3c42df9..c5b119a 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; @@ -24,6 +24,10 @@ class Result { bool get isError => error != null; } +DateTime fromTimestamp(Int64 timeStamp) { + return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt() * 1000); +} + // ignore: strict_raw_type Result asResult(server.ServerToClient? msg) { if (msg == null) { diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 01edd40..929dc0b 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:twonly/globals.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart' as my; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart' as my; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/services/mediafile.service.dart b/lib/src/services/mediafile.service.dart new file mode 100644 index 0000000..eea8842 --- /dev/null +++ b/lib/src/services/mediafile.service.dart @@ -0,0 +1,5 @@ +import 'package:twonly/src/utils/log.dart'; + +Future removeMediaFile(String mediaId) async { + Log.error('TODO removeMediaFile: $mediaId'); +} diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 5488cc1..562da78 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -9,10 +9,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart' as my; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart' as my; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/log.dart'; @@ -144,7 +144,7 @@ Future updatePushUser(Contact contact) async { await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys); } -Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { +Future handleNewPushKey(int fromUserId, int keyId, List key) async { final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 8087fac..1338663 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -1,10 +1,8 @@ -import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; -import 'package:twonly/src/model/json/message.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/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; @@ -92,42 +90,40 @@ Future signalEncryptMessage( }); } -Future signalDecryptMessage(int source, Uint8List msg) async { +Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> + signalDecryptMessage( + int source, + Uint8List encryptedContentRaw, + int type, +) async { try { - final signalStore = (await getSignalStore())!; - final session = SessionCipher.fromStore( - signalStore, + (await getSignalStore())!, SignalProtocolAddress(source.toString(), defaultDeviceId), ); - final msgs = removeLastXBytes(msg, 4); - if (msgs == null) { - Log.error('Message requires at least 4 bytes.'); - return null; - } - final body = msgs[0]; - final type = bytesToInt(msgs[1]); Uint8List plaintext; - if (type == CiphertextMessage.prekeyType) { - final pre = PreKeySignalMessage(body); - plaintext = await session.decrypt(pre); - } else if (type == CiphertextMessage.whisperType) { - final signalMsg = SignalMessage.fromSerialized(body); - plaintext = await session.decryptFromSignal(signalMsg); - } else { - Log.error('Type not known: $type'); - return null; + + switch (type) { + case CiphertextMessage.prekeyType: + plaintext = await session.decrypt( + PreKeySignalMessage(encryptedContentRaw), + ); + case CiphertextMessage.whisperType: + plaintext = await session.decryptFromSignal( + SignalMessage.fromSerialized(encryptedContentRaw), + ); + default: + Log.error('Unknown Message Decryption Type: $type'); + return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } - return MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(plaintext), - ), - ) as Map, - ); + + return (EncryptedContent.fromBuffer(plaintext), null); + } on InvalidKeyIdException catch (e) { + Log.error(e); + return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN); } catch (e) { - Log.error(e.toString()); - return null; + Log.error(e); + return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } } diff --git a/lib/src/services/signal/prekeys.signal.dart b/lib/src/services/signal/prekeys.signal.dart index 9ce59be..ccee8ec 100644 --- a/lib/src/services/signal/prekeys.signal.dart +++ b/lib/src/services/signal/prekeys.signal.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/utils/log.dart'; diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 5d18482..5a93816 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -11,7 +11,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; import 'package:twonly/src/services/api/media_upload.dart'; diff --git a/lib/src/services/twonly_safe/restore.twonly_safe.dart b/lib/src/services/twonly_safe/restore.twonly_safe.dart index 471bef2..28a14c0 100644 --- a/lib/src/services/twonly_safe/restore.twonly_safe.dart +++ b/lib/src/services/twonly_safe/restore.twonly_safe.dart @@ -11,7 +11,7 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 41b1847..63d2475 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -9,9 +9,9 @@ import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/log.dart'; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 40ded4d..26f8d0b 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -9,8 +9,8 @@ import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -308,7 +308,7 @@ class _CameraPreviewViewState extends State { imageBytes: imageBytes, sharedFromGallery: sharedFromGallery, sendTo: widget.sendTo, - mirrorVideo: isFront && Platform.isAndroid, + mirrorVideo: isFront && Platform.isAndroid && false, useHighQuality: true, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index b29c109..e00d990 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart index 86ba2c9..f63cba5 100644 --- a/lib/src/views/camera/share_image_components/best_friends_selector.dart +++ b/lib/src/views/camera/share_image_components/best_friends_selector.dart @@ -4,8 +4,8 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 6dc0239..acd8eaa 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index f957ba6..537a193 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -7,8 +7,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index d6398b4..c05331e 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index fb2157d..be2444d 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -7,9 +7,9 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/services/api/media_download.dart'; diff --git a/lib/src/views/chats/chat_list_components/last_message_time.dart b/lib/src/views/chats/chat_list_components/last_message_time.dart index b1f229f..273869c 100644 --- a/lib/src/views/chats/chat_list_components/last_message_time.dart +++ b/lib/src/views/chats/chat_list_components/last_message_time.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; class LastMessageTime extends StatefulWidget { diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 654bd7c..8d12d25 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -8,10 +8,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; @@ -134,7 +134,7 @@ class _ChatMessagesViewState extends State { final tmpEmojiReactionsToMessageId = >{}; // only send openedMessage to one text message, as receiver will then set all as read... - int? openedTextMessageOtherIds; + List openedTextMessageOtherIds; final messageOtherMessageIdToMyMessageId = {}; final messageIdToMessage = {}; @@ -154,7 +154,7 @@ class _ChatMessagesViewState extends State { msg.openedAt == null && (openedTextMessageOtherIds == null || openedTextMessageOtherIds < msg.messageOtherId!)) { - openedTextMessageOtherIds = msg.messageOtherId; + openedTextMessageOtherIds.add(msg.messageOtherId); } Message? responseTo; @@ -210,10 +210,10 @@ class _ChatMessagesViewState extends State { } } - if (openedTextMessageOtherIds != null) { + if (openedTextMessageOtherIds.isNotEmpty) { await notifyContactAboutOpeningMessage( widget.contact.userId, - [openedTextMessageOtherIds], + openedTextMessageOtherIds, ); } diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 4ba5c6b..d4770b5 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart'; diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 65208f0..9a247f9 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -4,8 +4,8 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/services/api/media_download.dart' as received; diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index 113400d..d859e8f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index 26774e0..28a4623 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; diff --git a/lib/src/views/chats/chat_messages_components/message_actions.dart b/lib/src/views/chats/chat_messages_components/message_actions.dart index 7ee69ea..e691a92 100644 --- a/lib/src/views/chats/chat_messages_components/message_actions.dart +++ b/lib/src/views/chats/chat_messages_components/message_actions.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class MessageActions extends StatefulWidget { const MessageActions({ diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 26fe88e..4118d69 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -6,8 +6,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index f6c2cda..52293e4 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -4,8 +4,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 14bdf7a..c02d4b2 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index f58449c..e6650e5 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -10,10 +10,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:twonly/globals.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/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/messages.dart'; diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 85c0773..affe878 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 2f3c80a..b37c01c 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; class FlameCounterWidget extends StatelessWidget { diff --git a/lib/src/views/components/initialsavatar.dart b/lib/src/views/components/initialsavatar.dart index 59068db..ec7ae11 100644 --- a/lib/src/views/components/initialsavatar.dart +++ b/lib/src/views/components/initialsavatar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/log.dart'; diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart index 2b30fce..ac834b3 100644 --- a/lib/src/views/components/user_context_menu.dart +++ b/lib/src/views/components/user_context_menu.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 27c1c45..4c3523d 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; class VerifiedShield extends StatelessWidget { diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 31fcfde..52d2ef3 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; diff --git a/lib/src/views/contact/contact_verify.view.dart b/lib/src/views/contact/contact_verify.view.dart index 05b05c3..2b37dd7 100644 --- a/lib/src/views/contact/contact_verify.view.dart +++ b/lib/src/views/contact/contact_verify.view.dart @@ -9,8 +9,8 @@ import 'package:image/image.dart' as imglib; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:lottie/lottie.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.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/views/components/fingerprint_text.dart'; diff --git a/lib/src/views/contact/contact_verify_qr_scan.view.dart b/lib/src/views/contact/contact_verify_qr_scan.view.dart index 566a833..5328b42 100644 --- a/lib/src/views/contact/contact_verify_qr_scan.view.dart +++ b/lib/src/views/contact/contact_verify_qr_scan.view.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; class ContactVerifyQrScanView extends StatefulWidget { diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 91702c6..588fdfe 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/media_upload.dart' as send; import 'package:twonly/src/services/thumbnail.service.dart'; diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index a1c0103..5489e9b 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/media_download.dart' as received; import 'package:twonly/src/services/api/media_upload.dart' as send; diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index 8d9c415..2587c7b 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/api/messages.dart'; class AutomatedTestingView extends StatefulWidget { diff --git a/lib/src/views/settings/developer/retransmission_data.view.dart b/lib/src/views/settings/developer/retransmission_data.view.dart index 34538fc..4889c52 100644 --- a/lib/src/views/settings/developer/retransmission_data.view.dart +++ b/lib/src/views/settings/developer/retransmission_data.view.dart @@ -6,8 +6,8 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/api/messages.dart'; class RetransmissionDataView extends StatefulWidget { diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index ce1dd86..394f5e3 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/user_context_menu.dart'; diff --git a/lib/src/views/settings/subscription/additional_users.view.dart b/lib/src/views/settings/subscription/additional_users.view.dart index 1afc62f..5a76e88 100644 --- a/lib/src/views/settings/subscription/additional_users.view.dart +++ b/lib/src/views/settings/subscription/additional_users.view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/utils/log.dart'; diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index bdaeccd..4c0c8fc 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -7,7 +7,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/connection.provider.dart'; diff --git a/pubspec.lock b/pubspec.lock index 8b26fd6..7c121f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -162,12 +162,13 @@ packages: source: hosted version: "0.11.2" camera_android_camerax: - dependency: transitive + dependency: "direct overridden" description: - name: camera_android_camerax - sha256: "92dcc36e8ff2fa1ea3acdbb609ca2976cded55dceb719b4869c124c6d011f110" - url: "https://pub.dev" - source: hosted + path: "packages/camera/camera_android_camerax" + ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + resolved-ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + url: "https://github.com/otsmr/flutter-packages.git" + source: git version: "0.6.23+2" camera_avfoundation: dependency: transitive @@ -662,9 +663,11 @@ packages: flutter_secure_storage: dependency: "direct main" description: - path: "dependencies/flutter_secure_storage/flutter_secure_storage" - relative: true - source: path + path: flutter_secure_storage + ref: "71b75a36f35f2ce945998e20c6c6aa1820babfc6" + resolved-ref: "71b75a36f35f2ce945998e20c6c6aa1820babfc6" + url: "https://github.com/juliansteenbakker/flutter_secure_storage.git" + source: git version: "10.0.0-beta.4" flutter_secure_storage_darwin: dependency: transitive @@ -1278,10 +1281,12 @@ packages: pie_menu: dependency: "direct main" description: - path: "dependencies/flutter-pie-menu" - relative: true - source: path - version: "3.3.0" + path: "." + ref: HEAD + resolved-ref: e1ae0b2dabdfa9ad204b2cf93c48a5962e243c6c + url: "https://github.com/otsmr/flutter-pie-menu.git" + source: git + version: "3.3.2" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a12ab31..d01d5d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: avatar_maker: ^0.4.0 background_downloader: ^9.2.2 cached_network_image: ^3.4.1 - camera: ^0.11.1 + camera: ^0.11.2 collection: ^1.18.0 connectivity_plus: ^7.0.0 cryptography_flutter_plus: ^2.3.4 @@ -29,9 +29,11 @@ dependencies: flutter_local_notifications: ^19.1.0 flutter_localizations: sdk: flutter - # flutter_secure_storage: ^10.0.0-beta.4 flutter_secure_storage: - path: ./dependencies/flutter_secure_storage/flutter_secure_storage + git: + url: https://github.com/juliansteenbakker/flutter_secure_storage.git + ref: 71b75a36f35f2ce945998e20c6c6aa1820babfc6 # from develop + path: flutter_secure_storage/ flutter_svg: ^2.0.17 flutter_zxing: path: ./dependencies/flutter_zxing @@ -58,7 +60,8 @@ dependencies: permission_handler: ^12.0.0+1 photo_view: ^0.15.0 pie_menu: - path: ./dependencies/flutter-pie-menu + git: + url: https://github.com/otsmr/flutter-pie-menu.git protobuf: ^4.0.0 provider: ^6.1.2 restart_app: ^1.3.2 @@ -72,6 +75,15 @@ dependencies: video_thumbnail: ^0.5.6 web_socket_channel: ^3.0.1 +dependency_overrides: + # hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY + camera_android_camerax: + # path: ../flutter-packages/packages/camera/camera_android_camerax + git: + url: https://github.com/otsmr/flutter-packages.git + path: packages/camera/camera_android_camerax + ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + dev_dependencies: build_runner: ^2.4.15 drift_dev: ^2.25.2 diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index 7fb971d..ceb9d77 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -8,11 +8,16 @@ if [ ! -f "pubspec.yaml" ]; then fi # Definitions for twonly Safe -protoc --proto_path="./lib/src/model/protobuf/backup/" --dart_out="./lib/src/model/protobuf/backup/" "backup.proto" +GENERATED_DIR="./lib/src/model/protobuf/client/generated/" +CLIENT_DIR="./lib/src/model/protobuf/client/" -# Definitions for the Push Notifications -protoc --proto_path="./lib/src/model/protobuf/push_notification/" --dart_out="./lib/src/model/protobuf/push_notification/" "push_notification.proto" -protoc --proto_path="./lib/src/model/protobuf/push_notification/" --swift_out="./ios/NotificationService/" "push_notification.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "backup.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "messages.proto" + +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" +protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" + +exit # Definitions for the Server API diff --git a/test/unit_test.dart b/test/unit_test.dart index b37a39f..36e6252 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -31,5 +33,12 @@ void main() { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1))); }); + + test('encoding uuid4', () async { + final uv4 = uuid.v4(); + final uv4Bytes = Uint8List.fromList(uv4.codeUnits); + final uv4String = utf8.decode(uv4Bytes.cast()); + expect(uv4String, uv4); + }); }); } From 2f3f927914c4eeee7e88d4da78534f446db7d185 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Oct 2025 14:44:49 +0200 Subject: [PATCH 03/76] handle text and media server messages, update pushkeys --- .../push_notification.pb.swift | 20 +- lib/src/constants/secure_storage_keys.dart | 4 +- lib/src/database/daos/contacts.dao.dart | 5 +- lib/src/database/daos/groups.dao.dart | 14 +- lib/src/database/daos/mediafiles.dao.dart | 54 ++ lib/src/database/daos/mediafiles.dao.g.dart | 8 + lib/src/database/daos/messages.dao.dart | 74 +-- lib/src/database/daos/messages.dao.g.dart | 1 + lib/src/database/daos/reactions.dao.dart | 12 +- lib/src/database/daos/receipts.dao.dart | 45 +- lib/src/database/tables/contacts.table.dart | 1 - lib/src/database/tables/groups.table.dart | 1 + lib/src/database/tables/mediafiles.table.dart | 23 +- lib/src/database/tables/messages.table.dart | 9 +- lib/src/database/tables/receipts.table.dart | 3 + lib/src/database/twonly.db.dart | 4 +- lib/src/database/twonly.db.g.dart | 460 +++++++++++------- .../client/generated/messages.pb.dart | 142 ++++-- .../client/generated/messages.pbenum.dart | 10 +- .../client/generated/messages.pbjson.dart | 117 +++-- .../generated/push_notification.pb.dart | 16 +- .../generated/push_notification.pbjson.dart | 8 +- lib/src/model/protobuf/client/messages.proto | 29 +- .../protobuf/client/push_notification.proto | 4 +- lib/src/services/api.service.dart | 2 +- lib/src/services/api/media_download.dart | 2 +- lib/src/services/api/messages.dart | 287 +++++------ lib/src/services/api/server_messages.dart | 6 +- .../contact.server_messages.dart | 14 +- .../media.server_messages.dart | 230 +++++---- .../messages.server_messages.dart | 15 +- .../pushkeys.server_messages.dart | 3 + .../reaction.server_message.dart | 2 + .../text_message.server_messages.dart | 141 +----- .../background.notifications.dart | 2 +- .../notifications/pushkeys.notifications.dart | 143 ++++-- .../services/signal/encryption.signal.dart | 14 +- lib/src/services/thumbnail.service.dart | 14 + lib/src/utils/misc.dart | 6 + lib/src/views/settings/notification.view.dart | 9 +- test/unit_test.dart | 8 + 41 files changed, 1172 insertions(+), 790 deletions(-) create mode 100644 lib/src/database/daos/mediafiles.dao.dart create mode 100644 lib/src/database/daos/mediafiles.dao.g.dart diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index 8bdc778..3bddafc 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -128,8 +128,8 @@ struct PushNotification: Sendable { var kind: PushKind = .reaction - var messageID: Int64 { - get {return _messageID ?? 0} + var messageID: String { + get {return _messageID ?? String()} set {_messageID = newValue} } /// Returns true if `messageID` has been explicitly set. @@ -150,7 +150,7 @@ struct PushNotification: Sendable { init() {} - fileprivate var _messageID: Int64? = nil + fileprivate var _messageID: String? = nil fileprivate var _reactionContent: String? = nil } @@ -177,8 +177,8 @@ struct PushUser: Sendable { var blocked: Bool = false - var lastMessageID: Int64 { - get {return _lastMessageID ?? 0} + var lastMessageID: String { + get {return _lastMessageID ?? String()} set {_lastMessageID = newValue} } /// Returns true if `lastMessageID` has been explicitly set. @@ -192,7 +192,7 @@ struct PushUser: Sendable { init() {} - fileprivate var _lastMessageID: Int64? = nil + fileprivate var _lastMessageID: String? = nil } struct PushKey: Sendable { @@ -273,7 +273,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }() - case 2: try { try decoder.decodeSingularInt64Field(value: &self._messageID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }() case 3: try { try decoder.decodeSingularStringField(value: &self._reactionContent) }() default: break } @@ -289,7 +289,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1) } try { if let v = self._messageID { - try visitor.visitSingularInt64Field(value: v, fieldNumber: 2) + try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() try { if let v = self._reactionContent { try visitor.visitSingularStringField(value: v, fieldNumber: 3) @@ -349,7 +349,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }() case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }() case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }() - case 4: try { try decoder.decodeSingularInt64Field(value: &self._lastMessageID) }() + case 4: try { try decoder.decodeSingularStringField(value: &self._lastMessageID) }() case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }() default: break } @@ -371,7 +371,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3) } try { if let v = self._lastMessageID { - try visitor.visitSingularInt64Field(value: v, fieldNumber: 4) + try visitor.visitSingularStringField(value: v, fieldNumber: 4) } }() if !self.pushKeys.isEmpty { try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5) diff --git a/lib/src/constants/secure_storage_keys.dart b/lib/src/constants/secure_storage_keys.dart index 8e2eac5..43da069 100644 --- a/lib/src/constants/secure_storage_keys.dart +++ b/lib/src/constants/secure_storage_keys.dart @@ -6,6 +6,6 @@ class SecureStorageKeys { static const String userData = 'userData'; static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash'; - static const String receivingPushKeys = 'receiving_push_keys'; - static const String sendingPushKeys = 'sending_push_keys'; + static const String receivingPushKeys = 'push_keys_receiving'; + static const String sendingPushKeys = 'push_keys_sending'; } diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 9050ee0..54e70ab 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -120,10 +120,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { Stream> watchNotAcceptedContacts() { return (select(contacts) ..where( - (t) => - t.accepted.equals(false) & - t.archived.equals(false) & - t.blocked.equals(false), + (t) => t.accepted.equals(false) & t.blocked.equals(false), )) .watch(); // return (select(contacts)).watch(); diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 6c9536a..b004215 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -12,10 +12,18 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { GroupsDao(super.db); Future isContactInGroup(int contactId, String groupId) async { - final entry = await (select(groupMembers) - ..where( - (t) => t.contactId.equals(contactId) & t.groupId.equals(groupId))) + final entry = await (select(groupMembers)..where( + // ignore: require_trailing_commas + (t) => t.contactId.equals(contactId) & t.groupId.equals(groupId))) .getSingleOrNull(); return entry != null; } + + Future updateGroup( + String groupId, + GroupsCompanion updates, + ) async { + await (update(groups)..where((c) => c.groupId.equals(groupId))) + .write(updates); + } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart new file mode 100644 index 0000000..057f59e --- /dev/null +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -0,0 +1,54 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'mediafiles.dao.g.dart'; + +@DriftAccessor(tables: [MediaFiles]) +class MediaFilesDao extends DatabaseAccessor + with _$MediaFilesDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + MediaFilesDao(super.db); + + Future insertMedia(MediaFilesCompanion mediaFile) async { + try { + final rowId = await into(mediaFiles).insert(mediaFile); + + return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) + .getSingle(); + } catch (e) { + Log.error('Could not insert media file: $e'); + return null; + } + } + + Future updateMedia( + String mediaId, + MediaFilesCompanion updates, + ) async { + await (update(mediaFiles)..where((c) => c.mediaId.equals(mediaId))) + .write(updates); + } + + Future getMediaFileById(String mediaId) async { + return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) + .getSingleOrNull(); + } + + Future resetPendingDownloadState() async { + await (update(mediaFiles) + ..where( + (c) => c.downloadState.equals( + DownloadState.downloading.name, + ), + )) + .write( + const MediaFilesCompanion( + downloadState: Value(DownloadState.pending), + ), + ); + } +} diff --git a/lib/src/database/daos/mediafiles.dao.g.dart b/lib/src/database/daos/mediafiles.dao.g.dart new file mode 100644 index 0000000..0157e2d --- /dev/null +++ b/lib/src/database/daos/mediafiles.dao.g.dart @@ -0,0 +1,8 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mediafiles.dao.dart'; + +// ignore_for_file: type=lint +mixin _$MediaFilesDaoMixin on DatabaseAccessor { + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; +} diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 1244086..b96904e 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -1,13 +1,24 @@ import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; 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/services/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; part 'messages.dao.g.dart'; -@DriftAccessor(tables: [Messages, Contacts, MediaFiles, MessageHistories]) +@DriftAccessor( + tables: [ + Messages, + Contacts, + MediaFiles, + MessageHistories, + Groups, + ], +) class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -156,22 +167,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // .write(updates); // } - // Future resetPendingDownloadState() { - // // All media files in the downloading state are reset to the pending state - // // When the app is used in mobile network, they will not be downloaded at the start - // // if they are not yet downloaded... - // const updates = - // MessagesCompanion(downloadState: Value(DownloadState.pending)); - // return (update(messages) - // ..where( - // (t) => - // t.messageOtherId.isNotNull() & - // t.downloadState.equals(DownloadState.downloading.index) & - // t.kind.equals(MessageKind.media.name), - // )) - // .write(updates); - // } - Future handleMessageDeletion( int contactId, String messageId, @@ -278,28 +273,33 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // .write(updatedValues); // } - // Future updateMessageByMessageId( - // int messageId, - // MessagesCompanion updatedValues, - // ) { - // return (update(messages)..where((c) => c.messageId.equals(messageId))) - // .write(updatedValues); - // } + Future updateMessageId( + String messageId, + MessagesCompanion updatedValues, + ) { + return (update(messages)..where((c) => c.messageId.equals(messageId))) + .write(updatedValues); + } - // Future insertMessage(MessagesCompanion message) async { - // try { - // await (update(contacts) - // ..where( - // (c) => c.userId.equals(message.contactId.value), - // )) - // .write(ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); + Future insertMessage(MessagesCompanion message) async { + try { + final rowId = await into(messages).insert(message); - // return await into(messages).insert(message); - // } catch (e) { - // Log.error('Error while inserting message: $e'); - // return null; - // } - // } + await twonlyDB.groupsDao.updateGroup( + message.groupId.value, + GroupsCompanion( + lastMessageExchange: Value(DateTime.now()), + archived: const Value(false), + ), + ); + + return await (select(messages)..where((t) => t.rowId.equals(rowId))) + .getSingle(); + } catch (e) { + Log.error('Could not insert message: $e'); + return null; + } + } // Future deleteMessagesByContactId(int contactId) { // return (delete(messages) diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart index bad5c9e..5f11d3a 100644 --- a/lib/src/database/daos/messages.dao.g.dart +++ b/lib/src/database/daos/messages.dao.g.dart @@ -9,4 +9,5 @@ mixin _$MessagesDaoMixin on DatabaseAccessor { $MessagesTable get messages => attachedDatabase.messages; $MessageHistoriesTable get messageHistories => attachedDatabase.messageHistories; + $GroupsTable get groups => attachedDatabase.groups; } diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 65eb24f..1325d30 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -31,11 +31,13 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { )) .go(); if (emoji != null) { - await into(reactions).insert(ReactionsCompanion( - messageId: Value(messageId), - emoji: Value(emoji), - senderId: Value(contactId), - )); + await into(reactions).insert( + ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: Value(contactId), + ), + ); } } catch (e) { Log.error(e); diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index cf2193e..df65eb5 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -15,8 +15,10 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { Future confirmReceipt(String receiptId, int fromUserId) async { final receipt = await (select(receipts) - ..where((t) => - t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId))) + ..where( + (t) => + t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId), + )) .getSingleOrNull(); if (receipt == null) return; @@ -26,7 +28,7 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { ..where((t) => t.messageId.equals(receipt.messageId!))) .write( const MessagesCompanion( - acknowledgeByUser: Value(true), + ackByUser: Value(true), ), ); } @@ -39,6 +41,14 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { .go(); } + Future deleteReceipt(String receiptId) async { + await (delete(receipts) + ..where( + (t) => t.receiptId.equals(receiptId), + )) + .go(); + } + Future insertReceipt(ReceiptsCompanion entry) async { try { final id = await into(receipts).insert(entry); @@ -49,4 +59,33 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { return null; } } + + Future getReceiptById(String receiptId) async { + try { + return await (select(receipts) + ..where( + (t) => t.receiptId.equals(receiptId), + )) + .getSingleOrNull(); + } catch (e) { + Log.error(e); + return null; + } + } + + Future> getReceiptsNotAckByServer() async { + return (select(receipts) + ..where( + (t) => t.ackByServerAt.isNull(), + )) + .get(); + } + + Future updateReceipt( + String receiptId, + ReceiptsCompanion updates, + ) async { + await (update(receipts)..where((c) => c.receiptId.equals(receiptId))) + .write(updates); + } } diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 0f9e2b0..0aa40c0 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -16,7 +16,6 @@ class Contacts extends Table { BoolColumn get hidden => boolean().withDefault(const Constant(false))(); BoolColumn get blocked => boolean().withDefault(const Constant(false))(); BoolColumn get verified => boolean().withDefault(const Constant(false))(); - BoolColumn get archived => boolean().withDefault(const Constant(false))(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); BoolColumn get alsoBestFriend => diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 08148ae..e6548e4 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -9,6 +9,7 @@ class Groups extends Table { BoolColumn get isGroupAdmin => boolean()(); BoolColumn get isGroupOfTwo => boolean()(); BoolColumn get pinned => boolean().withDefault(const Constant(false))(); + BoolColumn get archived => boolean().withDefault(const Constant(false))(); DateTimeColumn get lastMessageExchange => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 24edcd4..db4a812 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; @@ -14,13 +16,11 @@ enum UploadState { receiverNotified, } -enum DownloadState { - pending, -} +enum DownloadState { pending, downloading } @DataClassName('MediaFile') class MediaFiles extends Table { - TextColumn get mediaId => text().clientDefault(() => uuid.v4())(); + TextColumn get mediaId => text().clientDefault(() => uuid.v7())(); TextColumn get type => textEnum()(); @@ -34,6 +34,9 @@ class MediaFiles extends Table { BoolColumn get storedByContact => boolean().withDefault(const Constant(false))(); + TextColumn get reuploadRequestedBy => + text().map(IntListTypeConverter()).nullable()(); + IntColumn get displayLimitInMilliseconds => integer().nullable()(); BlobColumn get downloadToken => blob().nullable()(); @@ -46,3 +49,15 @@ class MediaFiles extends Table { @override Set get primaryKey => {mediaId}; } + +class IntListTypeConverter extends TypeConverter, String> { + @override + List fromSql(String fromDb) { + return List.from(jsonDecode(fromDb) as Iterable); + } + + @override + String toSql(List value) { + return json.encode(value); + } +} diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 2e33471..3537c63 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -1,11 +1,12 @@ import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @DataClassName('Message') class Messages extends Table { TextColumn get groupId => text()(); - TextColumn get messageId => text()(); + TextColumn get messageId => text().clientDefault(() => uuid.v7())(); // in case senderId is null, it was send by user itself IntColumn get senderId => @@ -23,10 +24,8 @@ class Messages extends Table { BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); - BoolColumn get acknowledgeByUser => - boolean().withDefault(const Constant(false))(); - BoolColumn get acknowledgeByServer => - boolean().withDefault(const Constant(false))(); + BoolColumn get ackByUser => boolean().withDefault(const Constant(false))(); + BoolColumn get ackByServer => boolean().withDefault(const Constant(false))(); IntColumn get openedByCounter => integer().withDefault(const Constant(0))(); DateTimeColumn get openedAt => dateTime().nullable()(); diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart index 3ccf85e..c0fa4e6 100644 --- a/lib/src/database/tables/receipts.table.dart +++ b/lib/src/database/tables/receipts.table.dart @@ -15,11 +15,14 @@ class Receipts extends Table { .nullable() .references(Messages, #messageId, onDelete: KeyAction.cascade)(); + /// This is the protobuf 'Message' BlobColumn get message => blob()(); BoolColumn get contactWillSendsReceipt => boolean().withDefault(const Constant(true))(); + DateTimeColumn get ackByServerAt => dateTime().nullable()(); + IntColumn get retryCount => integer().withDefault(const Constant(0))(); DateTimeColumn get lastRetry => dateTime().nullable()(); diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index d4f21f0..1594ce8 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -4,6 +4,7 @@ import 'package:drift_flutter/drift_flutter.dart' import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/daos/mediafiles.dao.dart'; import 'package:twonly/src/database/daos/messages.dao.dart'; import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart'; @@ -48,7 +49,8 @@ part 'twonly.db.g.dart'; SignalDao, ReceiptsDao, GroupsDao, - ReactionsDao + ReactionsDao, + MediaFilesDao, ], ) class TwonlyDB extends _$TwonlyDB { diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 8238265..472e56a 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -94,16 +94,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _archivedMeta = - const VerificationMeta('archived'); - @override - late final GeneratedColumn archived = GeneratedColumn( - 'archived', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), - defaultValue: const Constant(false)); static const VerificationMeta _deletedMeta = const VerificationMeta('deleted'); @override @@ -194,7 +184,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { hidden, blocked, verified, - archived, deleted, alsoBestFriend, deleteMessagesAfterXMinutes, @@ -266,10 +255,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_verifiedMeta, verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta)); } - if (data.containsKey('archived')) { - context.handle(_archivedMeta, - archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); - } if (data.containsKey('deleted')) { context.handle(_deletedMeta, deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); @@ -358,8 +343,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, verified: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, - archived: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, deleted: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, alsoBestFriend: attachedDatabase.typeMapping @@ -404,7 +387,6 @@ class Contact extends DataClass implements Insertable { final bool hidden; final bool blocked; final bool verified; - final bool archived; final bool deleted; final bool alsoBestFriend; final int deleteMessagesAfterXMinutes; @@ -427,7 +409,6 @@ class Contact extends DataClass implements Insertable { required this.hidden, required this.blocked, required this.verified, - required this.archived, required this.deleted, required this.alsoBestFriend, required this.deleteMessagesAfterXMinutes, @@ -458,7 +439,6 @@ class Contact extends DataClass implements Insertable { map['hidden'] = Variable(hidden); map['blocked'] = Variable(blocked); map['verified'] = Variable(verified); - map['archived'] = Variable(archived); map['deleted'] = Variable(deleted); map['also_best_friend'] = Variable(alsoBestFriend); map['delete_messages_after_x_minutes'] = @@ -501,7 +481,6 @@ class Contact extends DataClass implements Insertable { hidden: Value(hidden), blocked: Value(blocked), verified: Value(verified), - archived: Value(archived), deleted: Value(deleted), alsoBestFriend: Value(alsoBestFriend), deleteMessagesAfterXMinutes: Value(deleteMessagesAfterXMinutes), @@ -539,7 +518,6 @@ class Contact extends DataClass implements Insertable { hidden: serializer.fromJson(json['hidden']), blocked: serializer.fromJson(json['blocked']), verified: serializer.fromJson(json['verified']), - archived: serializer.fromJson(json['archived']), deleted: serializer.fromJson(json['deleted']), alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), deleteMessagesAfterXMinutes: @@ -570,7 +548,6 @@ class Contact extends DataClass implements Insertable { 'hidden': serializer.toJson(hidden), 'blocked': serializer.toJson(blocked), 'verified': serializer.toJson(verified), - 'archived': serializer.toJson(archived), 'deleted': serializer.toJson(deleted), 'alsoBestFriend': serializer.toJson(alsoBestFriend), 'deleteMessagesAfterXMinutes': @@ -598,7 +575,6 @@ class Contact extends DataClass implements Insertable { bool? hidden, bool? blocked, bool? verified, - bool? archived, bool? deleted, bool? alsoBestFriend, int? deleteMessagesAfterXMinutes, @@ -621,7 +597,6 @@ class Contact extends DataClass implements Insertable { hidden: hidden ?? this.hidden, blocked: blocked ?? this.blocked, verified: verified ?? this.verified, - archived: archived ?? this.archived, deleted: deleted ?? this.deleted, alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, deleteMessagesAfterXMinutes: @@ -657,7 +632,6 @@ class Contact extends DataClass implements Insertable { hidden: data.hidden.present ? data.hidden.value : this.hidden, blocked: data.blocked.present ? data.blocked.value : this.blocked, verified: data.verified.present ? data.verified.value : this.verified, - archived: data.archived.present ? data.archived.value : this.archived, deleted: data.deleted.present ? data.deleted.value : this.deleted, alsoBestFriend: data.alsoBestFriend.present ? data.alsoBestFriend.value @@ -701,7 +675,6 @@ class Contact extends DataClass implements Insertable { ..write('hidden: $hidden, ') ..write('blocked: $blocked, ') ..write('verified: $verified, ') - ..write('archived: $archived, ') ..write('deleted: $deleted, ') ..write('alsoBestFriend: $alsoBestFriend, ') ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') @@ -729,7 +702,6 @@ class Contact extends DataClass implements Insertable { hidden, blocked, verified, - archived, deleted, alsoBestFriend, deleteMessagesAfterXMinutes, @@ -756,7 +728,6 @@ class Contact extends DataClass implements Insertable { other.hidden == this.hidden && other.blocked == this.blocked && other.verified == this.verified && - other.archived == this.archived && other.deleted == this.deleted && other.alsoBestFriend == this.alsoBestFriend && other.deleteMessagesAfterXMinutes == @@ -782,7 +753,6 @@ class ContactsCompanion extends UpdateCompanion { final Value hidden; final Value blocked; final Value verified; - final Value archived; final Value deleted; final Value alsoBestFriend; final Value deleteMessagesAfterXMinutes; @@ -805,7 +775,6 @@ class ContactsCompanion extends UpdateCompanion { this.hidden = const Value.absent(), this.blocked = const Value.absent(), this.verified = const Value.absent(), - this.archived = const Value.absent(), this.deleted = const Value.absent(), this.alsoBestFriend = const Value.absent(), this.deleteMessagesAfterXMinutes = const Value.absent(), @@ -829,7 +798,6 @@ class ContactsCompanion extends UpdateCompanion { this.hidden = const Value.absent(), this.blocked = const Value.absent(), this.verified = const Value.absent(), - this.archived = const Value.absent(), this.deleted = const Value.absent(), this.alsoBestFriend = const Value.absent(), this.deleteMessagesAfterXMinutes = const Value.absent(), @@ -853,7 +821,6 @@ class ContactsCompanion extends UpdateCompanion { Expression? hidden, Expression? blocked, Expression? verified, - Expression? archived, Expression? deleted, Expression? alsoBestFriend, Expression? deleteMessagesAfterXMinutes, @@ -878,7 +845,6 @@ class ContactsCompanion extends UpdateCompanion { if (hidden != null) 'hidden': hidden, if (blocked != null) 'blocked': blocked, if (verified != null) 'verified': verified, - if (archived != null) 'archived': archived, if (deleted != null) 'deleted': deleted, if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, if (deleteMessagesAfterXMinutes != null) @@ -907,7 +873,6 @@ class ContactsCompanion extends UpdateCompanion { Value? hidden, Value? blocked, Value? verified, - Value? archived, Value? deleted, Value? alsoBestFriend, Value? deleteMessagesAfterXMinutes, @@ -930,7 +895,6 @@ class ContactsCompanion extends UpdateCompanion { hidden: hidden ?? this.hidden, blocked: blocked ?? this.blocked, verified: verified ?? this.verified, - archived: archived ?? this.archived, deleted: deleted ?? this.deleted, alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, deleteMessagesAfterXMinutes: @@ -982,9 +946,6 @@ class ContactsCompanion extends UpdateCompanion { if (verified.present) { map['verified'] = Variable(verified.value); } - if (archived.present) { - map['archived'] = Variable(archived.value); - } if (deleted.present) { map['deleted'] = Variable(deleted.value); } @@ -1035,7 +996,6 @@ class ContactsCompanion extends UpdateCompanion { ..write('hidden: $hidden, ') ..write('blocked: $blocked, ') ..write('verified: $verified, ') - ..write('archived: $archived, ') ..write('deleted: $deleted, ') ..write('alsoBestFriend: $alsoBestFriend, ') ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') @@ -1064,7 +1024,7 @@ class $MediaFilesTable extends MediaFiles 'media_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: false, - clientDefault: () => uuid.v4()); + clientDefault: () => uuid.v7()); @override late final GeneratedColumnWithTypeConverter type = GeneratedColumn('type', aliasedName, false, @@ -1111,6 +1071,13 @@ class $MediaFilesTable extends MediaFiles defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("stored_by_contact" IN (0, 1))'), defaultValue: const Constant(false)); + @override + late final GeneratedColumnWithTypeConverter?, String> + reuploadRequestedBy = GeneratedColumn( + 'reupload_requested_by', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $MediaFilesTable.$converterreuploadRequestedByn); static const VerificationMeta _displayLimitInMillisecondsMeta = const VerificationMeta('displayLimitInMilliseconds'); @override @@ -1158,6 +1125,7 @@ class $MediaFilesTable extends MediaFiles requiresAuthentication, reopenByContact, storedByContact, + reuploadRequestedBy, displayLimitInMilliseconds, downloadToken, encryptionKey, @@ -1260,6 +1228,9 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, storedByContact: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!, + reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}reupload_requested_by'])), displayLimitInMilliseconds: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}display_limit_in_milliseconds']), @@ -1294,6 +1265,10 @@ class $MediaFilesTable extends MediaFiles static JsonTypeConverter2 $converterdownloadStaten = JsonTypeConverter2.asNullable($converterdownloadState); + static TypeConverter, String> $converterreuploadRequestedBy = + IntListTypeConverter(); + static TypeConverter?, String?> $converterreuploadRequestedByn = + NullAwareTypeConverter.wrap($converterreuploadRequestedBy); } class MediaFile extends DataClass implements Insertable { @@ -1304,6 +1279,7 @@ class MediaFile extends DataClass implements Insertable { final bool requiresAuthentication; final bool reopenByContact; final bool storedByContact; + final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; final Uint8List? downloadToken; final Uint8List? encryptionKey; @@ -1318,6 +1294,7 @@ class MediaFile extends DataClass implements Insertable { required this.requiresAuthentication, required this.reopenByContact, required this.storedByContact, + this.reuploadRequestedBy, this.displayLimitInMilliseconds, this.downloadToken, this.encryptionKey, @@ -1343,6 +1320,11 @@ class MediaFile extends DataClass implements Insertable { map['requires_authentication'] = Variable(requiresAuthentication); map['reopen_by_contact'] = Variable(reopenByContact); map['stored_by_contact'] = Variable(storedByContact); + if (!nullToAbsent || reuploadRequestedBy != null) { + map['reupload_requested_by'] = Variable($MediaFilesTable + .$converterreuploadRequestedByn + .toSql(reuploadRequestedBy)); + } if (!nullToAbsent || displayLimitInMilliseconds != null) { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds); @@ -1376,6 +1358,9 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: Value(requiresAuthentication), reopenByContact: Value(reopenByContact), storedByContact: Value(storedByContact), + reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent + ? const Value.absent() + : Value(reuploadRequestedBy), displayLimitInMilliseconds: displayLimitInMilliseconds == null && nullToAbsent ? const Value.absent() @@ -1411,6 +1396,8 @@ class MediaFile extends DataClass implements Insertable { serializer.fromJson(json['requiresAuthentication']), reopenByContact: serializer.fromJson(json['reopenByContact']), storedByContact: serializer.fromJson(json['storedByContact']), + reuploadRequestedBy: + serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: serializer.fromJson(json['displayLimitInMilliseconds']), downloadToken: serializer.fromJson(json['downloadToken']), @@ -1434,6 +1421,7 @@ class MediaFile extends DataClass implements Insertable { 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'reopenByContact': serializer.toJson(reopenByContact), 'storedByContact': serializer.toJson(storedByContact), + 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), 'downloadToken': serializer.toJson(downloadToken), @@ -1452,6 +1440,7 @@ class MediaFile extends DataClass implements Insertable { bool? requiresAuthentication, bool? reopenByContact, bool? storedByContact, + Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), @@ -1468,6 +1457,9 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, storedByContact: storedByContact ?? this.storedByContact, + reuploadRequestedBy: reuploadRequestedBy.present + ? reuploadRequestedBy.value + : this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds.present ? displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, @@ -1500,6 +1492,9 @@ class MediaFile extends DataClass implements Insertable { storedByContact: data.storedByContact.present ? data.storedByContact.value : this.storedByContact, + reuploadRequestedBy: data.reuploadRequestedBy.present + ? data.reuploadRequestedBy.value + : this.reuploadRequestedBy, displayLimitInMilliseconds: data.displayLimitInMilliseconds.present ? data.displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, @@ -1529,6 +1524,7 @@ class MediaFile extends DataClass implements Insertable { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('storedByContact: $storedByContact, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') @@ -1548,6 +1544,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication, reopenByContact, storedByContact, + reuploadRequestedBy, displayLimitInMilliseconds, $driftBlobEquality.hash(downloadToken), $driftBlobEquality.hash(encryptionKey), @@ -1565,6 +1562,7 @@ class MediaFile extends DataClass implements Insertable { other.requiresAuthentication == this.requiresAuthentication && other.reopenByContact == this.reopenByContact && other.storedByContact == this.storedByContact && + other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && @@ -1582,6 +1580,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value requiresAuthentication; final Value reopenByContact; final Value storedByContact; + final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; final Value downloadToken; final Value encryptionKey; @@ -1597,6 +1596,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.storedByContact = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), @@ -1613,6 +1613,7 @@ class MediaFilesCompanion extends UpdateCompanion { required bool requiresAuthentication, this.reopenByContact = const Value.absent(), this.storedByContact = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), @@ -1630,6 +1631,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? requiresAuthentication, Expression? reopenByContact, Expression? storedByContact, + Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, Expression? downloadToken, Expression? encryptionKey, @@ -1647,6 +1649,8 @@ class MediaFilesCompanion extends UpdateCompanion { 'requires_authentication': requiresAuthentication, if (reopenByContact != null) 'reopen_by_contact': reopenByContact, if (storedByContact != null) 'stored_by_contact': storedByContact, + if (reuploadRequestedBy != null) + 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) 'display_limit_in_milliseconds': displayLimitInMilliseconds, if (downloadToken != null) 'download_token': downloadToken, @@ -1666,6 +1670,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? requiresAuthentication, Value? reopenByContact, Value? storedByContact, + Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, Value? downloadToken, Value? encryptionKey, @@ -1682,6 +1687,7 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, storedByContact: storedByContact ?? this.storedByContact, + reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, downloadToken: downloadToken ?? this.downloadToken, @@ -1721,6 +1727,11 @@ class MediaFilesCompanion extends UpdateCompanion { if (storedByContact.present) { map['stored_by_contact'] = Variable(storedByContact.value); } + if (reuploadRequestedBy.present) { + map['reupload_requested_by'] = Variable($MediaFilesTable + .$converterreuploadRequestedByn + .toSql(reuploadRequestedBy.value)); + } if (displayLimitInMilliseconds.present) { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds.value); @@ -1756,6 +1767,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('storedByContact: $storedByContact, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') @@ -1784,7 +1796,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { @override late final GeneratedColumn messageId = GeneratedColumn( 'message_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v7()); static const VerificationMeta _senderIdMeta = const VerificationMeta('senderId'); @override @@ -1838,25 +1852,25 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_edited" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _acknowledgeByUserMeta = - const VerificationMeta('acknowledgeByUser'); + static const VerificationMeta _ackByUserMeta = + const VerificationMeta('ackByUser'); @override - late final GeneratedColumn acknowledgeByUser = GeneratedColumn( - 'acknowledge_by_user', aliasedName, false, + late final GeneratedColumn ackByUser = GeneratedColumn( + 'ack_by_user', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("acknowledge_by_user" IN (0, 1))'), + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("ack_by_user" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _acknowledgeByServerMeta = - const VerificationMeta('acknowledgeByServer'); + static const VerificationMeta _ackByServerMeta = + const VerificationMeta('ackByServer'); @override - late final GeneratedColumn acknowledgeByServer = GeneratedColumn( - 'acknowledge_by_server', aliasedName, false, + late final GeneratedColumn ackByServer = GeneratedColumn( + 'ack_by_server', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("acknowledge_by_server" IN (0, 1))'), + 'CHECK ("ack_by_server" IN (0, 1))'), defaultValue: const Constant(false)); static const VerificationMeta _openedByCounterMeta = const VerificationMeta('openedByCounter'); @@ -1898,8 +1912,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { quotesMessageId, isDeletedFromSender, isEdited, - acknowledgeByUser, - acknowledgeByServer, + ackByUser, + ackByServer, openedByCounter, openedAt, createdAt, @@ -1924,8 +1938,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { if (data.containsKey('message_id')) { context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); - } else if (isInserting) { - context.missing(_messageIdMeta); } if (data.containsKey('sender_id')) { context.handle(_senderIdMeta, @@ -1955,17 +1967,17 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_isEditedMeta, isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta)); } - if (data.containsKey('acknowledge_by_user')) { + if (data.containsKey('ack_by_user')) { context.handle( - _acknowledgeByUserMeta, - acknowledgeByUser.isAcceptableOrUnknown( - data['acknowledge_by_user']!, _acknowledgeByUserMeta)); + _ackByUserMeta, + ackByUser.isAcceptableOrUnknown( + data['ack_by_user']!, _ackByUserMeta)); } - if (data.containsKey('acknowledge_by_server')) { + if (data.containsKey('ack_by_server')) { context.handle( - _acknowledgeByServerMeta, - acknowledgeByServer.isAcceptableOrUnknown( - data['acknowledge_by_server']!, _acknowledgeByServerMeta)); + _ackByServerMeta, + ackByServer.isAcceptableOrUnknown( + data['ack_by_server']!, _ackByServerMeta)); } if (data.containsKey('opened_by_counter')) { context.handle( @@ -2012,10 +2024,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, isEdited: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}is_edited'])!, - acknowledgeByUser: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}acknowledge_by_user'])!, - acknowledgeByServer: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}acknowledge_by_server'])!, + ackByUser: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}ack_by_user'])!, + ackByServer: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}ack_by_server'])!, openedByCounter: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}opened_by_counter'])!, openedAt: attachedDatabase.typeMapping @@ -2042,8 +2054,8 @@ class Message extends DataClass implements Insertable { final String? quotesMessageId; final bool isDeletedFromSender; final bool isEdited; - final bool acknowledgeByUser; - final bool acknowledgeByServer; + final bool ackByUser; + final bool ackByServer; final int openedByCounter; final DateTime? openedAt; final DateTime createdAt; @@ -2057,8 +2069,8 @@ class Message extends DataClass implements Insertable { this.quotesMessageId, required this.isDeletedFromSender, required this.isEdited, - required this.acknowledgeByUser, - required this.acknowledgeByServer, + required this.ackByUser, + required this.ackByServer, required this.openedByCounter, this.openedAt, required this.createdAt, @@ -2082,8 +2094,8 @@ class Message extends DataClass implements Insertable { } map['is_deleted_from_sender'] = Variable(isDeletedFromSender); map['is_edited'] = Variable(isEdited); - map['acknowledge_by_user'] = Variable(acknowledgeByUser); - map['acknowledge_by_server'] = Variable(acknowledgeByServer); + map['ack_by_user'] = Variable(ackByUser); + map['ack_by_server'] = Variable(ackByServer); map['opened_by_counter'] = Variable(openedByCounter); if (!nullToAbsent || openedAt != null) { map['opened_at'] = Variable(openedAt); @@ -2113,8 +2125,8 @@ class Message extends DataClass implements Insertable { : Value(quotesMessageId), isDeletedFromSender: Value(isDeletedFromSender), isEdited: Value(isEdited), - acknowledgeByUser: Value(acknowledgeByUser), - acknowledgeByServer: Value(acknowledgeByServer), + ackByUser: Value(ackByUser), + ackByServer: Value(ackByServer), openedByCounter: Value(openedByCounter), openedAt: openedAt == null && nullToAbsent ? const Value.absent() @@ -2139,9 +2151,8 @@ class Message extends DataClass implements Insertable { isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), isEdited: serializer.fromJson(json['isEdited']), - acknowledgeByUser: serializer.fromJson(json['acknowledgeByUser']), - acknowledgeByServer: - serializer.fromJson(json['acknowledgeByServer']), + ackByUser: serializer.fromJson(json['ackByUser']), + ackByServer: serializer.fromJson(json['ackByServer']), openedByCounter: serializer.fromJson(json['openedByCounter']), openedAt: serializer.fromJson(json['openedAt']), createdAt: serializer.fromJson(json['createdAt']), @@ -2160,8 +2171,8 @@ class Message extends DataClass implements Insertable { 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), 'isEdited': serializer.toJson(isEdited), - 'acknowledgeByUser': serializer.toJson(acknowledgeByUser), - 'acknowledgeByServer': serializer.toJson(acknowledgeByServer), + 'ackByUser': serializer.toJson(ackByUser), + 'ackByServer': serializer.toJson(ackByServer), 'openedByCounter': serializer.toJson(openedByCounter), 'openedAt': serializer.toJson(openedAt), 'createdAt': serializer.toJson(createdAt), @@ -2178,8 +2189,8 @@ class Message extends DataClass implements Insertable { Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, bool? isEdited, - bool? acknowledgeByUser, - bool? acknowledgeByServer, + bool? ackByUser, + bool? ackByServer, int? openedByCounter, Value openedAt = const Value.absent(), DateTime? createdAt, @@ -2195,8 +2206,8 @@ class Message extends DataClass implements Insertable { : this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, - acknowledgeByUser: acknowledgeByUser ?? this.acknowledgeByUser, - acknowledgeByServer: acknowledgeByServer ?? this.acknowledgeByServer, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, openedByCounter: openedByCounter ?? this.openedByCounter, openedAt: openedAt.present ? openedAt.value : this.openedAt, createdAt: createdAt ?? this.createdAt, @@ -2216,12 +2227,9 @@ class Message extends DataClass implements Insertable { ? data.isDeletedFromSender.value : this.isDeletedFromSender, isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, - acknowledgeByUser: data.acknowledgeByUser.present - ? data.acknowledgeByUser.value - : this.acknowledgeByUser, - acknowledgeByServer: data.acknowledgeByServer.present - ? data.acknowledgeByServer.value - : this.acknowledgeByServer, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: + data.ackByServer.present ? data.ackByServer.value : this.ackByServer, openedByCounter: data.openedByCounter.present ? data.openedByCounter.value : this.openedByCounter, @@ -2243,8 +2251,8 @@ class Message extends DataClass implements Insertable { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') - ..write('acknowledgeByUser: $acknowledgeByUser, ') - ..write('acknowledgeByServer: $acknowledgeByServer, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') ..write('openedByCounter: $openedByCounter, ') ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') @@ -2263,8 +2271,8 @@ class Message extends DataClass implements Insertable { quotesMessageId, isDeletedFromSender, isEdited, - acknowledgeByUser, - acknowledgeByServer, + ackByUser, + ackByServer, openedByCounter, openedAt, createdAt, @@ -2281,8 +2289,8 @@ class Message extends DataClass implements Insertable { other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && other.isEdited == this.isEdited && - other.acknowledgeByUser == this.acknowledgeByUser && - other.acknowledgeByServer == this.acknowledgeByServer && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer && other.openedByCounter == this.openedByCounter && other.openedAt == this.openedAt && other.createdAt == this.createdAt && @@ -2298,8 +2306,8 @@ class MessagesCompanion extends UpdateCompanion { final Value quotesMessageId; final Value isDeletedFromSender; final Value isEdited; - final Value acknowledgeByUser; - final Value acknowledgeByServer; + final Value ackByUser; + final Value ackByServer; final Value openedByCounter; final Value openedAt; final Value createdAt; @@ -2314,8 +2322,8 @@ class MessagesCompanion extends UpdateCompanion { this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), - this.acknowledgeByUser = const Value.absent(), - this.acknowledgeByServer = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), this.openedByCounter = const Value.absent(), this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), @@ -2324,22 +2332,21 @@ class MessagesCompanion extends UpdateCompanion { }); MessagesCompanion.insert({ required String groupId, - required String messageId, + this.messageId = const Value.absent(), this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), - this.acknowledgeByUser = const Value.absent(), - this.acknowledgeByServer = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), this.openedByCounter = const Value.absent(), this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), - }) : groupId = Value(groupId), - messageId = Value(messageId); + }) : groupId = Value(groupId); static Insertable custom({ Expression? groupId, Expression? messageId, @@ -2349,8 +2356,8 @@ class MessagesCompanion extends UpdateCompanion { Expression? quotesMessageId, Expression? isDeletedFromSender, Expression? isEdited, - Expression? acknowledgeByUser, - Expression? acknowledgeByServer, + Expression? ackByUser, + Expression? ackByServer, Expression? openedByCounter, Expression? openedAt, Expression? createdAt, @@ -2367,9 +2374,8 @@ class MessagesCompanion extends UpdateCompanion { if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, if (isEdited != null) 'is_edited': isEdited, - if (acknowledgeByUser != null) 'acknowledge_by_user': acknowledgeByUser, - if (acknowledgeByServer != null) - 'acknowledge_by_server': acknowledgeByServer, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, if (openedByCounter != null) 'opened_by_counter': openedByCounter, if (openedAt != null) 'opened_at': openedAt, if (createdAt != null) 'created_at': createdAt, @@ -2387,8 +2393,8 @@ class MessagesCompanion extends UpdateCompanion { Value? quotesMessageId, Value? isDeletedFromSender, Value? isEdited, - Value? acknowledgeByUser, - Value? acknowledgeByServer, + Value? ackByUser, + Value? ackByServer, Value? openedByCounter, Value? openedAt, Value? createdAt, @@ -2403,8 +2409,8 @@ class MessagesCompanion extends UpdateCompanion { quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, - acknowledgeByUser: acknowledgeByUser ?? this.acknowledgeByUser, - acknowledgeByServer: acknowledgeByServer ?? this.acknowledgeByServer, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, openedByCounter: openedByCounter ?? this.openedByCounter, openedAt: openedAt ?? this.openedAt, createdAt: createdAt ?? this.createdAt, @@ -2440,11 +2446,11 @@ class MessagesCompanion extends UpdateCompanion { if (isEdited.present) { map['is_edited'] = Variable(isEdited.value); } - if (acknowledgeByUser.present) { - map['acknowledge_by_user'] = Variable(acknowledgeByUser.value); + if (ackByUser.present) { + map['ack_by_user'] = Variable(ackByUser.value); } - if (acknowledgeByServer.present) { - map['acknowledge_by_server'] = Variable(acknowledgeByServer.value); + if (ackByServer.present) { + map['ack_by_server'] = Variable(ackByServer.value); } if (openedByCounter.present) { map['opened_by_counter'] = Variable(openedByCounter.value); @@ -2475,8 +2481,8 @@ class MessagesCompanion extends UpdateCompanion { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') - ..write('acknowledgeByUser: $acknowledgeByUser, ') - ..write('acknowledgeByServer: $acknowledgeByServer, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') ..write('openedByCounter: $openedByCounter, ') ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') @@ -3042,6 +3048,16 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _archivedMeta = + const VerificationMeta('archived'); + @override + late final GeneratedColumn archived = GeneratedColumn( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _lastMessageExchangeMeta = const VerificationMeta('lastMessageExchange'); @override @@ -3064,6 +3080,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { isGroupAdmin, isGroupOfTwo, pinned, + archived, lastMessageExchange, createdAt ]; @@ -3101,6 +3118,10 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); } + if (data.containsKey('archived')) { + context.handle(_archivedMeta, + archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); + } if (data.containsKey('last_message_exchange')) { context.handle( _lastMessageExchangeMeta, @@ -3128,6 +3149,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { .read(DriftSqlType.bool, data['${effectivePrefix}is_group_of_two'])!, pinned: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, lastMessageExchange: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}last_message_exchange'])!, @@ -3147,6 +3170,7 @@ class Group extends DataClass implements Insertable { final bool isGroupAdmin; final bool isGroupOfTwo; final bool pinned; + final bool archived; final DateTime lastMessageExchange; final DateTime createdAt; const Group( @@ -3154,6 +3178,7 @@ class Group extends DataClass implements Insertable { required this.isGroupAdmin, required this.isGroupOfTwo, required this.pinned, + required this.archived, required this.lastMessageExchange, required this.createdAt}); @override @@ -3163,6 +3188,7 @@ class Group extends DataClass implements Insertable { map['is_group_admin'] = Variable(isGroupAdmin); map['is_group_of_two'] = Variable(isGroupOfTwo); map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); map['last_message_exchange'] = Variable(lastMessageExchange); map['created_at'] = Variable(createdAt); return map; @@ -3174,6 +3200,7 @@ class Group extends DataClass implements Insertable { isGroupAdmin: Value(isGroupAdmin), isGroupOfTwo: Value(isGroupOfTwo), pinned: Value(pinned), + archived: Value(archived), lastMessageExchange: Value(lastMessageExchange), createdAt: Value(createdAt), ); @@ -3187,6 +3214,7 @@ class Group extends DataClass implements Insertable { isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), lastMessageExchange: serializer.fromJson(json['lastMessageExchange']), createdAt: serializer.fromJson(json['createdAt']), @@ -3200,6 +3228,7 @@ class Group extends DataClass implements Insertable { 'isGroupAdmin': serializer.toJson(isGroupAdmin), 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), 'lastMessageExchange': serializer.toJson(lastMessageExchange), 'createdAt': serializer.toJson(createdAt), }; @@ -3210,6 +3239,7 @@ class Group extends DataClass implements Insertable { bool? isGroupAdmin, bool? isGroupOfTwo, bool? pinned, + bool? archived, DateTime? lastMessageExchange, DateTime? createdAt}) => Group( @@ -3217,6 +3247,7 @@ class Group extends DataClass implements Insertable { isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, createdAt: createdAt ?? this.createdAt, ); @@ -3230,6 +3261,7 @@ class Group extends DataClass implements Insertable { ? data.isGroupOfTwo.value : this.isGroupOfTwo, pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, lastMessageExchange: data.lastMessageExchange.present ? data.lastMessageExchange.value : this.lastMessageExchange, @@ -3244,6 +3276,7 @@ class Group extends DataClass implements Insertable { ..write('isGroupAdmin: $isGroupAdmin, ') ..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('pinned: $pinned, ') + ..write('archived: $archived, ') ..write('lastMessageExchange: $lastMessageExchange, ') ..write('createdAt: $createdAt') ..write(')')) @@ -3252,7 +3285,7 @@ class Group extends DataClass implements Insertable { @override int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, - lastMessageExchange, createdAt); + archived, lastMessageExchange, createdAt); @override bool operator ==(Object other) => identical(this, other) || @@ -3261,6 +3294,7 @@ class Group extends DataClass implements Insertable { other.isGroupAdmin == this.isGroupAdmin && other.isGroupOfTwo == this.isGroupOfTwo && other.pinned == this.pinned && + other.archived == this.archived && other.lastMessageExchange == this.lastMessageExchange && other.createdAt == this.createdAt); } @@ -3270,6 +3304,7 @@ class GroupsCompanion extends UpdateCompanion { final Value isGroupAdmin; final Value isGroupOfTwo; final Value pinned; + final Value archived; final Value lastMessageExchange; final Value createdAt; final Value rowid; @@ -3278,6 +3313,7 @@ class GroupsCompanion extends UpdateCompanion { this.isGroupAdmin = const Value.absent(), this.isGroupOfTwo = const Value.absent(), this.pinned = const Value.absent(), + this.archived = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), @@ -3287,6 +3323,7 @@ class GroupsCompanion extends UpdateCompanion { required bool isGroupAdmin, required bool isGroupOfTwo, this.pinned = const Value.absent(), + this.archived = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), @@ -3297,6 +3334,7 @@ class GroupsCompanion extends UpdateCompanion { Expression? isGroupAdmin, Expression? isGroupOfTwo, Expression? pinned, + Expression? archived, Expression? lastMessageExchange, Expression? createdAt, Expression? rowid, @@ -3306,6 +3344,7 @@ class GroupsCompanion extends UpdateCompanion { if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, if (lastMessageExchange != null) 'last_message_exchange': lastMessageExchange, if (createdAt != null) 'created_at': createdAt, @@ -3318,6 +3357,7 @@ class GroupsCompanion extends UpdateCompanion { Value? isGroupAdmin, Value? isGroupOfTwo, Value? pinned, + Value? archived, Value? lastMessageExchange, Value? createdAt, Value? rowid}) { @@ -3326,6 +3366,7 @@ class GroupsCompanion extends UpdateCompanion { isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, createdAt: createdAt ?? this.createdAt, rowid: rowid ?? this.rowid, @@ -3347,6 +3388,9 @@ class GroupsCompanion extends UpdateCompanion { if (pinned.present) { map['pinned'] = Variable(pinned.value); } + if (archived.present) { + map['archived'] = Variable(archived.value); + } if (lastMessageExchange.present) { map['last_message_exchange'] = Variable(lastMessageExchange.value); @@ -3367,6 +3411,7 @@ class GroupsCompanion extends UpdateCompanion { ..write('isGroupAdmin: $isGroupAdmin, ') ..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('pinned: $pinned, ') + ..write('archived: $archived, ') ..write('lastMessageExchange: $lastMessageExchange, ') ..write('createdAt: $createdAt, ') ..write('rowid: $rowid') @@ -3707,6 +3752,12 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("contact_will_sends_receipt" IN (0, 1))'), defaultValue: const Constant(true)); + static const VerificationMeta _ackByServerAtMeta = + const VerificationMeta('ackByServerAt'); + @override + late final GeneratedColumn ackByServerAt = + GeneratedColumn('ack_by_server_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _retryCountMeta = const VerificationMeta('retryCount'); @override @@ -3736,6 +3787,7 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { messageId, message, contactWillSendsReceipt, + ackByServerAt, retryCount, lastRetry, createdAt @@ -3777,6 +3829,12 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { data['contact_will_sends_receipt']!, _contactWillSendsReceiptMeta)); } + if (data.containsKey('ack_by_server_at')) { + context.handle( + _ackByServerAtMeta, + ackByServerAt.isAcceptableOrUnknown( + data['ack_by_server_at']!, _ackByServerAtMeta)); + } if (data.containsKey('retry_count')) { context.handle( _retryCountMeta, @@ -3811,6 +3869,8 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { contactWillSendsReceipt: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}contact_will_sends_receipt'])!, + ackByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']), retryCount: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, lastRetry: attachedDatabase.typeMapping @@ -3830,8 +3890,11 @@ class Receipt extends DataClass implements Insertable { final String receiptId; final int contactId; final String? messageId; + + /// This is the protobuf 'Message' final Uint8List message; final bool contactWillSendsReceipt; + final DateTime? ackByServerAt; final int retryCount; final DateTime? lastRetry; final DateTime createdAt; @@ -3841,6 +3904,7 @@ class Receipt extends DataClass implements Insertable { this.messageId, required this.message, required this.contactWillSendsReceipt, + this.ackByServerAt, required this.retryCount, this.lastRetry, required this.createdAt}); @@ -3854,6 +3918,9 @@ class Receipt extends DataClass implements Insertable { } map['message'] = Variable(message); map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt); + if (!nullToAbsent || ackByServerAt != null) { + map['ack_by_server_at'] = Variable(ackByServerAt); + } map['retry_count'] = Variable(retryCount); if (!nullToAbsent || lastRetry != null) { map['last_retry'] = Variable(lastRetry); @@ -3871,6 +3938,9 @@ class Receipt extends DataClass implements Insertable { : Value(messageId), message: Value(message), contactWillSendsReceipt: Value(contactWillSendsReceipt), + ackByServerAt: ackByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(ackByServerAt), retryCount: Value(retryCount), lastRetry: lastRetry == null && nullToAbsent ? const Value.absent() @@ -3889,6 +3959,7 @@ class Receipt extends DataClass implements Insertable { message: serializer.fromJson(json['message']), contactWillSendsReceipt: serializer.fromJson(json['contactWillSendsReceipt']), + ackByServerAt: serializer.fromJson(json['ackByServerAt']), retryCount: serializer.fromJson(json['retryCount']), lastRetry: serializer.fromJson(json['lastRetry']), createdAt: serializer.fromJson(json['createdAt']), @@ -3904,6 +3975,7 @@ class Receipt extends DataClass implements Insertable { 'message': serializer.toJson(message), 'contactWillSendsReceipt': serializer.toJson(contactWillSendsReceipt), + 'ackByServerAt': serializer.toJson(ackByServerAt), 'retryCount': serializer.toJson(retryCount), 'lastRetry': serializer.toJson(lastRetry), 'createdAt': serializer.toJson(createdAt), @@ -3916,6 +3988,7 @@ class Receipt extends DataClass implements Insertable { Value messageId = const Value.absent(), Uint8List? message, bool? contactWillSendsReceipt, + Value ackByServerAt = const Value.absent(), int? retryCount, Value lastRetry = const Value.absent(), DateTime? createdAt}) => @@ -3926,6 +3999,8 @@ class Receipt extends DataClass implements Insertable { message: message ?? this.message, contactWillSendsReceipt: contactWillSendsReceipt ?? this.contactWillSendsReceipt, + ackByServerAt: + ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt, retryCount: retryCount ?? this.retryCount, lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, createdAt: createdAt ?? this.createdAt, @@ -3939,6 +4014,9 @@ class Receipt extends DataClass implements Insertable { contactWillSendsReceipt: data.contactWillSendsReceipt.present ? data.contactWillSendsReceipt.value : this.contactWillSendsReceipt, + ackByServerAt: data.ackByServerAt.present + ? data.ackByServerAt.value + : this.ackByServerAt, retryCount: data.retryCount.present ? data.retryCount.value : this.retryCount, lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, @@ -3954,6 +4032,7 @@ class Receipt extends DataClass implements Insertable { ..write('messageId: $messageId, ') ..write('message: $message, ') ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('ackByServerAt: $ackByServerAt, ') ..write('retryCount: $retryCount, ') ..write('lastRetry: $lastRetry, ') ..write('createdAt: $createdAt') @@ -3968,6 +4047,7 @@ class Receipt extends DataClass implements Insertable { messageId, $driftBlobEquality.hash(message), contactWillSendsReceipt, + ackByServerAt, retryCount, lastRetry, createdAt); @@ -3980,6 +4060,7 @@ class Receipt extends DataClass implements Insertable { other.messageId == this.messageId && $driftBlobEquality.equals(other.message, this.message) && other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.ackByServerAt == this.ackByServerAt && other.retryCount == this.retryCount && other.lastRetry == this.lastRetry && other.createdAt == this.createdAt); @@ -3991,6 +4072,7 @@ class ReceiptsCompanion extends UpdateCompanion { final Value messageId; final Value message; final Value contactWillSendsReceipt; + final Value ackByServerAt; final Value retryCount; final Value lastRetry; final Value createdAt; @@ -4001,6 +4083,7 @@ class ReceiptsCompanion extends UpdateCompanion { this.messageId = const Value.absent(), this.message = const Value.absent(), this.contactWillSendsReceipt = const Value.absent(), + this.ackByServerAt = const Value.absent(), this.retryCount = const Value.absent(), this.lastRetry = const Value.absent(), this.createdAt = const Value.absent(), @@ -4012,6 +4095,7 @@ class ReceiptsCompanion extends UpdateCompanion { this.messageId = const Value.absent(), required Uint8List message, this.contactWillSendsReceipt = const Value.absent(), + this.ackByServerAt = const Value.absent(), this.retryCount = const Value.absent(), this.lastRetry = const Value.absent(), this.createdAt = const Value.absent(), @@ -4024,6 +4108,7 @@ class ReceiptsCompanion extends UpdateCompanion { Expression? messageId, Expression? message, Expression? contactWillSendsReceipt, + Expression? ackByServerAt, Expression? retryCount, Expression? lastRetry, Expression? createdAt, @@ -4036,6 +4121,7 @@ class ReceiptsCompanion extends UpdateCompanion { if (message != null) 'message': message, if (contactWillSendsReceipt != null) 'contact_will_sends_receipt': contactWillSendsReceipt, + if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, if (retryCount != null) 'retry_count': retryCount, if (lastRetry != null) 'last_retry': lastRetry, if (createdAt != null) 'created_at': createdAt, @@ -4049,6 +4135,7 @@ class ReceiptsCompanion extends UpdateCompanion { Value? messageId, Value? message, Value? contactWillSendsReceipt, + Value? ackByServerAt, Value? retryCount, Value? lastRetry, Value? createdAt, @@ -4060,6 +4147,7 @@ class ReceiptsCompanion extends UpdateCompanion { message: message ?? this.message, contactWillSendsReceipt: contactWillSendsReceipt ?? this.contactWillSendsReceipt, + ackByServerAt: ackByServerAt ?? this.ackByServerAt, retryCount: retryCount ?? this.retryCount, lastRetry: lastRetry ?? this.lastRetry, createdAt: createdAt ?? this.createdAt, @@ -4086,6 +4174,9 @@ class ReceiptsCompanion extends UpdateCompanion { map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt.value); } + if (ackByServerAt.present) { + map['ack_by_server_at'] = Variable(ackByServerAt.value); + } if (retryCount.present) { map['retry_count'] = Variable(retryCount.value); } @@ -4109,6 +4200,7 @@ class ReceiptsCompanion extends UpdateCompanion { ..write('messageId: $messageId, ') ..write('message: $message, ') ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('ackByServerAt: $ackByServerAt, ') ..write('retryCount: $retryCount, ') ..write('lastRetry: $lastRetry, ') ..write('createdAt: $createdAt, ') @@ -5744,6 +5836,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB); late final GroupsDao groupsDao = GroupsDao(this as TwonlyDB); late final ReactionsDao reactionsDao = ReactionsDao(this as TwonlyDB); + late final MediaFilesDao mediaFilesDao = MediaFilesDao(this as TwonlyDB); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -5833,7 +5926,6 @@ typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ Value hidden, Value blocked, Value verified, - Value archived, Value deleted, Value alsoBestFriend, Value deleteMessagesAfterXMinutes, @@ -5857,7 +5949,6 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ Value hidden, Value blocked, Value verified, - Value archived, Value deleted, Value alsoBestFriend, Value deleteMessagesAfterXMinutes, @@ -6019,9 +6110,6 @@ class $$ContactsTableFilterComposer ColumnFilters get verified => $composableBuilder( column: $table.verified, builder: (column) => ColumnFilters(column)); - ColumnFilters get archived => $composableBuilder( - column: $table.archived, builder: (column) => ColumnFilters(column)); - ColumnFilters get deleted => $composableBuilder( column: $table.deleted, builder: (column) => ColumnFilters(column)); @@ -6232,9 +6320,6 @@ class $$ContactsTableOrderingComposer ColumnOrderings get verified => $composableBuilder( column: $table.verified, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get archived => $composableBuilder( - column: $table.archived, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get deleted => $composableBuilder( column: $table.deleted, builder: (column) => ColumnOrderings(column)); @@ -6316,9 +6401,6 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get verified => $composableBuilder(column: $table.verified, builder: (column) => column); - GeneratedColumn get archived => - $composableBuilder(column: $table.archived, builder: (column) => column); - GeneratedColumn get deleted => $composableBuilder(column: $table.deleted, builder: (column) => column); @@ -6521,7 +6603,6 @@ class $$ContactsTableTableManager extends RootTableManager< Value hidden = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), - Value archived = const Value.absent(), Value deleted = const Value.absent(), Value alsoBestFriend = const Value.absent(), Value deleteMessagesAfterXMinutes = const Value.absent(), @@ -6545,7 +6626,6 @@ class $$ContactsTableTableManager extends RootTableManager< hidden: hidden, blocked: blocked, verified: verified, - archived: archived, deleted: deleted, alsoBestFriend: alsoBestFriend, deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, @@ -6569,7 +6649,6 @@ class $$ContactsTableTableManager extends RootTableManager< Value hidden = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), - Value archived = const Value.absent(), Value deleted = const Value.absent(), Value alsoBestFriend = const Value.absent(), Value deleteMessagesAfterXMinutes = const Value.absent(), @@ -6593,7 +6672,6 @@ class $$ContactsTableTableManager extends RootTableManager< hidden: hidden, blocked: blocked, verified: verified, - archived: archived, deleted: deleted, alsoBestFriend: alsoBestFriend, deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, @@ -6739,6 +6817,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ required bool requiresAuthentication, Value reopenByContact, Value storedByContact, + Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, Value encryptionKey, @@ -6755,6 +6834,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value requiresAuthentication, Value reopenByContact, Value storedByContact, + Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, Value encryptionKey, @@ -6823,6 +6903,11 @@ class $$MediaFilesTableFilterComposer column: $table.storedByContact, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters?, List, String> + get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get displayLimitInMilliseconds => $composableBuilder( column: $table.displayLimitInMilliseconds, builder: (column) => ColumnFilters(column)); @@ -6899,6 +6984,10 @@ class $$MediaFilesTableOrderingComposer column: $table.storedByContact, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get displayLimitInMilliseconds => $composableBuilder( column: $table.displayLimitInMilliseconds, builder: (column) => ColumnOrderings(column)); @@ -6955,6 +7044,10 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get storedByContact => $composableBuilder( column: $table.storedByContact, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> + get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, builder: (column) => column); + GeneratedColumn get displayLimitInMilliseconds => $composableBuilder( column: $table.displayLimitInMilliseconds, builder: (column) => column); @@ -7025,6 +7118,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value storedByContact = const Value.absent(), + Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), @@ -7041,6 +7135,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, storedByContact: storedByContact, + reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, encryptionKey: encryptionKey, @@ -7057,6 +7152,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< required bool requiresAuthentication, Value reopenByContact = const Value.absent(), Value storedByContact = const Value.absent(), + Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), @@ -7073,6 +7169,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, storedByContact: storedByContact, + reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, encryptionKey: encryptionKey, @@ -7128,15 +7225,15 @@ typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< PrefetchHooks Function({bool messagesRefs})>; typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String groupId, - required String messageId, + Value messageId, Value senderId, Value content, Value mediaId, Value quotesMessageId, Value isDeletedFromSender, Value isEdited, - Value acknowledgeByUser, - Value acknowledgeByServer, + Value ackByUser, + Value ackByServer, Value openedByCounter, Value openedAt, Value createdAt, @@ -7152,8 +7249,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value quotesMessageId, Value isDeletedFromSender, Value isEdited, - Value acknowledgeByUser, - Value acknowledgeByServer, + Value ackByUser, + Value ackByServer, Value openedByCounter, Value openedAt, Value createdAt, @@ -7286,13 +7383,11 @@ class $$MessagesTableFilterComposer ColumnFilters get isEdited => $composableBuilder( column: $table.isEdited, builder: (column) => ColumnFilters(column)); - ColumnFilters get acknowledgeByUser => $composableBuilder( - column: $table.acknowledgeByUser, - builder: (column) => ColumnFilters(column)); + ColumnFilters get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnFilters(column)); - ColumnFilters get acknowledgeByServer => $composableBuilder( - column: $table.acknowledgeByServer, - builder: (column) => ColumnFilters(column)); + ColumnFilters get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnFilters(column)); ColumnFilters get openedByCounter => $composableBuilder( column: $table.openedByCounter, @@ -7456,13 +7551,11 @@ class $$MessagesTableOrderingComposer ColumnOrderings get isEdited => $composableBuilder( column: $table.isEdited, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get acknowledgeByUser => $composableBuilder( - column: $table.acknowledgeByUser, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get acknowledgeByServer => $composableBuilder( - column: $table.acknowledgeByServer, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnOrderings(column)); ColumnOrderings get openedByCounter => $composableBuilder( column: $table.openedByCounter, @@ -7562,11 +7655,11 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get isEdited => $composableBuilder(column: $table.isEdited, builder: (column) => column); - GeneratedColumn get acknowledgeByUser => $composableBuilder( - column: $table.acknowledgeByUser, builder: (column) => column); + GeneratedColumn get ackByUser => + $composableBuilder(column: $table.ackByUser, builder: (column) => column); - GeneratedColumn get acknowledgeByServer => $composableBuilder( - column: $table.acknowledgeByServer, builder: (column) => column); + GeneratedColumn get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => column); GeneratedColumn get openedByCounter => $composableBuilder( column: $table.openedByCounter, builder: (column) => column); @@ -7741,8 +7834,8 @@ class $$MessagesTableTableManager extends RootTableManager< Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), - Value acknowledgeByUser = const Value.absent(), - Value acknowledgeByServer = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), Value openedByCounter = const Value.absent(), Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), @@ -7758,8 +7851,8 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, - acknowledgeByUser: acknowledgeByUser, - acknowledgeByServer: acknowledgeByServer, + ackByUser: ackByUser, + ackByServer: ackByServer, openedByCounter: openedByCounter, openedAt: openedAt, createdAt: createdAt, @@ -7768,15 +7861,15 @@ class $$MessagesTableTableManager extends RootTableManager< ), createCompanionCallback: ({ required String groupId, - required String messageId, + Value messageId = const Value.absent(), Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), - Value acknowledgeByUser = const Value.absent(), - Value acknowledgeByServer = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), Value openedByCounter = const Value.absent(), Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), @@ -7792,8 +7885,8 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, - acknowledgeByUser: acknowledgeByUser, - acknowledgeByServer: acknowledgeByServer, + ackByUser: ackByUser, + ackByServer: ackByServer, openedByCounter: openedByCounter, openedAt: openedAt, createdAt: createdAt, @@ -8518,6 +8611,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required bool isGroupAdmin, required bool isGroupOfTwo, Value pinned, + Value archived, Value lastMessageExchange, Value createdAt, Value rowid, @@ -8527,6 +8621,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value isGroupAdmin, Value isGroupOfTwo, Value pinned, + Value archived, Value lastMessageExchange, Value createdAt, Value rowid, @@ -8552,6 +8647,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get pinned => $composableBuilder( column: $table.pinned, builder: (column) => ColumnFilters(column)); + ColumnFilters get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnFilters(column)); @@ -8582,6 +8680,9 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get pinned => $composableBuilder( column: $table.pinned, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnOrderings(column)); @@ -8611,6 +8712,9 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); + GeneratedColumn get archived => + $composableBuilder(column: $table.archived, builder: (column) => column); + GeneratedColumn get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => column); @@ -8645,6 +8749,7 @@ class $$GroupsTableTableManager extends RootTableManager< Value isGroupAdmin = const Value.absent(), Value isGroupOfTwo = const Value.absent(), Value pinned = const Value.absent(), + Value archived = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), @@ -8654,6 +8759,7 @@ class $$GroupsTableTableManager extends RootTableManager< isGroupAdmin: isGroupAdmin, isGroupOfTwo: isGroupOfTwo, pinned: pinned, + archived: archived, lastMessageExchange: lastMessageExchange, createdAt: createdAt, rowid: rowid, @@ -8663,6 +8769,7 @@ class $$GroupsTableTableManager extends RootTableManager< required bool isGroupAdmin, required bool isGroupOfTwo, Value pinned = const Value.absent(), + Value archived = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), @@ -8672,6 +8779,7 @@ class $$GroupsTableTableManager extends RootTableManager< isGroupAdmin: isGroupAdmin, isGroupOfTwo: isGroupOfTwo, pinned: pinned, + archived: archived, lastMessageExchange: lastMessageExchange, createdAt: createdAt, rowid: rowid, @@ -8965,6 +9073,7 @@ typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({ Value messageId, required Uint8List message, Value contactWillSendsReceipt, + Value ackByServerAt, Value retryCount, Value lastRetry, Value createdAt, @@ -8976,6 +9085,7 @@ typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({ Value messageId, Value message, Value contactWillSendsReceipt, + Value ackByServerAt, Value retryCount, Value lastRetry, Value createdAt, @@ -9036,6 +9146,9 @@ class $$ReceiptsTableFilterComposer column: $table.contactWillSendsReceipt, builder: (column) => ColumnFilters(column)); + ColumnFilters get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get retryCount => $composableBuilder( column: $table.retryCount, builder: (column) => ColumnFilters(column)); @@ -9105,6 +9218,10 @@ class $$ReceiptsTableOrderingComposer column: $table.contactWillSendsReceipt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get retryCount => $composableBuilder( column: $table.retryCount, builder: (column) => ColumnOrderings(column)); @@ -9173,6 +9290,9 @@ class $$ReceiptsTableAnnotationComposer GeneratedColumn get contactWillSendsReceipt => $composableBuilder( column: $table.contactWillSendsReceipt, builder: (column) => column); + GeneratedColumn get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, builder: (column) => column); + GeneratedColumn get retryCount => $composableBuilder( column: $table.retryCount, builder: (column) => column); @@ -9251,6 +9371,7 @@ class $$ReceiptsTableTableManager extends RootTableManager< Value messageId = const Value.absent(), Value message = const Value.absent(), Value contactWillSendsReceipt = const Value.absent(), + Value ackByServerAt = const Value.absent(), Value retryCount = const Value.absent(), Value lastRetry = const Value.absent(), Value createdAt = const Value.absent(), @@ -9262,6 +9383,7 @@ class $$ReceiptsTableTableManager extends RootTableManager< messageId: messageId, message: message, contactWillSendsReceipt: contactWillSendsReceipt, + ackByServerAt: ackByServerAt, retryCount: retryCount, lastRetry: lastRetry, createdAt: createdAt, @@ -9273,6 +9395,7 @@ class $$ReceiptsTableTableManager extends RootTableManager< Value messageId = const Value.absent(), required Uint8List message, Value contactWillSendsReceipt = const Value.absent(), + Value ackByServerAt = const Value.absent(), Value retryCount = const Value.absent(), Value lastRetry = const Value.absent(), Value createdAt = const Value.absent(), @@ -9284,6 +9407,7 @@ class $$ReceiptsTableTableManager extends RootTableManager< messageId: messageId, message: message, contactWillSendsReceipt: contactWillSendsReceipt, + ackByServerAt: ackByServerAt, retryCount: retryCount, lastRetry: lastRetry, createdAt: createdAt, diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index bc9d4bd..2facb77 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -218,6 +218,7 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage { factory EncryptedContent_TextMessage({ $core.String? senderMessageId, $core.String? text, + $fixnum.Int64? timestamp, $core.String? quoteMessageId, }) { final $result = create(); @@ -227,6 +228,9 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage { if (text != null) { $result.text = text; } + if (timestamp != null) { + $result.timestamp = timestamp; + } if (quoteMessageId != null) { $result.quoteMessageId = quoteMessageId; } @@ -239,7 +243,8 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.TextMessage', createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') ..aOS(2, _omitFieldNames ? '' : 'text') - ..aOS(3, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') + ..aInt64(3, _omitFieldNames ? '' : 'timestamp') + ..aOS(4, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') ..hasRequiredFields = false ; @@ -283,13 +288,22 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage { void clearText() => clearField(2); @$pb.TagNumber(3) - $core.String get quoteMessageId => $_getSZ(2); + $fixnum.Int64 get timestamp => $_getI64(2); @$pb.TagNumber(3) - set quoteMessageId($core.String v) { $_setString(2, v); } + set timestamp($fixnum.Int64 v) { $_setInt64(2, v); } @$pb.TagNumber(3) - $core.bool hasQuoteMessageId() => $_has(2); + $core.bool hasTimestamp() => $_has(2); @$pb.TagNumber(3) - void clearQuoteMessageId() => clearField(3); + void clearTimestamp() => clearField(3); + + @$pb.TagNumber(4) + $core.String get quoteMessageId => $_getSZ(3); + @$pb.TagNumber(4) + set quoteMessageId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasQuoteMessageId() => $_has(3); + @$pb.TagNumber(4) + void clearQuoteMessageId() => clearField(4); } class EncryptedContent_Reaction extends $pb.GeneratedMessage { @@ -374,6 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { factory EncryptedContent_MessageUpdate({ EncryptedContent_MessageUpdate_Type? type, $core.String? senderMessageId, + $core.Iterable<$core.String>? multipleSenderMessageIds, $core.String? text, $fixnum.Int64? timestamp, }) { @@ -384,6 +399,9 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { if (senderMessageId != null) { $result.senderMessageId = senderMessageId; } + if (multipleSenderMessageIds != null) { + $result.multipleSenderMessageIds.addAll(multipleSenderMessageIds); + } if (text != null) { $result.text = text; } @@ -399,8 +417,9 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values) ..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') - ..aOS(3, _omitFieldNames ? '' : 'text') - ..aInt64(4, _omitFieldNames ? '' : 'timestamp') + ..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds') + ..aOS(4, _omitFieldNames ? '' : 'text') + ..aInt64(5, _omitFieldNames ? '' : 'timestamp') ..hasRequiredFields = false ; @@ -444,22 +463,25 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { void clearSenderMessageId() => clearField(2); @$pb.TagNumber(3) - $core.String get text => $_getSZ(2); - @$pb.TagNumber(3) - set text($core.String v) { $_setString(2, v); } - @$pb.TagNumber(3) - $core.bool hasText() => $_has(2); - @$pb.TagNumber(3) - void clearText() => clearField(3); + $core.List<$core.String> get multipleSenderMessageIds => $_getList(2); @$pb.TagNumber(4) - $fixnum.Int64 get timestamp => $_getI64(3); + $core.String get text => $_getSZ(3); @$pb.TagNumber(4) - set timestamp($fixnum.Int64 v) { $_setInt64(3, v); } + set text($core.String v) { $_setString(3, v); } @$pb.TagNumber(4) - $core.bool hasTimestamp() => $_has(3); + $core.bool hasText() => $_has(3); @$pb.TagNumber(4) - void clearTimestamp() => clearField(4); + void clearText() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get timestamp => $_getI64(4); + @$pb.TagNumber(5) + set timestamp($fixnum.Int64 v) { $_setInt64(4, v); } + @$pb.TagNumber(5) + $core.bool hasTimestamp() => $_has(4); + @$pb.TagNumber(5) + void clearTimestamp() => clearField(5); } class EncryptedContent_Media extends $pb.GeneratedMessage { @@ -468,6 +490,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { EncryptedContent_Media_Type? type, $fixnum.Int64? displayLimitInMilliseconds, $core.bool? requiresAuthentication, + $fixnum.Int64? timestamp, + $core.String? quoteMessageId, $core.List<$core.int>? downloadToken, $core.List<$core.int>? encryptionKey, $core.List<$core.int>? encryptionMac, @@ -486,6 +510,12 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { if (requiresAuthentication != null) { $result.requiresAuthentication = requiresAuthentication; } + if (timestamp != null) { + $result.timestamp = timestamp; + } + if (quoteMessageId != null) { + $result.quoteMessageId = quoteMessageId; + } if (downloadToken != null) { $result.downloadToken = downloadToken; } @@ -506,13 +536,15 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Media', createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') - ..e(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.IMAGE, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values) + ..e(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.REUPLOAD, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values) ..aInt64(3, _omitFieldNames ? '' : 'displayLimitInMilliseconds', protoName: 'displayLimitInMilliseconds') ..aOB(4, _omitFieldNames ? '' : 'requiresAuthentication', protoName: 'requiresAuthentication') - ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken') - ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey') - ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac') - ..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce') + ..aInt64(5, _omitFieldNames ? '' : 'timestamp') + ..aOS(6, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken') + ..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey') + ..a<$core.List<$core.int>>(9, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac') + ..a<$core.List<$core.int>>(10, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce') ..hasRequiredFields = false ; @@ -574,40 +606,58 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { void clearRequiresAuthentication() => clearField(4); @$pb.TagNumber(5) - $core.List<$core.int> get downloadToken => $_getN(4); + $fixnum.Int64 get timestamp => $_getI64(4); @$pb.TagNumber(5) - set downloadToken($core.List<$core.int> v) { $_setBytes(4, v); } + set timestamp($fixnum.Int64 v) { $_setInt64(4, v); } @$pb.TagNumber(5) - $core.bool hasDownloadToken() => $_has(4); + $core.bool hasTimestamp() => $_has(4); @$pb.TagNumber(5) - void clearDownloadToken() => clearField(5); + void clearTimestamp() => clearField(5); @$pb.TagNumber(6) - $core.List<$core.int> get encryptionKey => $_getN(5); + $core.String get quoteMessageId => $_getSZ(5); @$pb.TagNumber(6) - set encryptionKey($core.List<$core.int> v) { $_setBytes(5, v); } + set quoteMessageId($core.String v) { $_setString(5, v); } @$pb.TagNumber(6) - $core.bool hasEncryptionKey() => $_has(5); + $core.bool hasQuoteMessageId() => $_has(5); @$pb.TagNumber(6) - void clearEncryptionKey() => clearField(6); + void clearQuoteMessageId() => clearField(6); @$pb.TagNumber(7) - $core.List<$core.int> get encryptionMac => $_getN(6); + $core.List<$core.int> get downloadToken => $_getN(6); @$pb.TagNumber(7) - set encryptionMac($core.List<$core.int> v) { $_setBytes(6, v); } + set downloadToken($core.List<$core.int> v) { $_setBytes(6, v); } @$pb.TagNumber(7) - $core.bool hasEncryptionMac() => $_has(6); + $core.bool hasDownloadToken() => $_has(6); @$pb.TagNumber(7) - void clearEncryptionMac() => clearField(7); + void clearDownloadToken() => clearField(7); @$pb.TagNumber(8) - $core.List<$core.int> get encryptionNonce => $_getN(7); + $core.List<$core.int> get encryptionKey => $_getN(7); @$pb.TagNumber(8) - set encryptionNonce($core.List<$core.int> v) { $_setBytes(7, v); } + set encryptionKey($core.List<$core.int> v) { $_setBytes(7, v); } @$pb.TagNumber(8) - $core.bool hasEncryptionNonce() => $_has(7); + $core.bool hasEncryptionKey() => $_has(7); @$pb.TagNumber(8) - void clearEncryptionNonce() => clearField(8); + void clearEncryptionKey() => clearField(8); + + @$pb.TagNumber(9) + $core.List<$core.int> get encryptionMac => $_getN(8); + @$pb.TagNumber(9) + set encryptionMac($core.List<$core.int> v) { $_setBytes(8, v); } + @$pb.TagNumber(9) + $core.bool hasEncryptionMac() => $_has(8); + @$pb.TagNumber(9) + void clearEncryptionMac() => clearField(9); + + @$pb.TagNumber(10) + $core.List<$core.int> get encryptionNonce => $_getN(9); + @$pb.TagNumber(10) + set encryptionNonce($core.List<$core.int> v) { $_setBytes(9, v); } + @$pb.TagNumber(10) + $core.bool hasEncryptionNonce() => $_has(9); + @$pb.TagNumber(10) + void clearEncryptionNonce() => clearField(10); } class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { @@ -807,6 +857,7 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage { EncryptedContent_PushKeys_Type? type, $fixnum.Int64? keyId, $core.List<$core.int>? key, + $fixnum.Int64? createdAt, }) { final $result = create(); if (type != null) { @@ -818,6 +869,9 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage { if (key != null) { $result.key = key; } + if (createdAt != null) { + $result.createdAt = createdAt; + } return $result; } EncryptedContent_PushKeys._() : super(); @@ -828,6 +882,7 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage { ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_PushKeys_Type.REQUEST, valueOf: EncryptedContent_PushKeys_Type.valueOf, enumValues: EncryptedContent_PushKeys_Type.values) ..aInt64(2, _omitFieldNames ? '' : 'keyId', protoName: 'keyId') ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY) + ..aInt64(4, _omitFieldNames ? '' : 'createdAt', protoName: 'createdAt') ..hasRequiredFields = false ; @@ -878,6 +933,15 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage { $core.bool hasKey() => $_has(2); @$pb.TagNumber(3) void clearKey() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get createdAt => $_getI64(3); + @$pb.TagNumber(4) + set createdAt($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasCreatedAt() => $_has(3); + @$pb.TagNumber(4) + void clearCreatedAt() => clearField(4); } class EncryptedContent_FlameSync extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart index 37118a9..4651437 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -18,12 +18,14 @@ class Message_Type extends $pb.ProtobufEnum { static const Message_Type PLAINTEXT_CONTENT = Message_Type._(1, _omitEnumNames ? '' : 'PLAINTEXT_CONTENT'); static const Message_Type CIPHERTEXT = Message_Type._(2, _omitEnumNames ? '' : 'CIPHERTEXT'); static const Message_Type PREKEY_BUNDLE = Message_Type._(3, _omitEnumNames ? '' : 'PREKEY_BUNDLE'); + static const Message_Type TEST_NOTIFICATION = Message_Type._(4, _omitEnumNames ? '' : 'TEST_NOTIFICATION'); static const $core.List values = [ SENDER_DELIVERY_RECEIPT, PLAINTEXT_CONTENT, CIPHERTEXT, PREKEY_BUNDLE, + TEST_NOTIFICATION, ]; static final $core.Map<$core.int, Message_Type> _byValue = $pb.ProtobufEnum.initByValue(values); @@ -65,11 +67,13 @@ class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum { } class EncryptedContent_Media_Type extends $pb.ProtobufEnum { - static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'IMAGE'); - static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'VIDEO'); - static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'GIF'); + static const EncryptedContent_Media_Type REUPLOAD = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'REUPLOAD'); + static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE'); + static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO'); + static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(3, _omitEnumNames ? '' : 'GIF'); static const $core.List values = [ + REUPLOAD, IMAGE, VIDEO, GIF, diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 9da2b0c..ef30eb5 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -37,6 +37,7 @@ const Message_Type$json = { {'1': 'PLAINTEXT_CONTENT', '2': 1}, {'1': 'CIPHERTEXT', '2': 2}, {'1': 'PREKEY_BUNDLE', '2': 3}, + {'1': 'TEST_NOTIFICATION', '2': 4}, ], }; @@ -45,9 +46,10 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( 'CgdNZXNzYWdlEiEKBHR5cGUYASABKA4yDS5NZXNzYWdlLlR5cGVSBHR5cGUSHAoJcmVjZWlwdE' 'lkGAIgASgJUglyZWNlaXB0SWQSLwoQZW5jcnlwdGVkQ29udGVudBgDIAEoDEgAUhBlbmNyeXB0' 'ZWRDb250ZW50iAEBEkIKEHBsYWludGV4dENvbnRlbnQYBCABKAsyES5QbGFpbnRleHRDb250ZW' - '50SAFSEHBsYWludGV4dENvbnRlbnSIAQEiXQoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD' + '50SAFSEHBsYWludGV4dENvbnRlbnSIAQEidAoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD' 'RUlQVBAAEhUKEVBMQUlOVEVYVF9DT05URU5UEAESDgoKQ0lQSEVSVEVYVBACEhEKDVBSRUtFWV' - '9CVU5ETEUQA0ITChFfZW5jcnlwdGVkQ29udGVudEITChFfcGxhaW50ZXh0Q29udGVudA=='); + '9CVU5ETEUQAxIVChFURVNUX05PVElGSUNBVElPThAEQhMKEV9lbmNyeXB0ZWRDb250ZW50QhMK' + 'EV9wbGFpbnRleHRDb250ZW50'); @$core.Deprecated('Use plaintextContentDescriptor instead') const PlaintextContent$json = { @@ -126,7 +128,8 @@ const EncryptedContent_TextMessage$json = { '2': [ {'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'}, - {'1': 'quoteMessageId', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true}, + {'1': 'timestamp', '3': 3, '4': 1, '5': 3, '10': 'timestamp'}, + {'1': 'quoteMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true}, ], '8': [ {'1': '_quoteMessageId'}, @@ -152,14 +155,15 @@ const EncryptedContent_MessageUpdate$json = { '1': 'MessageUpdate', '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'}, - {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '10': 'senderMessageId'}, - {'1': 'text', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'text', '17': true}, - {'1': 'timestamp', '3': 4, '4': 1, '5': 3, '9': 1, '10': 'timestamp', '17': true}, + {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true}, + {'1': 'multipleSenderMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleSenderMessageIds'}, + {'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true}, + {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, ], '4': [EncryptedContent_MessageUpdate_Type$json], '8': [ + {'1': '_senderMessageId'}, {'1': '_text'}, - {'1': '_timestamp'}, ], }; @@ -181,14 +185,17 @@ const EncryptedContent_Media$json = { {'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.EncryptedContent.Media.Type', '10': 'type'}, {'1': 'displayLimitInMilliseconds', '3': 3, '4': 1, '5': 3, '9': 0, '10': 'displayLimitInMilliseconds', '17': true}, {'1': 'requiresAuthentication', '3': 4, '4': 1, '5': 8, '10': 'requiresAuthentication'}, - {'1': 'downloadToken', '3': 5, '4': 1, '5': 12, '9': 1, '10': 'downloadToken', '17': true}, - {'1': 'encryptionKey', '3': 6, '4': 1, '5': 12, '9': 2, '10': 'encryptionKey', '17': true}, - {'1': 'encryptionMac', '3': 7, '4': 1, '5': 12, '9': 3, '10': 'encryptionMac', '17': true}, - {'1': 'encryptionNonce', '3': 8, '4': 1, '5': 12, '9': 4, '10': 'encryptionNonce', '17': true}, + {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, + {'1': 'quoteMessageId', '3': 6, '4': 1, '5': 9, '9': 1, '10': 'quoteMessageId', '17': true}, + {'1': 'downloadToken', '3': 7, '4': 1, '5': 12, '9': 2, '10': 'downloadToken', '17': true}, + {'1': 'encryptionKey', '3': 8, '4': 1, '5': 12, '9': 3, '10': 'encryptionKey', '17': true}, + {'1': 'encryptionMac', '3': 9, '4': 1, '5': 12, '9': 4, '10': 'encryptionMac', '17': true}, + {'1': 'encryptionNonce', '3': 10, '4': 1, '5': 12, '9': 5, '10': 'encryptionNonce', '17': true}, ], '4': [EncryptedContent_Media_Type$json], '8': [ {'1': '_displayLimitInMilliseconds'}, + {'1': '_quoteMessageId'}, {'1': '_downloadToken'}, {'1': '_encryptionKey'}, {'1': '_encryptionMac'}, @@ -200,9 +207,10 @@ const EncryptedContent_Media$json = { const EncryptedContent_Media_Type$json = { '1': 'Type', '2': [ - {'1': 'IMAGE', '2': 0}, - {'1': 'VIDEO', '2': 1}, - {'1': 'GIF', '2': 2}, + {'1': 'REUPLOAD', '2': 0}, + {'1': 'IMAGE', '2': 1}, + {'1': 'VIDEO', '2': 2}, + {'1': 'GIF', '2': 3}, ], }; @@ -276,11 +284,13 @@ const EncryptedContent_PushKeys$json = { {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.PushKeys.Type', '10': 'type'}, {'1': 'keyId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true}, {'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true}, + {'1': 'createdAt', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'createdAt', '17': true}, ], '4': [EncryptedContent_PushKeys_Type$json], '8': [ {'1': '_keyId'}, {'1': '_key'}, + {'1': '_createdAt'}, ], }; @@ -317,42 +327,47 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy' 'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA' 'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u' - 'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqLAQoLVG' + 'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG' 'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' - 'dGV4dBgCIAEoCVIEdGV4dBIrCg5xdW90ZU1lc3NhZ2VJZBgDIAEoCUgAUg5xdW90ZU1lc3NhZ2' - 'VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQagQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJ' - 'ZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhkKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBn' - 'JlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFCCAoGX2Vtb2ppQgkKB19yZW1vdmUa9QEKDU1lc3Nh' - 'Z2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS' - '5UeXBlUgR0eXBlEigKD3NlbmRlck1lc3NhZ2VJZBgCIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhcK' - 'BHRleHQYAyABKAlIAFIEdGV4dIgBARIhCgl0aW1lc3RhbXAYBCABKANIAVIJdGltZXN0YW1wiA' - 'EBIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCBwoFX3Rl' - 'eHRCDAoKX3RpbWVzdGFtcBqgBAoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW' - '5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlw' - 'ZVIEdHlwZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TG' - 'ltaXRJbk1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZy' - 'ZXF1aXJlc0F1dGhlbnRpY2F0aW9uEikKDWRvd25sb2FkVG9rZW4YBSABKAxIAVINZG93bmxvYW' - 'RUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAYgASgMSAJSDWVuY3J5cHRpb25LZXmIAQESKQoN' - 'ZW5jcnlwdGlvbk1hYxgHIAEoDEgDUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb25Ob2' - '5jZRgIIAEoDEgEUg9lbmNyeXB0aW9uTm9uY2WIAQEiJQoEVHlwZRIJCgVJTUFHRRAAEgkKBVZJ' - 'REVPEAESBwoDR0lGEAJCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhAKDl9kb3dubG' - '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0' - 'aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW' - '50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJn' - 'ZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUF' - 'RJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVk' - 'Q29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEg' - 'oKBlJFSkVDVBABEgoKBkFDQ0VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4y' - 'JC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdm' - 'cYAiABKAlIAFIJYXZhdGFyU3ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlO' - 'YW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDg' - 'oMX2Rpc3BsYXlOYW1lGqQBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29u' - 'dGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2' - 'tleRgDIAEoDEgBUgNrZXmIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoG' - 'X2tleUlkQgYKBF9rZXkahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW' - '1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3Vu' - 'dGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCFw' - 'oVX3NlbmRlclByb2ZpbGVDb3VudGVyQg4KDF90ZXh0TWVzc2FnZUIQCg5fbWVzc2FnZVVwZGF0' - 'ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YW' - 'N0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb24='); + 'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU' + '1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa' + 'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh' + 'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC' + 'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' + 'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh' + 'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW' + 'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0' + 'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA' + 'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4' + 'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA' + 'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw' + 'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2' + '5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp' + 'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA' + 'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93' + 'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ' + 'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp' + 'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB' + 'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs' + 'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX' + 'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh' + 'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG' + 'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw' + 'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2' + '50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx' + 'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0' + 'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50' + 'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3' + 'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH' + 'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg' + 'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl' + 'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ' + 'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ' + 'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV' + 'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291' + 'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA' + 'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO' + 'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG' + 'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL' + 'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u'); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pb.dart b/lib/src/model/protobuf/client/generated/push_notification.pb.dart index c0e8a0d..eb929fd 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pb.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pb.dart @@ -113,7 +113,7 @@ class EncryptedPushNotification extends $pb.GeneratedMessage { class PushNotification extends $pb.GeneratedMessage { factory PushNotification({ PushKind? kind, - $fixnum.Int64? messageId, + $core.String? messageId, $core.String? reactionContent, }) { final $result = create(); @@ -134,7 +134,7 @@ class PushNotification extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushNotification', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: PushKind.reaction, valueOf: PushKind.valueOf, enumValues: PushKind.values) - ..aInt64(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') + ..aOS(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') ..aOS(3, _omitFieldNames ? '' : 'reactionContent', protoName: 'reactionContent') ..hasRequiredFields = false ; @@ -170,9 +170,9 @@ class PushNotification extends $pb.GeneratedMessage { void clearKind() => clearField(1); @$pb.TagNumber(2) - $fixnum.Int64 get messageId => $_getI64(1); + $core.String get messageId => $_getSZ(1); @$pb.TagNumber(2) - set messageId($fixnum.Int64 v) { $_setInt64(1, v); } + set messageId($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasMessageId() => $_has(1); @$pb.TagNumber(2) @@ -237,7 +237,7 @@ class PushUser extends $pb.GeneratedMessage { $fixnum.Int64? userId, $core.String? displayName, $core.bool? blocked, - $fixnum.Int64? lastMessageId, + $core.String? lastMessageId, $core.Iterable? pushKeys, }) { final $result = create(); @@ -266,7 +266,7 @@ class PushUser extends $pb.GeneratedMessage { ..aInt64(1, _omitFieldNames ? '' : 'userId', protoName: 'userId') ..aOS(2, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') ..aOB(3, _omitFieldNames ? '' : 'blocked') - ..aInt64(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId') + ..aOS(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId') ..pc(5, _omitFieldNames ? '' : 'pushKeys', $pb.PbFieldType.PM, protoName: 'pushKeys', subBuilder: PushKey.create) ..hasRequiredFields = false ; @@ -320,9 +320,9 @@ class PushUser extends $pb.GeneratedMessage { void clearBlocked() => clearField(3); @$pb.TagNumber(4) - $fixnum.Int64 get lastMessageId => $_getI64(3); + $core.String get lastMessageId => $_getSZ(3); @$pb.TagNumber(4) - set lastMessageId($fixnum.Int64 v) { $_setInt64(3, v); } + set lastMessageId($core.String v) { $_setString(3, v); } @$pb.TagNumber(4) $core.bool hasLastMessageId() => $_has(3); @$pb.TagNumber(4) diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart index d66d5aa..8ff6dc0 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart @@ -64,7 +64,7 @@ const PushNotification$json = { '1': 'PushNotification', '2': [ {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.PushKind', '10': 'kind'}, - {'1': 'messageId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'messageId', '17': true}, + {'1': 'messageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'messageId', '17': true}, {'1': 'reactionContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'reactionContent', '17': true}, ], '8': [ @@ -76,7 +76,7 @@ const PushNotification$json = { /// Descriptor for `PushNotification`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List pushNotificationDescriptor = $convert.base64Decode( 'ChBQdXNoTm90aWZpY2F0aW9uEh0KBGtpbmQYASABKA4yCS5QdXNoS2luZFIEa2luZBIhCgltZX' - 'NzYWdlSWQYAiABKANIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB' + 'NzYWdlSWQYAiABKAlIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB' 'Ug9yZWFjdGlvbkNvbnRlbnSIAQFCDAoKX21lc3NhZ2VJZEISChBfcmVhY3Rpb25Db250ZW50'); @$core.Deprecated('Use pushUsersDescriptor instead') @@ -98,7 +98,7 @@ const PushUser$json = { {'1': 'userId', '3': 1, '4': 1, '5': 3, '10': 'userId'}, {'1': 'displayName', '3': 2, '4': 1, '5': 9, '10': 'displayName'}, {'1': 'blocked', '3': 3, '4': 1, '5': 8, '10': 'blocked'}, - {'1': 'lastMessageId', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'lastMessageId', '17': true}, + {'1': 'lastMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'lastMessageId', '17': true}, {'1': 'pushKeys', '3': 5, '4': 3, '5': 11, '6': '.PushKey', '10': 'pushKeys'}, ], '8': [ @@ -110,7 +110,7 @@ const PushUser$json = { final $typed_data.Uint8List pushUserDescriptor = $convert.base64Decode( 'CghQdXNoVXNlchIWCgZ1c2VySWQYASABKANSBnVzZXJJZBIgCgtkaXNwbGF5TmFtZRgCIAEoCV' 'ILZGlzcGxheU5hbWUSGAoHYmxvY2tlZBgDIAEoCFIHYmxvY2tlZBIpCg1sYXN0TWVzc2FnZUlk' - 'GAQgASgDSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug' + 'GAQgASgJSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug' 'hwdXNoS2V5c0IQCg5fbGFzdE1lc3NhZ2VJZA=='); @$core.Deprecated('Use pushKeyDescriptor instead') diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 39b3105..d8dc45d 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -6,6 +6,7 @@ message Message { PLAINTEXT_CONTENT = 1; CIPHERTEXT = 2; PREKEY_BUNDLE = 3; + TEST_NOTIFICATION = 4; } Type type = 1; string receiptId = 2; @@ -46,7 +47,8 @@ message EncryptedContent { message TextMessage { string senderMessageId = 1; string text = 2; - optional string quoteMessageId = 3; + int64 timestamp = 3; + optional string quoteMessageId = 4; } message Reaction { @@ -62,27 +64,31 @@ message EncryptedContent { OPENED = 2; } Type type = 1; - string senderMessageId = 2; - optional string text = 3; - optional int64 timestamp = 4; + optional string senderMessageId = 2; + repeated string multipleSenderMessageIds = 3; + optional string text = 4; + int64 timestamp = 5; } message Media { enum Type { - IMAGE = 0; - VIDEO = 1; - GIF = 2; + REUPLOAD = 0; + IMAGE = 1; + VIDEO = 2; + GIF = 3; } string senderMessageId = 1; Type type = 2; optional int64 displayLimitInMilliseconds = 3; bool requiresAuthentication = 4; + int64 timestamp = 5; + optional string quoteMessageId = 6; - optional bytes downloadToken = 5; - optional bytes encryptionKey = 6; - optional bytes encryptionMac = 7; - optional bytes encryptionNonce = 8; + optional bytes downloadToken = 7; + optional bytes encryptionKey = 8; + optional bytes encryptionMac = 9; + optional bytes encryptionNonce = 10; } message MediaUpdate { @@ -124,6 +130,7 @@ message EncryptedContent { Type type = 1; optional int64 keyId = 2; optional bytes key = 3; + optional int64 createdAt = 4; } message FlameSync { diff --git a/lib/src/model/protobuf/client/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto index 3c19946..0abeedb 100644 --- a/lib/src/model/protobuf/client/push_notification.proto +++ b/lib/src/model/protobuf/client/push_notification.proto @@ -26,7 +26,7 @@ enum PushKind { message PushNotification { PushKind kind = 1; - optional int64 messageId = 2; + optional string messageId = 2; optional string reactionContent = 3; } @@ -39,7 +39,7 @@ message PushUser { int64 userId = 1; string displayName = 2; bool blocked = 3; - optional int64 lastMessageId = 4; + optional string lastMessageId = 4; repeated PushKey pushKeys = 5; } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 8a54cff..07b2dcd 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -114,7 +114,7 @@ class ApiService { _channel = null; isAuthenticated = false; globalCallbackConnectionState(isConnected: false); - await twonlyDB.messagesDao.resetPendingDownloadState(); + await twonlyDB.mediaFilesDao.resetPendingDownloadState(); } Future startReconnectionTimer() async { diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart index 0c2708d..61887ed 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/media_download.dart @@ -119,7 +119,7 @@ Future handleDownloadStatusUpdateInternal( Mutex protectDownload = Mutex(); -Future startDownloadMedia(Message message, bool force) async { +Future startDownloadMedia(MediaFile media, bool force) async { Log.info( 'Download blocked for ${message.messageId} because of network state.', ); diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 08cbb6c..a76fcc3 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,163 +1,153 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; -import 'package:twonly/src/services/api/server_messages.dart' - show messageGetsAck; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; final lockRetransmission = Mutex(); Future tryTransmitMessages() async { return lockRetransmission.protect(() async { - final retransIds = - await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages(); + final receipts = await twonlyDB.receiptsDao.getReceiptsNotAckByServer(); - Log.info('Retransmitting ${retransIds.length} text messages'); + if (receipts.isEmpty) return; - if (retransIds.isEmpty) return; + Log.info('Reuploading ${receipts.length} messages to the server.'); - for (final retransId in retransIds) { - await sendRetransmitMessage(retransId); + for (final receipt in receipts) { + await tryToSendCompleteMessage(receipt: receipt); } }); } -Future tryToSendCompleteMessage(String receiptId) async { +Future tryToSendCompleteMessage({ + String? receiptId, + Receipt? receipt, + bool reupload = false, +}) async { try { - final retrans = await twonlyDB.messageRetransmissionDao - .getRetransmissionById(retransId) - .getSingleOrNull(); - - /// SET THE Message().receiptID !!!!!!! - /// ALSO THE encryptedContent is NOT YET ENCRYPTED! - - if (retrans == null) { - Log.error('$retransId not found in database'); - return; - } - - if (retrans.acknowledgeByServerAt != null) { - Log.error('$retransId message already retransmitted'); - return; - } - - final json = MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(retrans.plaintextContent), - ), - ) as Map, - ); - - Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}'); - - final contact = await twonlyDB.contactsDao - .getContactByUserId(retrans.contactId) - .getSingleOrNull(); - if (contact == null || contact.deleted) { - Log.warn('Contact deleted $retransId or not found in database.'); - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(retransId); - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, - const MessagesCompanion(errorWhileSending: Value(true)), - ); + if (receiptId == null && receipt == null) return; + if (receipt == null) { + receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); + if (receipt == null) { + Log.error('Receipt $receiptId not found.'); + return; } + } + receiptId = receipt.receiptId; + + if (reupload) { + await twonlyDB.receiptsDao.updateReceipt( + receiptId, + const ReceiptsCompanion( + ackByServerAt: Value(null), + ), + ); + } + + if (receipt.ackByServerAt != null) { + Log.error('$receiptId message already uploaded!'); return; } - final encryptedBytes = await signalEncryptMessage( - retrans.contactId, - retrans.plaintextContent, + Log.info('Uploading $receiptId (Message to ${receipt.contactId})'); + + final message = pb.Message.fromBuffer(receipt.message) + ..receiptId = receiptId; + + final encryptedContent = + pb.EncryptedContent.fromBuffer(message.encryptedContent); + + var pushData = await getPushDataFromEncryptedContent( + receipt.contactId, + receipt.messageId, + encryptedContent, ); - if (encryptedBytes == null) { - Log.error('Could not encrypt the message. Aborting and trying again.'); - return; + if (message.type == pb.Message_Type.TEST_NOTIFICATION) { + pushData = (PushNotification()..kind = PushKind.testNotification) + .writeToBuffer(); } - final encryptedHash = (await Sha256().hash(encryptedBytes)).bytes; - - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - encryptedHash: Value(Uint8List.fromList(encryptedHash)), - ), - ); + if (message.type == pb.Message_Type.CIPHERTEXT) { + final cipherText = await signalEncryptMessage( + receipt.contactId, + Uint8List.fromList(message.encryptedContent), + ); + if (cipherText == null) { + Log.error('Could not encrypt the message. Aborting and trying again.'); + return; + } + message.encryptedContent = cipherText.serialize(); + switch (cipherText.getType()) { + case CiphertextMessage.prekeyType: + message.type = pb.Message_Type.PREKEY_BUNDLE; + case CiphertextMessage.whisperType: + message.type = pb.Message_Type.CIPHERTEXT; + default: + Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); + return; + } + } final resp = await apiService.sendTextMessage( - retrans.contactId, - encryptedBytes, - retrans.pushData, + receipt.contactId, + message.writeToBuffer(), + pushData, ); - var retry = true; - if (resp.isError) { - Log.error('Could not retransmit message.'); + Log.error('Could not transmit message $receiptId got ${resp.error}.'); if (resp.error == ErrorCode.UserIdNotFound) { - retry = false; - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, - const MessagesCompanion(errorWhileSending: Value(true)), - ); - } + await twonlyDB.receiptsDao.deleteReceipt(receiptId); await twonlyDB.contactsDao.updateContact( - retrans.contactId, + receipt.contactId, const ContactsCompanion(deleted: Value(true)), ); + return; } } if (resp.isSuccess) { - retry = false; - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, + if (receipt.messageId != null) { + await twonlyDB.messagesDao.updateMessageId( + receipt.messageId!, const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), + ackByServer: Value(true), ), ); } - } - - if (!retry) { - if (!messageGetsAck(json.kind)) { - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(retransId); + if (!receipt.contactWillSendsReceipt) { + await twonlyDB.receiptsDao.deleteReceipt(receiptId); } else { - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(DateTime.now()), - retryCount: Value(retrans.retryCount + 1), + await twonlyDB.receiptsDao.updateReceipt( + receiptId, + ReceiptsCompanion( + ackByServerAt: Value(DateTime.now()), + retryCount: Value(receipt.retryCount + 1), lastRetry: Value(DateTime.now()), ), ); } } } catch (e) { - Log.error('error resending message: $e'); - await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId); + Log.error('Unknown Error when sending message: $e'); + if (receiptId != null) { + await twonlyDB.receiptsDao.deleteReceipt(receiptId); + } + if (receipt != null) { + await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); + } } } @@ -177,78 +167,31 @@ Future sendCipherText( ); if (receipt != null) { - await tryToSendCompleteMessage(receipt.receiptId); + await tryToSendCompleteMessage(receipt: receipt); } } -// Future sendTextMessage( -// int target, -// TextMessageContent content, -// PushNotification? pushNotification, -// ) async { -// final messageSendAt = DateTime.now(); -// DateTime? openedAt; - -// if (pushNotification != null && pushNotification.hasReactionContent()) { -// openedAt = DateTime.now(); -// } - -// final messageId = await twonlyDB.messagesDao.insertMessage( -// MessagesCompanion( -// contactId: Value(target), -// kind: const Value(MessageKind.textMessage), -// sendAt: Value(messageSendAt), -// responseToOtherMessageId: Value(content.responseToMessageId), -// responseToMessageId: Value(content.responseToOtherMessageId), -// downloadState: const Value(DownloadState.downloaded), -// openedAt: Value(openedAt), -// contentJson: Value( -// jsonEncode(content.toJson()), -// ), -// ), -// ); - -// if (messageId == null) return; - -// if (pushNotification != null && !pushNotification.hasReactionContent()) { -// pushNotification.messageId = Int64(messageId); -// } - -// final msg = MessageJson( -// kind: MessageKind.textMessage, -// messageSenderId: messageId, -// content: content, -// timestamp: messageSendAt, -// ); - -// await encryptAndSendMessageAsync( -// messageId, -// target, -// msg, -// pushNotification: pushNotification, -// ); -// } - Future notifyContactAboutOpeningMessage( - int fromUserId, - List messageOtherIds, + int contactId, + List messageOtherIds, ) async { var biggestMessageId = messageOtherIds.first; for (final messageOtherId in messageOtherIds) { - if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId; - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.opened, - messageReceiverId: messageOtherId, - content: MessageContent(), - timestamp: DateTime.now(), - ), - ); + if (isUUIDNewer(messageOtherId, biggestMessageId)) { + biggestMessageId = messageOtherId; + } } - await updateLastMessageId(fromUserId, biggestMessageId); + await sendCipherText( + contactId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.OPENED, + multipleSenderMessageIds: messageOtherIds, + ), + ), + ); + await updateLastMessageId(contactId, biggestMessageId); } Future notifyContactsAboutProfileChange({int? onlyToContact}) async { @@ -256,11 +199,13 @@ Future notifyContactsAboutProfileChange({int? onlyToContact}) async { if (user == null) return; if (user.avatarSvg == null) return; - final encryptedContent = pb.EncryptedContent() - ..contactUpdate = (pb.EncryptedContent_ContactUpdate() - ..type = pb.EncryptedContent_ContactUpdate_Type.UPDATE - ..avatarSvg = user.avatarSvg! - ..displayName = user.displayName); + final encryptedContent = pb.EncryptedContent( + contactUpdate: pb.EncryptedContent_ContactUpdate( + type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, + avatarSvg: user.avatarSvg, + displayName: user.displayName, + ), + ); if (onlyToContact != null) { await sendCipherText(onlyToContact, encryptedContent); diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 953924c..58b75ca 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -65,7 +65,7 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { Log.info( 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', ); - await tryToSendCompleteMessage(receiptId); + await tryToSendCompleteMessage(receiptId: receiptId, reupload: true); } case Message_Type.CIPHERTEXT: @@ -97,8 +97,10 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { contactWillSendsReceipt: const Value(false), ), ); - await tryToSendCompleteMessage(receiptId); + await tryToSendCompleteMessage(receiptId: receiptId); } + case Message_Type.TEST_NOTIFICATION: + return; } } diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index aff4c79..d305800 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -9,6 +9,7 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; Future handleContactRequest( @@ -17,6 +18,7 @@ Future handleContactRequest( ) async { switch (contactRequest.type) { case EncryptedContent_ContactRequest_Type.REQUEST: + Log.info('Got a contact request from $fromUserId'); // Request the username by the server so an attacker can not // forge the displayed username in the contact request final username = await apiService.getUsername(fromUserId); @@ -33,6 +35,7 @@ Future handleContactRequest( } await setupNotificationWithUsers(); case EncryptedContent_ContactRequest_Type.ACCEPT: + Log.info('Got a contact accept from $fromUserId'); await twonlyDB.contactsDao.updateContact( fromUserId, const ContactsCompanion( @@ -41,19 +44,23 @@ Future handleContactRequest( ), ); case EncryptedContent_ContactRequest_Type.REJECT: + Log.info('Got a contact reject from $fromUserId'); await twonlyDB.contactsDao.deleteContactByUserId(fromUserId); } } Future handleContactUpdate( - int fromUserId, - EncryptedContent_ContactUpdate contactUpdate, - int? senderProfileCounter) async { + int fromUserId, + EncryptedContent_ContactUpdate contactUpdate, + int? senderProfileCounter, +) async { switch (contactUpdate.type) { case EncryptedContent_ContactUpdate_Type.REQUEST: + Log.info('Got a contact update request from $fromUserId'); await notifyContactsAboutProfileChange(onlyToContact: fromUserId); case EncryptedContent_ContactUpdate_Type.UPDATE: + Log.info('Got a contact update $fromUserId'); if (contactUpdate.hasAvatarSvg() && contactUpdate.hasDisplayName() && senderProfileCounter != null) { @@ -74,6 +81,7 @@ Future handleFlameSync( int contactId, EncryptedContent_FlameSync flameSync, ) async { + Log.info('Got a flameSync from $contactId'); final contact = await twonlyDB.contactsDao .getContactByUserId(contactId) .getSingleOrNull(); diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 7b2583b..741c774 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -1,92 +1,158 @@ +import 'dart:async'; +import 'package:drift/drift.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/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/utils/log.dart'; -Future handleMedia(int fromUserId, String groupId, EncryptedContent_Media media) async { -TODO -} +Future handleMedia( + int fromUserId, + String groupId, + EncryptedContent_Media media, +) async { + Log.info( + 'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', + ); -Future handleMediaUpdate(int fromUserId, String groupId, EncryptedContent_MediaUpdate mediaUpdate) async { -TODO + late MediaType mediaType; + switch (media.type) { + case EncryptedContent_Media_Type.REUPLOAD: + final message = await twonlyDB.messagesDao + .getMessageById(media.senderMessageId) + .getSingleOrNull(); + if (message == null || + message.senderId != fromUserId || + message.mediaId == null) { + return; + } + // in case there was already a downloaded file delete it... + await removeMediaFile(message.mediaId!); - // switch (message.kind) { - // case MessageKind.receiveMediaError: - // if (message.messageReceiverId != null) { - // final openedMessage = await twonlyDB.messagesDao - // .getMessageByIdAndContactId(fromUserId, message.messageReceiverId!) - // .getSingleOrNull(); - - // if (openedMessage != null) { - // /// message found - - // /// checks if - // /// 1. this was a media upload - // /// 2. the media was not already retransmitted - // /// 3. the media was send in the last two days - // if (openedMessage.mediaUploadId != null && - // openedMessage.mediaRetransmissionState == - // MediaRetransmitting.none && - // openedMessage.sendAt - // .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { - // // reset the media upload state to pending, - // // this will cause the media to be re-encrypted again - // await twonlyDB.mediaUploadsDao.updateMediaUpload( - // openedMessage.mediaUploadId!, - // const MediaUploadsCompanion( - // state: Value( - // UploadState.pending, - // ), - // ), - // ); - // // reset the message upload so the upload will be done again - // await twonlyDB.messagesDao.updateMessageByOtherUser( - // fromUserId, - // message.messageReceiverId!, - // const MessagesCompanion( - // downloadState: Value(DownloadState.pending), - // mediaRetransmissionState: - // Value(MediaRetransmitting.retransmitted), - // ), - // ); - // unawaited(retryMediaUpload(false)); - // } else { - // await twonlyDB.messagesDao.updateMessageByOtherUser( - // fromUserId, - // message.messageReceiverId!, - // const MessagesCompanion( - // errorWhileSending: Value(true), - // ), - // ); - // } - // } - // } - - - if (message.kind == MessageKind.storedMediaFile) { - if (message.messageReceiverId != null) { - /// stored media file just updates the message - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - mediaStored: Value(true), - errorWhileSending: Value(false), + await twonlyDB.mediaFilesDao.updateMedia( + message.mediaId!, + MediaFilesCompanion( + downloadState: const Value(DownloadState.pending), + downloadToken: Value(Uint8List.fromList(media.downloadToken)), + encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), + encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), + encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), ), ); - final msg = await twonlyDB.messagesDao - .getMessageByIdAndContactId( - fromUserId, - message.messageReceiverId!, - ) - .getSingleOrNull(); - if (msg != null && msg.mediaUploadId != null) { - final filePath = await getMediaFilePath(msg.mediaUploadId, 'send'); - if (filePath.contains('mp4')) { - unawaited(createThumbnailsForVideo(File(filePath))); - } else { - unawaited(createThumbnailsForImage(File(filePath))); - } + + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + + if (mediaFile != null) { + unawaited(startDownloadMedia(mediaFile, false)); } - } - } else if (message.content != null) {} + + return; + case EncryptedContent_Media_Type.IMAGE: + mediaType = MediaType.image; + case EncryptedContent_Media_Type.VIDEO: + mediaType = MediaType.video; + case EncryptedContent_Media_Type.GIF: + mediaType = MediaType.gif; + } + + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + downloadState: const Value(DownloadState.pending), + type: Value(mediaType), + requiresAuthentication: Value(media.requiresAuthentication), + displayLimitInMilliseconds: Value( + media.hasDisplayLimitInMilliseconds() + ? media.displayLimitInMilliseconds.toInt() + : null, + ), + downloadToken: Value(Uint8List.fromList(media.downloadToken)), + encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), + encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), + encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), + createdAt: Value(fromTimestamp(media.timestamp)), + ), + ); + + if (mediaFile == null) { + return; + } + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(media.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + mediaId: Value(mediaFile.mediaId), + ackByServer: const Value(true), + ackByUser: const Value(true), + quotesMessageId: Value( + media.hasQuoteMessageId() ? media.quoteMessageId : null, + ), + createdAt: Value(fromTimestamp(media.timestamp)), + ), + ); + if (message != null) { + Log.info('Inserted a new media message with ID: ${message.messageId}'); + await twonlyDB.contactsDao.incFlameCounter( + fromUserId, + true, + fromTimestamp(media.timestamp), + ); + + unawaited(startDownloadMedia(mediaFile, false)); + } +} + +Future handleMediaUpdate( + int fromUserId, + String groupId, + EncryptedContent_MediaUpdate mediaUpdate, +) async { + final message = await twonlyDB.messagesDao + .getMessageById(mediaUpdate.targetMessageId) + .getSingleOrNull(); + if (message == null || message.mediaId == null) return; + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + if (mediaFile == null) return; + + switch (mediaUpdate.type) { + case EncryptedContent_MediaUpdate_Type.REOPENED: + Log.info('Got media file reopened ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + reopenByContact: Value(true), + ), + ); + case EncryptedContent_MediaUpdate_Type.STORED: + Log.info('Got media file stored ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + storedByContact: Value(true), + ), + ); + + unawaited(createThumbnailForMediaFile(mediaFile)); + + case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: + Log.info('Got media file decryption error ${mediaFile.mediaId}'); + final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? []; + reuploadRequestedBy.add(fromUserId); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + uploadState: const Value(UploadState.pending), + reuploadRequestedBy: Value(reuploadRequestedBy), + ), + ); + } } diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/server_messages/messages.server_messages.dart index 6a65873..297aa0e 100644 --- a/lib/src/services/api/server_messages/messages.server_messages.dart +++ b/lib/src/services/api/server_messages/messages.server_messages.dart @@ -10,12 +10,15 @@ Future handleMessageUpdate( ) async { switch (messageUpdate.type) { case EncryptedContent_MessageUpdate_Type.OPENED: - Log.info('Opened message ${messageUpdate.senderMessageId}'); - await twonlyDB.messagesDao.handleMessageOpened( - groupId, - messageUpdate.senderMessageId, - fromTimestamp(messageUpdate.timestamp), - ); + Log.info( + 'Opened message ${messageUpdate.multipleSenderMessageIds.length}'); + for (final senderMessageId in messageUpdate.multipleSenderMessageIds) { + await twonlyDB.messagesDao.handleMessageOpened( + groupId, + senderMessageId, + fromTimestamp(messageUpdate.timestamp), + ); + } case EncryptedContent_MessageUpdate_Type.DELETE: Log.info('Delete message ${messageUpdate.senderMessageId}'); await twonlyDB.messagesDao.handleMessageDeletion( diff --git a/lib/src/services/api/server_messages/pushkeys.server_messages.dart b/lib/src/services/api/server_messages/pushkeys.server_messages.dart index 4e755ff..6e3cb4d 100644 --- a/lib/src/services/api/server_messages/pushkeys.server_messages.dart +++ b/lib/src/services/api/server_messages/pushkeys.server_messages.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); @@ -11,6 +12,7 @@ Future handlePushKey( ) async { switch (pushKeys.type) { case EncryptedContent_PushKeys_Type.REQUEST: + Log.info('Got a pushkey request from $contactId'); if (lastPushKeyRequest .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { lastPushKeyRequest = DateTime.now(); @@ -18,6 +20,7 @@ Future handlePushKey( } case EncryptedContent_PushKeys_Type.UPDATE: + Log.info('Got a pushkey update from $contactId'); await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); } } diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/server_messages/reaction.server_message.dart index 733457c..892a27d 100644 --- a/lib/src/services/api/server_messages/reaction.server_message.dart +++ b/lib/src/services/api/server_messages/reaction.server_message.dart @@ -1,11 +1,13 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/utils/log.dart'; Future handleReaction( int fromUserId, String groupId, EncryptedContent_Reaction reaction, ) async { + Log.info('Got a reaction from $fromUserId'); if (reaction.hasRemove()) { if (reaction.remove) { await twonlyDB.reactionsDao diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index df9140c..c5cef42 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -1,125 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/utils/log.dart'; Future handleTextMessage( int fromUserId, String groupId, EncryptedContent_TextMessage textMessage, ) async { - TODO - // final content = message.content!; - // // when a message is received doubled ignore it... + Log.info( + 'Got a text message: ${textMessage.senderMessageId} from $groupId', + ); - // final openedMessage = await twonlyDB.messagesDao - // .getMessageByOtherMessageId(fromUserId, message.messageSenderId!) - // .getSingleOrNull(); - - // if (openedMessage != null) { - // if (openedMessage.errorWhileSending) { - // await twonlyDB.messagesDao - // .deleteMessagesByMessageId(openedMessage.messageId); - // } else { - // Log.error( - // 'Got a duplicated message from other user: ${message.messageSenderId!}', - // ); - // final ok = client.Response_Ok()..none = true; - // return client.Response()..ok = ok; - // } - // } - - // int? responseToMessageId; - // int? responseToOtherMessageId; - // int? messageId; - - // var acknowledgeByUser = false; - // DateTime? openedAt; - - // if (message.kind == MessageKind.reopenedMedia) { - // acknowledgeByUser = true; - // openedAt = DateTime.now(); - // } - - // if (content is TextMessageContent) { - // responseToMessageId = content.responseToMessageId; - // responseToOtherMessageId = content.responseToOtherMessageId; - - // if (responseToMessageId != null || responseToOtherMessageId != null) { - // // reactions are shown in the notification directly... - // if (isEmoji(content.text)) { - // openedAt = DateTime.now(); - // } - // } - // } - // if (content is ReopenedMediaFileContent) { - // responseToMessageId = content.messageId; - // } - - // if (responseToMessageId != null) { - // await twonlyDB.messagesDao.updateMessageByOtherUser( - // fromUserId, - // responseToMessageId, - // MessagesCompanion( - // errorWhileSending: const Value(false), - // openedAt: Value( - // DateTime.now(), - // ), // when a user reacted to the media file, it should be marked as opened - // ), - // ); - // } - - // final contentJson = jsonEncode(content.toJson()); - // final update = MessagesCompanion( - // contactId: Value(fromUserId), - // kind: Value(message.kind), - // messageOtherId: Value(message.messageSenderId), - // contentJson: Value(contentJson), - // acknowledgeByServer: const Value(true), - // acknowledgeByUser: Value(acknowledgeByUser), - // responseToMessageId: Value(responseToMessageId), - // responseToOtherMessageId: Value(responseToOtherMessageId), - // openedAt: Value(openedAt), - // downloadState: Value( - // message.kind == MessageKind.media - // ? DownloadState.pending - // : DownloadState.downloaded, - // ), - // sendAt: Value(message.timestamp), - // ); - - // messageId = await twonlyDB.messagesDao.insertMessage( - // update, - // ); - - // if (messageId == null) { - // Log.error('could not insert message into db'); - // return client.Response()..error = ErrorCode.InternalError; - // } - - // Log.info('Inserted a new message with id: $messageId'); - - // if (message.kind == MessageKind.media) { - // await twonlyDB.contactsDao.incFlameCounter( - // fromUserId, - // true, - // message.timestamp, - // ); - - // final msg = await twonlyDB.messagesDao - // .getMessageByMessageId(messageId) - // .getSingleOrNull(); - // if (msg != null) { - // unawaited(startDownloadMedia(msg, false)); - // } - // } - // } else { - // Log.error('Content is not defined $message'); - // } - - // // unarchive contact when receiving a new message - // await twonlyDB.contactsDao.updateContact( - // fromUserId, - // const ContactsCompanion( - // archived: Value(false), - // ), - // ); - // return null; + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(textMessage.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + content: Value(textMessage.text), + ackByServer: const Value(true), + ackByUser: const Value(true), + quotesMessageId: Value( + textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, + ), + createdAt: Value(fromTimestamp(textMessage.timestamp)), + ), + ); + if (message != null) { + Log.info('Inserted a new text message with ID: ${message.messageId}'); + } } diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 294fd03..fde1e92 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -7,7 +7,7 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pbenum.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart' diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 562da78..1abf6fc 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -7,15 +7,17 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart' as my; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; /// This function must be called after the database is setup Future setupNotificationWithUsers({ @@ -104,19 +106,14 @@ Future setupNotificationWithUsers({ } Future sendNewPushKey(int userId, PushKey pushKey) async { - await encryptAndSendMessageAsync( - null, + await sendCipherText( userId, - my.MessageJson( - kind: MessageKind.pushKey, - content: my.PushKeyContent( - keyId: pushKey.id.toInt(), - key: pushKey.key, - ), - timestamp: DateTime.fromMillisecondsSinceEpoch( - pushKey.createdAtUnixTimestamp.toInt(), - ), - ), + EncryptedContent() + ..pushKeys = (EncryptedContent_PushKeys() + ..type = EncryptedContent_PushKeys_Type.UPDATE + ..key = pushKey.key + ..keyId = pushKey.id + ..createdAt = pushKey.createdAtUnixTimestamp), ); } @@ -132,7 +129,7 @@ Future updatePushUser(Contact contact) async { displayName: getContactDisplayName(contact), pushKeys: [], blocked: contact.blocked, - lastMessageId: Int64(), + lastMessageId: uuid.v7(), ), ); } else { @@ -160,7 +157,7 @@ Future handleNewPushKey(int fromUserId, int keyId, List key) async { displayName: getContactDisplayName(contact), pushKeys: [], blocked: contact.blocked, - lastMessageId: Int64(), + lastMessageId: uuid.v7(), ), ); pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); @@ -174,8 +171,8 @@ Future handleNewPushKey(int fromUserId, int keyId, List key) async { pushUser!.pushKeys.clear(); pushUser.pushKeys.add( PushKey( - id: Int64(pushKey.keyId), - key: pushKey.key, + id: Int64(keyId), + key: key, createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), ), ); @@ -183,7 +180,7 @@ Future handleNewPushKey(int fromUserId, int keyId, List key) async { await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys); } -Future updateLastMessageId(int fromUserId, int messageId) async { +Future updateLastMessageId(int fromUserId, String messageId) async { final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId); @@ -192,15 +189,103 @@ Future updateLastMessageId(int fromUserId, int messageId) async { return; } - if (pushUser.lastMessageId < Int64(messageId)) { - pushUser.lastMessageId = Int64(messageId); + if (isUUIDNewer(messageId, pushUser.lastMessageId)) { + pushUser.lastMessageId = messageId; await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); } } +Future getPushDataFromEncryptedContent( + int toUserId, + String? messageId, + EncryptedContent content, +) async { + late PushKind kind; + String? reactionContent; + + if (content.hasReaction()) { + if (content.reaction.remove) return null; + + final msg = await twonlyDB.messagesDao + .getMessageById(content.reaction.targetMessageId) + .getSingleOrNull(); + if (msg == null) return null; + if (msg.content != null) { + kind = PushKind.reactionToText; + } else if (msg.mediaId != null) { + final media = await twonlyDB.mediaFilesDao.getMediaFileById(msg.mediaId!); + if (media == null) return null; + switch (media.type) { + case MediaType.image: + kind = PushKind.reactionToImage; + case MediaType.video: + kind = PushKind.reactionToVideo; + case MediaType.gif: + kind = PushKind.reaction; + } + } + reactionContent = content.reaction.emoji; + } + + if (content.hasTextMessage()) { + kind = PushKind.text; + if (content.textMessage.hasQuoteMessageId()) { + kind = PushKind.response; + } + } + if (content.hasMedia()) { + switch (content.media.type) { + case EncryptedContent_Media_Type.IMAGE: + kind = PushKind.image; + case EncryptedContent_Media_Type.VIDEO: + kind = PushKind.video; + // ignore: no_default_cases + default: + return null; + } + if (content.media.requiresAuthentication) { + kind = PushKind.twonly; + } + } + + if (content.hasContactRequest()) { + switch (content.contactRequest.type) { + case EncryptedContent_ContactRequest_Type.REQUEST: + kind = PushKind.contactRequest; + case EncryptedContent_ContactRequest_Type.ACCEPT: + kind = PushKind.acceptRequest; + case EncryptedContent_ContactRequest_Type.REJECT: + return null; + } + } + + if (content.hasMediaUpdate()) { + switch (content.mediaUpdate.type) { + case EncryptedContent_MediaUpdate_Type.REOPENED: + kind = PushKind.reopenedMedia; + case EncryptedContent_MediaUpdate_Type.STORED: + kind = PushKind.storedMediaFile; + case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: + return null; + } + } + + final pushNotification = PushNotification()..kind = kind; + if (reactionContent != null) { + pushNotification.reactionContent = reactionContent; + } + if (messageId != null) { + pushNotification.messageId = messageId; + } + return encryptPushNotification(toUserId, pushNotification); +} + /// this will trigger a push notification /// push notification only containing the message kind and username -Future getPushData(int toUserId, PushNotification content) async { +Future encryptPushNotification( + int toUserId, + PushNotification content, +) async { final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); var key = 'InsecureOnlyUsedForAddingContact'.codeUnits; @@ -218,14 +303,12 @@ Future getPushData(int toUserId, PushNotification content) async { // this will be enforced after every app uses this system... :/ // return null; Log.error('Using insecure key as the receiver does not send a push key!'); - await encryptAndSendMessageAsync( - null, + + await sendCipherText( toUserId, - my.MessageJson( - kind: MessageKind.requestPushKey, - content: my.MessageContent(), - timestamp: DateTime.now(), - ), + EncryptedContent() + ..pushKeys = (EncryptedContent_PushKeys() + ..type = EncryptedContent_PushKeys_Type.REQUEST), ); } } else { diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 1338663..50aad34 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -7,16 +7,15 @@ import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; /// This caused some troubles, so protection the encryption... final lockingSignalEncryption = Mutex(); -Future signalEncryptMessage( +Future signalEncryptMessage( int target, Uint8List plaintextContent, ) async { - return lockingSignalEncryption.protect(() async { + return lockingSignalEncryption.protect(() async { try { final signalStore = (await getSignalStore())!; final address = SignalProtocolAddress(target.toString(), defaultDeviceId); @@ -75,14 +74,7 @@ Future signalEncryptMessage( Log.error('did not get the identity of the remote address'); } } - - final ciphertext = await session.encrypt(plaintextContent); - - final b = BytesBuilder() - ..add(ciphertext.serialize()) - ..add(intToBytes(ciphertext.getType())); - - return b.takeBytes(); + return await session.encrypt(plaintextContent); } catch (e) { Log.error(e.toString()); return null; diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/thumbnail.service.dart index e1311e7..a40d07c 100644 --- a/lib/src/services/thumbnail.service.dart +++ b/lib/src/services/thumbnail.service.dart @@ -2,9 +2,23 @@ import 'dart:io'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:path/path.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; + +Future createThumbnailForMediaFile(MediaFile media) async { + + switch (media.type) { + case MediaType.image: + TODO + break; + default: + } + +} + Future createThumbnailsForImage(File file) async { final fileExtension = file.path.split('.').last.toLowerCase(); if (fileExtension != 'png') { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 63d2475..1f1973d 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -267,3 +267,9 @@ MediaMessageContent? getMediaContent(Message message) { return null; } } + +bool isUUIDNewer(String uuid1, String uuid2) { + final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); + final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); + return timestamp1 > timestamp2; +} diff --git a/lib/src/views/settings/notification.view.dart b/lib/src/views/settings/notification.view.dart index db9be48..49b4e72 100644 --- a/lib/src/views/settings/notification.view.dart +++ b/lib/src/views/settings/notification.view.dart @@ -1,13 +1,12 @@ import 'dart:io'; - -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -52,10 +51,10 @@ class NotificationView extends StatelessWidget { if (run) { final user = await getUser(); if (user != null) { - final pushData = await getPushData( + final pushData = await encryptPushNotification( user.userId, PushNotification( - messageId: Int64(), + messageId: uuid.v4(), kind: PushKind.testNotification, ), ); diff --git a/test/unit_test.dart b/test/unit_test.dart index 36e6252..8d4c715 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,9 +1,11 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -40,5 +42,11 @@ void main() { final uv4String = utf8.decode(uv4Bytes.cast()); expect(uv4String, uv4); }); + test('comparing uui7', () async { + final uv7Old = uuid.v7(); + sleep(const Duration(milliseconds: 1000)); + final uv7New = uuid.v7(); + expect(isUUIDNewer(uv7New, uv7Old), true); + }); }); } From 0207aaf07413f0157cbc04da7aaab0901bf80edf Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Oct 2025 09:03:17 +0200 Subject: [PATCH 04/76] fixing more issues --- lib/src/database/daos/messages.dao.dart | 4 + lib/src/database/daos/messages.dao.g.dart | 2 +- lib/src/database/daos/reactions.dao.g.dart | 1 + lib/src/database/daos/receipts.dao.g.dart | 1 + lib/src/database/tables/mediafiles.table.dart | 2 +- lib/src/database/tables/messages.table.dart | 4 +- lib/src/database/twonly.db.g.dart | 1424 +++++++++-------- lib/src/services/api/utils.dart | 54 +- lib/src/services/flame.service.dart | 18 +- .../background.notifications.dart | 7 +- 10 files changed, 849 insertions(+), 668 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index b96904e..963ef02 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -348,6 +348,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return select(messages)..where((t) => t.messageId.equals(messageId)); } + Future> getMessagesByMediaId(String mediaId) async { + return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); + } + // Future> getMessagesByMediaUploadId(int mediaUploadId) async { // return (select(messages) // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart index 5f11d3a..e763f72 100644 --- a/lib/src/database/daos/messages.dao.g.dart +++ b/lib/src/database/daos/messages.dao.g.dart @@ -4,10 +4,10 @@ part of 'messages.dao.dart'; // ignore_for_file: type=lint mixin _$MessagesDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; $ContactsTable get contacts => attachedDatabase.contacts; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; $MessageHistoriesTable get messageHistories => attachedDatabase.messageHistories; - $GroupsTable get groups => attachedDatabase.groups; } diff --git a/lib/src/database/daos/reactions.dao.g.dart b/lib/src/database/daos/reactions.dao.g.dart index 2fcd9af..26ac0da 100644 --- a/lib/src/database/daos/reactions.dao.g.dart +++ b/lib/src/database/daos/reactions.dao.g.dart @@ -4,6 +4,7 @@ part of 'reactions.dao.dart'; // ignore_for_file: type=lint mixin _$ReactionsDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; $ContactsTable get contacts => attachedDatabase.contacts; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart index d0b9b41..5a06998 100644 --- a/lib/src/database/daos/receipts.dao.g.dart +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -5,6 +5,7 @@ part of 'receipts.dao.dart'; // ignore_for_file: type=lint mixin _$ReceiptsDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; + $GroupsTable get groups => attachedDatabase.groups; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; $ReceiptsTable get receipts => attachedDatabase.receipts; diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index db4a812..34990a4 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -16,7 +16,7 @@ enum UploadState { receiverNotified, } -enum DownloadState { pending, downloading } +enum DownloadState { pending, downloading, reuploadRequested } @DataClassName('MediaFile') class MediaFiles extends Table { diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 3537c63..b7ef767 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -1,11 +1,13 @@ import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @DataClassName('Message') class Messages extends Table { - TextColumn get groupId => text()(); + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); TextColumn get messageId => text().clientDefault(() => uuid.v7())(); // in case senderId is null, it was send by user itself diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 472e56a..55b6fe6 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1011,6 +1011,418 @@ class ContactsCompanion extends UpdateCompanion { } } +class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v4()); + static const VerificationMeta _isGroupAdminMeta = + const VerificationMeta('isGroupAdmin'); + @override + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_admin" IN (0, 1))')); + static const VerificationMeta _isGroupOfTwoMeta = + const VerificationMeta('isGroupOfTwo'); + @override + late final GeneratedColumn isGroupOfTwo = GeneratedColumn( + 'is_group_of_two', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_of_two" IN (0, 1))')); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _archivedMeta = + const VerificationMeta('archived'); + @override + late final GeneratedColumn archived = GeneratedColumn( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _lastMessageExchangeMeta = + const VerificationMeta('lastMessageExchange'); + @override + late final GeneratedColumn lastMessageExchange = + GeneratedColumn('last_message_exchange', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isGroupOfTwo, + pinned, + archived, + lastMessageExchange, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } + if (data.containsKey('is_group_admin')) { + context.handle( + _isGroupAdminMeta, + isGroupAdmin.isAcceptableOrUnknown( + data['is_group_admin']!, _isGroupAdminMeta)); + } else if (isInserting) { + context.missing(_isGroupAdminMeta); + } + if (data.containsKey('is_group_of_two')) { + context.handle( + _isGroupOfTwoMeta, + isGroupOfTwo.isAcceptableOrUnknown( + data['is_group_of_two']!, _isGroupOfTwoMeta)); + } else if (isInserting) { + context.missing(_isGroupOfTwoMeta); + } + if (data.containsKey('pinned')) { + context.handle(_pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + } + if (data.containsKey('archived')) { + context.handle(_archivedMeta, + archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); + } + if (data.containsKey('last_message_exchange')) { + context.handle( + _lastMessageExchangeMeta, + lastMessageExchange.isAcceptableOrUnknown( + data['last_message_exchange']!, _lastMessageExchangeMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupId}; + @override + Group map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Group( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + isGroupAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, + isGroupOfTwo: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_of_two'])!, + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_exchange'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $GroupsTable createAlias(String alias) { + return $GroupsTable(attachedDatabase, alias); + } +} + +class Group extends DataClass implements Insertable { + final String groupId; + final bool isGroupAdmin; + final bool isGroupOfTwo; + final bool pinned; + final bool archived; + final DateTime lastMessageExchange; + final DateTime createdAt; + const Group( + {required this.groupId, + required this.isGroupAdmin, + required this.isGroupOfTwo, + required this.pinned, + required this.archived, + required this.lastMessageExchange, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_group_of_two'] = Variable(isGroupOfTwo); + map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); + map['last_message_exchange'] = Variable(lastMessageExchange); + map['created_at'] = Variable(createdAt); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isGroupOfTwo: Value(isGroupOfTwo), + pinned: Value(pinned), + archived: Value(archived), + lastMessageExchange: Value(lastMessageExchange), + createdAt: Value(createdAt), + ); + } + + factory Group.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Group( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), + pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), + lastMessageExchange: + serializer.fromJson(json['lastMessageExchange']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), + 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Group copyWith( + {String? groupId, + bool? isGroupAdmin, + bool? isGroupOfTwo, + bool? pinned, + bool? archived, + DateTime? lastMessageExchange, + DateTime? createdAt}) => + Group( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + createdAt: createdAt ?? this.createdAt, + ); + Group copyWithCompanion(GroupsCompanion data) { + return Group( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isGroupOfTwo: data.isGroupOfTwo.present + ? data.isGroupOfTwo.value + : this.isGroupOfTwo, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Group(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isGroupOfTwo: $isGroupOfTwo, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, + archived, lastMessageExchange, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Group && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isGroupOfTwo == this.isGroupOfTwo && + other.pinned == this.pinned && + other.archived == this.archived && + other.lastMessageExchange == this.lastMessageExchange && + other.createdAt == this.createdAt); +} + +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isGroupOfTwo; + final Value pinned; + final Value archived; + final Value lastMessageExchange; + final Value createdAt; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isGroupOfTwo = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + this.groupId = const Value.absent(), + required bool isGroupAdmin, + required bool isGroupOfTwo, + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : isGroupAdmin = Value(isGroupAdmin), + isGroupOfTwo = Value(isGroupOfTwo); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isGroupOfTwo, + Expression? pinned, + Expression? archived, + Expression? lastMessageExchange, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith( + {Value? groupId, + Value? isGroupAdmin, + Value? isGroupOfTwo, + Value? pinned, + Value? archived, + Value? lastMessageExchange, + Value? createdAt, + Value? rowid}) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); + } + if (isGroupOfTwo.present) { + map['is_group_of_two'] = Variable(isGroupOfTwo.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (archived.present) { + map['archived'] = Variable(archived.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = + Variable(lastMessageExchange.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isGroupOfTwo: $isGroupOfTwo, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $MediaFilesTable extends MediaFiles with TableInfo<$MediaFilesTable, MediaFile> { @override @@ -1790,7 +2202,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { @override late final GeneratedColumn groupId = GeneratedColumn( 'group_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @override @@ -3008,418 +3423,6 @@ class ReactionsCompanion extends UpdateCompanion { } } -class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $GroupsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _groupIdMeta = - const VerificationMeta('groupId'); - @override - late final GeneratedColumn groupId = GeneratedColumn( - 'group_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v4()); - static const VerificationMeta _isGroupAdminMeta = - const VerificationMeta('isGroupAdmin'); - @override - late final GeneratedColumn isGroupAdmin = GeneratedColumn( - 'is_group_admin', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_group_admin" IN (0, 1))')); - static const VerificationMeta _isGroupOfTwoMeta = - const VerificationMeta('isGroupOfTwo'); - @override - late final GeneratedColumn isGroupOfTwo = GeneratedColumn( - 'is_group_of_two', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_group_of_two" IN (0, 1))')); - static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); - @override - late final GeneratedColumn pinned = GeneratedColumn( - 'pinned', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _archivedMeta = - const VerificationMeta('archived'); - @override - late final GeneratedColumn archived = GeneratedColumn( - 'archived', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _lastMessageExchangeMeta = - const VerificationMeta('lastMessageExchange'); - @override - late final GeneratedColumn lastMessageExchange = - GeneratedColumn('last_message_exchange', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); - @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - @override - List get $columns => [ - groupId, - isGroupAdmin, - isGroupOfTwo, - pinned, - archived, - lastMessageExchange, - createdAt - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'groups'; - @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('group_id')) { - context.handle(_groupIdMeta, - groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); - } - if (data.containsKey('is_group_admin')) { - context.handle( - _isGroupAdminMeta, - isGroupAdmin.isAcceptableOrUnknown( - data['is_group_admin']!, _isGroupAdminMeta)); - } else if (isInserting) { - context.missing(_isGroupAdminMeta); - } - if (data.containsKey('is_group_of_two')) { - context.handle( - _isGroupOfTwoMeta, - isGroupOfTwo.isAcceptableOrUnknown( - data['is_group_of_two']!, _isGroupOfTwoMeta)); - } else if (isInserting) { - context.missing(_isGroupOfTwoMeta); - } - if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, - pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); - } - if (data.containsKey('archived')) { - context.handle(_archivedMeta, - archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); - } - if (data.containsKey('last_message_exchange')) { - context.handle( - _lastMessageExchangeMeta, - lastMessageExchange.isAcceptableOrUnknown( - data['last_message_exchange']!, _lastMessageExchangeMeta)); - } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - return context; - } - - @override - Set get $primaryKey => {groupId}; - @override - Group map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Group( - groupId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, - isGroupAdmin: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, - isGroupOfTwo: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_group_of_two'])!, - pinned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, - archived: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, - lastMessageExchange: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}last_message_exchange'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - ); - } - - @override - $GroupsTable createAlias(String alias) { - return $GroupsTable(attachedDatabase, alias); - } -} - -class Group extends DataClass implements Insertable { - final String groupId; - final bool isGroupAdmin; - final bool isGroupOfTwo; - final bool pinned; - final bool archived; - final DateTime lastMessageExchange; - final DateTime createdAt; - const Group( - {required this.groupId, - required this.isGroupAdmin, - required this.isGroupOfTwo, - required this.pinned, - required this.archived, - required this.lastMessageExchange, - required this.createdAt}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['group_id'] = Variable(groupId); - map['is_group_admin'] = Variable(isGroupAdmin); - map['is_group_of_two'] = Variable(isGroupOfTwo); - map['pinned'] = Variable(pinned); - map['archived'] = Variable(archived); - map['last_message_exchange'] = Variable(lastMessageExchange); - map['created_at'] = Variable(createdAt); - return map; - } - - GroupsCompanion toCompanion(bool nullToAbsent) { - return GroupsCompanion( - groupId: Value(groupId), - isGroupAdmin: Value(isGroupAdmin), - isGroupOfTwo: Value(isGroupOfTwo), - pinned: Value(pinned), - archived: Value(archived), - lastMessageExchange: Value(lastMessageExchange), - createdAt: Value(createdAt), - ); - } - - factory Group.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return Group( - groupId: serializer.fromJson(json['groupId']), - isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), - isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), - pinned: serializer.fromJson(json['pinned']), - archived: serializer.fromJson(json['archived']), - lastMessageExchange: - serializer.fromJson(json['lastMessageExchange']), - createdAt: serializer.fromJson(json['createdAt']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'groupId': serializer.toJson(groupId), - 'isGroupAdmin': serializer.toJson(isGroupAdmin), - 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), - 'pinned': serializer.toJson(pinned), - 'archived': serializer.toJson(archived), - 'lastMessageExchange': serializer.toJson(lastMessageExchange), - 'createdAt': serializer.toJson(createdAt), - }; - } - - Group copyWith( - {String? groupId, - bool? isGroupAdmin, - bool? isGroupOfTwo, - bool? pinned, - bool? archived, - DateTime? lastMessageExchange, - DateTime? createdAt}) => - Group( - groupId: groupId ?? this.groupId, - isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, - isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, - pinned: pinned ?? this.pinned, - archived: archived ?? this.archived, - lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, - createdAt: createdAt ?? this.createdAt, - ); - Group copyWithCompanion(GroupsCompanion data) { - return Group( - groupId: data.groupId.present ? data.groupId.value : this.groupId, - isGroupAdmin: data.isGroupAdmin.present - ? data.isGroupAdmin.value - : this.isGroupAdmin, - isGroupOfTwo: data.isGroupOfTwo.present - ? data.isGroupOfTwo.value - : this.isGroupOfTwo, - pinned: data.pinned.present ? data.pinned.value : this.pinned, - archived: data.archived.present ? data.archived.value : this.archived, - lastMessageExchange: data.lastMessageExchange.present - ? data.lastMessageExchange.value - : this.lastMessageExchange, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - ); - } - - @override - String toString() { - return (StringBuffer('Group(') - ..write('groupId: $groupId, ') - ..write('isGroupAdmin: $isGroupAdmin, ') - ..write('isGroupOfTwo: $isGroupOfTwo, ') - ..write('pinned: $pinned, ') - ..write('archived: $archived, ') - ..write('lastMessageExchange: $lastMessageExchange, ') - ..write('createdAt: $createdAt') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, - archived, lastMessageExchange, createdAt); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is Group && - other.groupId == this.groupId && - other.isGroupAdmin == this.isGroupAdmin && - other.isGroupOfTwo == this.isGroupOfTwo && - other.pinned == this.pinned && - other.archived == this.archived && - other.lastMessageExchange == this.lastMessageExchange && - other.createdAt == this.createdAt); -} - -class GroupsCompanion extends UpdateCompanion { - final Value groupId; - final Value isGroupAdmin; - final Value isGroupOfTwo; - final Value pinned; - final Value archived; - final Value lastMessageExchange; - final Value createdAt; - final Value rowid; - const GroupsCompanion({ - this.groupId = const Value.absent(), - this.isGroupAdmin = const Value.absent(), - this.isGroupOfTwo = const Value.absent(), - this.pinned = const Value.absent(), - this.archived = const Value.absent(), - this.lastMessageExchange = const Value.absent(), - this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), - }); - GroupsCompanion.insert({ - this.groupId = const Value.absent(), - required bool isGroupAdmin, - required bool isGroupOfTwo, - this.pinned = const Value.absent(), - this.archived = const Value.absent(), - this.lastMessageExchange = const Value.absent(), - this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), - }) : isGroupAdmin = Value(isGroupAdmin), - isGroupOfTwo = Value(isGroupOfTwo); - static Insertable custom({ - Expression? groupId, - Expression? isGroupAdmin, - Expression? isGroupOfTwo, - Expression? pinned, - Expression? archived, - Expression? lastMessageExchange, - Expression? createdAt, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (groupId != null) 'group_id': groupId, - if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, - if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, - if (pinned != null) 'pinned': pinned, - if (archived != null) 'archived': archived, - if (lastMessageExchange != null) - 'last_message_exchange': lastMessageExchange, - if (createdAt != null) 'created_at': createdAt, - if (rowid != null) 'rowid': rowid, - }); - } - - GroupsCompanion copyWith( - {Value? groupId, - Value? isGroupAdmin, - Value? isGroupOfTwo, - Value? pinned, - Value? archived, - Value? lastMessageExchange, - Value? createdAt, - Value? rowid}) { - return GroupsCompanion( - groupId: groupId ?? this.groupId, - isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, - isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, - pinned: pinned ?? this.pinned, - archived: archived ?? this.archived, - lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, - createdAt: createdAt ?? this.createdAt, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (groupId.present) { - map['group_id'] = Variable(groupId.value); - } - if (isGroupAdmin.present) { - map['is_group_admin'] = Variable(isGroupAdmin.value); - } - if (isGroupOfTwo.present) { - map['is_group_of_two'] = Variable(isGroupOfTwo.value); - } - if (pinned.present) { - map['pinned'] = Variable(pinned.value); - } - if (archived.present) { - map['archived'] = Variable(archived.value); - } - if (lastMessageExchange.present) { - map['last_message_exchange'] = - Variable(lastMessageExchange.value); - } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('GroupsCompanion(') - ..write('groupId: $groupId, ') - ..write('isGroupAdmin: $isGroupAdmin, ') - ..write('isGroupOfTwo: $isGroupOfTwo, ') - ..write('pinned: $pinned, ') - ..write('archived: $archived, ') - ..write('lastMessageExchange: $lastMessageExchange, ') - ..write('createdAt: $createdAt, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - class $GroupMembersTable extends GroupMembers with TableInfo<$GroupMembersTable, GroupMember> { @override @@ -5810,12 +5813,12 @@ abstract class _$TwonlyDB extends GeneratedDatabase { _$TwonlyDB(QueryExecutor e) : super(e); $TwonlyDBManager get managers => $TwonlyDBManager(this); late final $ContactsTable contacts = $ContactsTable(this); + late final $GroupsTable groups = $GroupsTable(this); late final $MediaFilesTable mediaFiles = $MediaFilesTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $MessageHistoriesTable messageHistories = $MessageHistoriesTable(this); late final $ReactionsTable reactions = $ReactionsTable(this); - late final $GroupsTable groups = $GroupsTable(this); late final $GroupMembersTable groupMembers = $GroupMembersTable(this); late final $ReceiptsTable receipts = $ReceiptsTable(this); late final $SignalIdentityKeyStoresTable signalIdentityKeyStores = @@ -5843,11 +5846,11 @@ abstract class _$TwonlyDB extends GeneratedDatabase { @override List get allSchemaEntities => [ contacts, + groups, mediaFiles, messages, messageHistories, reactions, - groups, groupMembers, receipts, signalIdentityKeyStores, @@ -5860,6 +5863,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( [ + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('messages', limitUpdateKind: UpdateKind.delete), @@ -6809,6 +6819,287 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< bool receiptsRefs, bool signalContactPreKeysRefs, bool signalContactSignedPreKeysRefs})>; +typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ + Value groupId, + required bool isGroupAdmin, + required bool isGroupOfTwo, + Value pinned, + Value archived, + Value lastMessageExchange, + Value createdAt, + Value rowid, +}); +typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ + Value groupId, + Value isGroupAdmin, + Value isGroupOfTwo, + Value pinned, + Value archived, + Value lastMessageExchange, + Value createdAt, + Value rowid, +}); + +final class $$GroupsTableReferences + extends BaseReferences<_$TwonlyDB, $GroupsTable, Group> { + $$GroupsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.groups.groupId, db.messages.groupId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, builder: (column) => ColumnFilters(column)); + + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnFilters(column)); + + ColumnFilters get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$GroupsTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumn get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => column); + + GeneratedColumn get isGroupOfTwo => $composableBuilder( + column: $table.isGroupOfTwo, builder: (column) => column); + + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); + + GeneratedColumn get archived => + $composableBuilder(column: $table.archived, builder: (column) => column); + + GeneratedColumn get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$GroupsTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, + PrefetchHooks Function({bool messagesRefs})> { + $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value isGroupAdmin = const Value.absent(), + Value isGroupOfTwo = const Value.absent(), + Value pinned = const Value.absent(), + Value archived = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isGroupOfTwo: isGroupOfTwo, + pinned: pinned, + archived: archived, + lastMessageExchange: lastMessageExchange, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + Value groupId = const Value.absent(), + required bool isGroupAdmin, + required bool isGroupOfTwo, + Value pinned = const Value.absent(), + Value archived = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion.insert( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isGroupOfTwo: isGroupOfTwo, + pinned: pinned, + archived: archived, + lastMessageExchange: lastMessageExchange, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$GroupsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({messagesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (messagesRefs) db.messages], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$GroupsTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0).messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, + PrefetchHooks Function({bool messagesRefs})>; typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value mediaId, required MediaType type, @@ -7262,6 +7553,20 @@ final class $$MessagesTableReferences extends BaseReferences<_$TwonlyDB, $MessagesTable, Message> { $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.messages.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + static $ContactsTable _senderIdTable(_$TwonlyDB db) => db.contacts.createAlias( $_aliasNameGenerator(db.messages.senderId, db.contacts.userId)); @@ -7367,9 +7672,6 @@ class $$MessagesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnFilters(column)); - ColumnFilters get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnFilters(column)); @@ -7402,6 +7704,26 @@ class $$MessagesTableFilterComposer ColumnFilters get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableFilterComposer get senderId { final $$ContactsTableFilterComposer composer = $composerBuilder( composer: this, @@ -7535,9 +7857,6 @@ class $$MessagesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnOrderings(column)); @@ -7570,6 +7889,26 @@ class $$MessagesTableOrderingComposer ColumnOrderings get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableOrderingComposer get senderId { final $$ContactsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -7640,9 +7979,6 @@ class $$MessagesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get groupId => - $composableBuilder(column: $table.groupId, builder: (column) => column); - GeneratedColumn get messageId => $composableBuilder(column: $table.messageId, builder: (column) => column); @@ -7673,6 +8009,26 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => column); + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableAnnotationComposer get senderId { final $$ContactsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -7809,7 +8165,8 @@ class $$MessagesTableTableManager extends RootTableManager< (Message, $$MessagesTableReferences), Message, PrefetchHooks Function( - {bool senderId, + {bool groupId, + bool senderId, bool mediaId, bool quotesMessageId, bool messageHistoriesRefs, @@ -7898,7 +8255,8 @@ class $$MessagesTableTableManager extends RootTableManager< (e.readTable(table), $$MessagesTableReferences(db, table, e))) .toList(), prefetchHooksCallback: ( - {senderId = false, + {groupId = false, + senderId = false, mediaId = false, quotesMessageId = false, messageHistoriesRefs = false, @@ -7924,6 +8282,16 @@ class $$MessagesTableTableManager extends RootTableManager< dynamic, dynamic, dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$MessagesTableReferences._groupIdTable(db), + referencedColumn: + $$MessagesTableReferences._groupIdTable(db).groupId, + ) as T; + } if (senderId) { state = state.withJoin( currentTable: table, @@ -8017,7 +8385,8 @@ typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< (Message, $$MessagesTableReferences), Message, PrefetchHooks Function( - {bool senderId, + {bool groupId, + bool senderId, bool mediaId, bool quotesMessageId, bool messageHistoriesRefs, @@ -8606,203 +8975,6 @@ typedef $$ReactionsTableProcessedTableManager = ProcessedTableManager< (Reaction, $$ReactionsTableReferences), Reaction, PrefetchHooks Function({bool messageId, bool senderId})>; -typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ - Value groupId, - required bool isGroupAdmin, - required bool isGroupOfTwo, - Value pinned, - Value archived, - Value lastMessageExchange, - Value createdAt, - Value rowid, -}); -typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ - Value groupId, - Value isGroupAdmin, - Value isGroupOfTwo, - Value pinned, - Value archived, - Value lastMessageExchange, - Value createdAt, - Value rowid, -}); - -class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { - $$GroupsTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get isGroupAdmin => $composableBuilder( - column: $table.isGroupAdmin, builder: (column) => ColumnFilters(column)); - - ColumnFilters get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, builder: (column) => ColumnFilters(column)); - - ColumnFilters get pinned => $composableBuilder( - column: $table.pinned, builder: (column) => ColumnFilters(column)); - - ColumnFilters get archived => $composableBuilder( - column: $table.archived, builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); -} - -class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { - $$GroupsTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get isGroupAdmin => $composableBuilder( - column: $table.isGroupAdmin, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get pinned => $composableBuilder( - column: $table.pinned, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get archived => $composableBuilder( - column: $table.archived, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); -} - -class $$GroupsTableAnnotationComposer - extends Composer<_$TwonlyDB, $GroupsTable> { - $$GroupsTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get groupId => - $composableBuilder(column: $table.groupId, builder: (column) => column); - - GeneratedColumn get isGroupAdmin => $composableBuilder( - column: $table.isGroupAdmin, builder: (column) => column); - - GeneratedColumn get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, builder: (column) => column); - - GeneratedColumn get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => column); - - GeneratedColumn get archived => - $composableBuilder(column: $table.archived, builder: (column) => column); - - GeneratedColumn get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, builder: (column) => column); - - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); -} - -class $$GroupsTableTableManager extends RootTableManager< - _$TwonlyDB, - $GroupsTable, - Group, - $$GroupsTableFilterComposer, - $$GroupsTableOrderingComposer, - $$GroupsTableAnnotationComposer, - $$GroupsTableCreateCompanionBuilder, - $$GroupsTableUpdateCompanionBuilder, - (Group, BaseReferences<_$TwonlyDB, $GroupsTable, Group>), - Group, - PrefetchHooks Function()> { - $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) - : super(TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $$GroupsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$GroupsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$GroupsTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value groupId = const Value.absent(), - Value isGroupAdmin = const Value.absent(), - Value isGroupOfTwo = const Value.absent(), - Value pinned = const Value.absent(), - Value archived = const Value.absent(), - Value lastMessageExchange = const Value.absent(), - Value createdAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - GroupsCompanion( - groupId: groupId, - isGroupAdmin: isGroupAdmin, - isGroupOfTwo: isGroupOfTwo, - pinned: pinned, - archived: archived, - lastMessageExchange: lastMessageExchange, - createdAt: createdAt, - rowid: rowid, - ), - createCompanionCallback: ({ - Value groupId = const Value.absent(), - required bool isGroupAdmin, - required bool isGroupOfTwo, - Value pinned = const Value.absent(), - Value archived = const Value.absent(), - Value lastMessageExchange = const Value.absent(), - Value createdAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - GroupsCompanion.insert( - groupId: groupId, - isGroupAdmin: isGroupAdmin, - isGroupOfTwo: isGroupOfTwo, - pinned: pinned, - archived: archived, - lastMessageExchange: lastMessageExchange, - createdAt: createdAt, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), - prefetchHooksCallback: null, - )); -} - -typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDB, - $GroupsTable, - Group, - $$GroupsTableFilterComposer, - $$GroupsTableOrderingComposer, - $$GroupsTableAnnotationComposer, - $$GroupsTableCreateCompanionBuilder, - $$GroupsTableUpdateCompanionBuilder, - (Group, BaseReferences<_$TwonlyDB, $GroupsTable, Group>), - Group, - PrefetchHooks Function()>; typedef $$GroupMembersTableCreateCompanionBuilder = GroupMembersCompanion Function({ required String groupId, @@ -10636,6 +10808,8 @@ class $TwonlyDBManager { $TwonlyDBManager(this._db); $$ContactsTableTableManager get contacts => $$ContactsTableTableManager(_db, _db.contacts); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db, _db.groups); $$MediaFilesTableTableManager get mediaFiles => $$MediaFilesTableTableManager(_db, _db.mediaFiles); $$MessagesTableTableManager get messages => @@ -10644,8 +10818,6 @@ class $TwonlyDBManager { $$MessageHistoriesTableTableManager(_db, _db.messageHistories); $$ReactionsTableTableManager get reactions => $$ReactionsTableTableManager(_db, _db.reactions); - $$GroupsTableTableManager get groups => - $$GroupsTableTableManager(_db, _db.groups); $$GroupMembersTableTableManager get groupMembers => $$GroupMembersTableTableManager(_db, _db.groupMembers); $$ReceiptsTableTableManager get receipts => diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index c5b119a..af05082 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -1,15 +1,16 @@ import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' + hide Message; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; @@ -57,44 +58,41 @@ ClientToServer createClientToServerFromApplicationData( } Future deleteContact(int contactId) async { - await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId); await twonlyDB.signalDao.deleteAllByContactId(contactId); await deleteSessionWithTarget(contactId); await twonlyDB.contactsDao.deleteContactByUserId(contactId); } Future rejectUser(int contactId) async { - await encryptAndSendMessageAsync( - null, + await sendCipherText( contactId, - MessageJson( - kind: MessageKind.rejectRequest, - timestamp: DateTime.now(), - content: MessageContent(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REJECT, + ), ), ); } -Future handleMediaError(Message message) async { - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - mediaRetransmissionState: Value( - MediaRetransmitting.requested, +Future handleMediaError(MediaFile media) async { + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.reuploadRequested), + ), + ); + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); + if (messages.length != 1) return; + final message = messages.first; + if (message.senderId == null) return; + await sendCipherText( + message.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, + targetMessageId: message.messageId, ), ), ); - if (message.messageOtherId != null) { - await encryptAndSendMessageAsync( - null, - message.contactId, - MessageJson( - kind: MessageKind.receiveMediaError, - timestamp: DateTime.now(), - content: MessageContent(), - messageReceiverId: message.messageOtherId, - ), - ); - } } diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 929dc0b..1f98386 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart' as my; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -38,17 +38,15 @@ Future syncFlameCounters() async { // only sync when flame counter is higher than three days if (flameCounter < 1 && bestFriend.userId != contact.userId) continue; - await encryptAndSendMessageAsync( - null, + await sendCipherText( contact.userId, - my.MessageJson( - kind: MessageKind.flameSync, - content: my.FlameSyncContent( - flameCounter: flameCounter, - lastFlameCounterChange: contact.lastFlameCounterChange!, + EncryptedContent( + flameSync: EncryptedContent_FlameSync( + flameCounter: Int64(flameCounter), + lastFlameCounterChange: + Int64(contact.lastFlameCounterChange!.millisecondsSinceEpoch), bestFriend: contact.userId == bestFriend.userId, ), - timestamp: DateTime.now(), ), ); diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index fde1e92..a675212 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -7,9 +7,11 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.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/model/protobuf/client/generated/push_notification.pbenum.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart' show gMediaShowInfinite; @@ -75,7 +77,10 @@ Future handlePushData(String pushDataB64) async { ); } else if (foundPushUser != null) { if (pushNotification.hasMessageId()) { - if (pushNotification.messageId <= foundPushUser.lastMessageId) { + if (isUUIDNewer( + foundPushUser.lastMessageId, + pushNotification.messageId, + )) { Log.info( 'Got a push notification for a message which was already opened.', ); From f5cbcf154b09a9b08fd2ebfd34453e9fa04f66d1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 22 Oct 2025 00:01:55 +0200 Subject: [PATCH 05/76] fixing issues with media_download --- lib/main.dart | 4 +- lib/src/database/daos/mediafiles.dao.dart | 6 + lib/src/database/daos/messages.dao.dart | 6 +- lib/src/database/tables/mediafiles.table.dart | 11 +- lib/src/database/twonly.db.g.dart | 97 ++--- lib/src/services/api/media_download.dart | 403 +++++------------- .../media.server_messages.dart | 19 +- lib/src/services/mediafile.service.dart | 123 +++++- lib/src/services/thumbnail.service.dart | 52 +-- .../create_backup.twonly_safe.dart | 13 +- .../twonly_safe/restore.twonly_safe.dart | 41 +- .../views/camera/share_image_editor_view.dart | 4 + 12 files changed, 332 insertions(+), 447 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1d1ba15..8751c01 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -45,6 +45,8 @@ void main() async { apiService = ApiService(); twonlyDB = TwonlyDB(); + await initFileDownloader(); + // await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); @@ -56,8 +58,6 @@ void main() async { // unawaited(performTwonlySafeBackup()); - await initFileDownloader(); - runApp( MultiProvider( providers: [ diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 057f59e..a6832f7 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -51,4 +51,10 @@ class MediaFilesDao extends DatabaseAccessor ), ); } + + Future> getAllMediaFilesPendingDownload() async { + return (select(mediaFiles) + ..where((t) => t.downloadState.equals(DownloadState.pending.name))) + .get(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 963ef02..ed29987 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -177,7 +177,11 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { if (msg.mediaId != null) { await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) .go(); - await removeMediaFile(msg.mediaId!); + + final mediaService = await MediaFileService.fromMediaId(msg.mediaId!); + if (mediaService != null) { + mediaService.fullMediaRemoval(); + } } await (delete(messageHistories) ..where((t) => t.messageId.equals(messageId))) diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 34990a4..8b44793 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -16,7 +16,13 @@ enum UploadState { receiverNotified, } -enum DownloadState { pending, downloading, reuploadRequested } +enum DownloadState { + pending, + downloading, + downloaded, + ready, + reuploadRequested +} @DataClassName('MediaFile') class MediaFiles extends Table { @@ -31,8 +37,7 @@ class MediaFiles extends Table { BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); - BoolColumn get storedByContact => - boolean().withDefault(const Constant(false))(); + BoolColumn get stored => boolean().withDefault(const Constant(false))(); TextColumn get reuploadRequestedBy => text().map(IntListTypeConverter()).nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 55b6fe6..d69d580 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1473,15 +1473,14 @@ class $MediaFilesTable extends MediaFiles defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("reopen_by_contact" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _storedByContactMeta = - const VerificationMeta('storedByContact'); + static const VerificationMeta _storedMeta = const VerificationMeta('stored'); @override - late final GeneratedColumn storedByContact = GeneratedColumn( - 'stored_by_contact', aliasedName, false, + late final GeneratedColumn stored = GeneratedColumn( + 'stored', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("stored_by_contact" IN (0, 1))'), + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), defaultValue: const Constant(false)); @override late final GeneratedColumnWithTypeConverter?, String> @@ -1536,7 +1535,7 @@ class $MediaFilesTable extends MediaFiles downloadState, requiresAuthentication, reopenByContact, - storedByContact, + stored, reuploadRequestedBy, displayLimitInMilliseconds, downloadToken, @@ -1573,11 +1572,9 @@ class $MediaFilesTable extends MediaFiles reopenByContact.isAcceptableOrUnknown( data['reopen_by_contact']!, _reopenByContactMeta)); } - if (data.containsKey('stored_by_contact')) { - context.handle( - _storedByContactMeta, - storedByContact.isAcceptableOrUnknown( - data['stored_by_contact']!, _storedByContactMeta)); + if (data.containsKey('stored')) { + context.handle(_storedMeta, + stored.isAcceptableOrUnknown(data['stored']!, _storedMeta)); } if (data.containsKey('display_limit_in_milliseconds')) { context.handle( @@ -1638,8 +1635,8 @@ class $MediaFilesTable extends MediaFiles data['${effectivePrefix}requires_authentication'])!, reopenByContact: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, - storedByContact: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!, + stored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}reupload_requested_by'])), @@ -1690,7 +1687,7 @@ class MediaFile extends DataClass implements Insertable { final DownloadState? downloadState; final bool requiresAuthentication; final bool reopenByContact; - final bool storedByContact; + final bool stored; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; final Uint8List? downloadToken; @@ -1705,7 +1702,7 @@ class MediaFile extends DataClass implements Insertable { this.downloadState, required this.requiresAuthentication, required this.reopenByContact, - required this.storedByContact, + required this.stored, this.reuploadRequestedBy, this.displayLimitInMilliseconds, this.downloadToken, @@ -1731,7 +1728,7 @@ class MediaFile extends DataClass implements Insertable { } map['requires_authentication'] = Variable(requiresAuthentication); map['reopen_by_contact'] = Variable(reopenByContact); - map['stored_by_contact'] = Variable(storedByContact); + map['stored'] = Variable(stored); if (!nullToAbsent || reuploadRequestedBy != null) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -1769,7 +1766,7 @@ class MediaFile extends DataClass implements Insertable { : Value(downloadState), requiresAuthentication: Value(requiresAuthentication), reopenByContact: Value(reopenByContact), - storedByContact: Value(storedByContact), + stored: Value(stored), reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent ? const Value.absent() : Value(reuploadRequestedBy), @@ -1807,7 +1804,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: serializer.fromJson(json['requiresAuthentication']), reopenByContact: serializer.fromJson(json['reopenByContact']), - storedByContact: serializer.fromJson(json['storedByContact']), + stored: serializer.fromJson(json['stored']), reuploadRequestedBy: serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: @@ -1832,7 +1829,7 @@ class MediaFile extends DataClass implements Insertable { $MediaFilesTable.$converterdownloadStaten.toJson(downloadState)), 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'reopenByContact': serializer.toJson(reopenByContact), - 'storedByContact': serializer.toJson(storedByContact), + 'stored': serializer.toJson(stored), 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), @@ -1851,7 +1848,7 @@ class MediaFile extends DataClass implements Insertable { Value downloadState = const Value.absent(), bool? requiresAuthentication, bool? reopenByContact, - bool? storedByContact, + bool? stored, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -1868,7 +1865,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, - storedByContact: storedByContact ?? this.storedByContact, + stored: stored ?? this.stored, reuploadRequestedBy: reuploadRequestedBy.present ? reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -1901,9 +1898,7 @@ class MediaFile extends DataClass implements Insertable { reopenByContact: data.reopenByContact.present ? data.reopenByContact.value : this.reopenByContact, - storedByContact: data.storedByContact.present - ? data.storedByContact.value - : this.storedByContact, + stored: data.stored.present ? data.stored.value : this.stored, reuploadRequestedBy: data.reuploadRequestedBy.present ? data.reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -1935,7 +1930,7 @@ class MediaFile extends DataClass implements Insertable { ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') - ..write('storedByContact: $storedByContact, ') + ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') @@ -1955,7 +1950,7 @@ class MediaFile extends DataClass implements Insertable { downloadState, requiresAuthentication, reopenByContact, - storedByContact, + stored, reuploadRequestedBy, displayLimitInMilliseconds, $driftBlobEquality.hash(downloadToken), @@ -1973,7 +1968,7 @@ class MediaFile extends DataClass implements Insertable { other.downloadState == this.downloadState && other.requiresAuthentication == this.requiresAuthentication && other.reopenByContact == this.reopenByContact && - other.storedByContact == this.storedByContact && + other.stored == this.stored && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && @@ -1991,7 +1986,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value downloadState; final Value requiresAuthentication; final Value reopenByContact; - final Value storedByContact; + final Value stored; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; final Value downloadToken; @@ -2007,7 +2002,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.downloadState = const Value.absent(), this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), - this.storedByContact = const Value.absent(), + this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), @@ -2024,7 +2019,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.downloadState = const Value.absent(), required bool requiresAuthentication, this.reopenByContact = const Value.absent(), - this.storedByContact = const Value.absent(), + this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), @@ -2042,7 +2037,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? downloadState, Expression? requiresAuthentication, Expression? reopenByContact, - Expression? storedByContact, + Expression? stored, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, Expression? downloadToken, @@ -2060,7 +2055,7 @@ class MediaFilesCompanion extends UpdateCompanion { if (requiresAuthentication != null) 'requires_authentication': requiresAuthentication, if (reopenByContact != null) 'reopen_by_contact': reopenByContact, - if (storedByContact != null) 'stored_by_contact': storedByContact, + if (stored != null) 'stored': stored, if (reuploadRequestedBy != null) 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) @@ -2081,7 +2076,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? downloadState, Value? requiresAuthentication, Value? reopenByContact, - Value? storedByContact, + Value? stored, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, Value? downloadToken, @@ -2098,7 +2093,7 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication: requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, - storedByContact: storedByContact ?? this.storedByContact, + stored: stored ?? this.stored, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, @@ -2136,8 +2131,8 @@ class MediaFilesCompanion extends UpdateCompanion { if (reopenByContact.present) { map['reopen_by_contact'] = Variable(reopenByContact.value); } - if (storedByContact.present) { - map['stored_by_contact'] = Variable(storedByContact.value); + if (stored.present) { + map['stored'] = Variable(stored.value); } if (reuploadRequestedBy.present) { map['reupload_requested_by'] = Variable($MediaFilesTable @@ -2178,7 +2173,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') - ..write('storedByContact: $storedByContact, ') + ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') @@ -7107,7 +7102,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value downloadState, required bool requiresAuthentication, Value reopenByContact, - Value storedByContact, + Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, @@ -7124,7 +7119,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value downloadState, Value requiresAuthentication, Value reopenByContact, - Value storedByContact, + Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, @@ -7190,9 +7185,8 @@ class $$MediaFilesTableFilterComposer column: $table.reopenByContact, builder: (column) => ColumnFilters(column)); - ColumnFilters get storedByContact => $composableBuilder( - column: $table.storedByContact, - builder: (column) => ColumnFilters(column)); + ColumnFilters get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnFilters(column)); ColumnWithTypeConverterFilters?, List, String> get reuploadRequestedBy => $composableBuilder( @@ -7271,9 +7265,8 @@ class $$MediaFilesTableOrderingComposer column: $table.reopenByContact, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get storedByContact => $composableBuilder( - column: $table.storedByContact, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnOrderings(column)); ColumnOrderings get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, @@ -7332,8 +7325,8 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get reopenByContact => $composableBuilder( column: $table.reopenByContact, builder: (column) => column); - GeneratedColumn get storedByContact => $composableBuilder( - column: $table.storedByContact, builder: (column) => column); + GeneratedColumn get stored => + $composableBuilder(column: $table.stored, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get reuploadRequestedBy => $composableBuilder( @@ -7408,7 +7401,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value downloadState = const Value.absent(), Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), - Value storedByContact = const Value.absent(), + Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -7425,7 +7418,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< downloadState: downloadState, requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, - storedByContact: storedByContact, + stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, @@ -7442,7 +7435,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value downloadState = const Value.absent(), required bool requiresAuthentication, Value reopenByContact = const Value.absent(), - Value storedByContact = const Value.absent(), + Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -7459,7 +7452,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< downloadState: downloadState, requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, - storedByContact: storedByContact, + stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart index 61887ed..67bc7f5 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/media_download.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; @@ -10,24 +8,23 @@ import 'package:drift/drift.dart'; import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; Future tryDownloadAllMediaFiles({bool force = false}) async { // This is called when WebSocket is newly connected, so allow all downloads to be restarted. - final messages = - await twonlyDB.messagesDao.getAllMessagesPendingDownloading(); + final mediaFiles = + await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload(); - for (final message in messages) { - await startDownloadMedia(message, force); + for (final mediaFile in mediaFiles) { + await startDownloadMedia(mediaFile, force); } } @@ -44,7 +41,7 @@ Map> defaultAutoDownloadOptions = { ], }; -Future isAllowedToDownload(bool isVideo) async { +Future isAllowedToDownload({required bool isVideo}) async { final connectivityResult = await Connectivity().checkConnectivity(); final user = await getUser(); @@ -76,7 +73,7 @@ Future isAllowedToDownload(bool isVideo) async { } Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { - final messageId = int.parse(update.task.taskId.replaceAll('download_', '')); + final mediaId = update.task.taskId.replaceAll('download_', ''); var failed = false; if (update.status == TaskStatus.failed || @@ -93,83 +90,45 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { ); } } else { - Log.info('Got ${update.status} for $messageId'); + Log.info('Got ${update.status} for $mediaId'); return; } - await handleDownloadStatusUpdateInternal(messageId, failed); -} -Future handleDownloadStatusUpdateInternal( - int messageId, - bool failed, -) async { if (failed) { - Log.error('Download failed for $messageId'); - final message = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (message != null && message.downloadState != DownloadState.downloaded) { - await handleMediaError(message); - } + await requestMediaReupload(mediaId); } else { - Log.info('Download was successfully for $messageId'); - await handleEncryptedFile(messageId); + await handleEncryptedFile(mediaId); } } Mutex protectDownload = Mutex(); Future startDownloadMedia(MediaFile media, bool force) async { - Log.info( - 'Download blocked for ${message.messageId} because of network state.', - ); - if (message.contentJson == null) { - Log.error('Content of ${message.messageId} not found.'); - await handleMediaError(message); + final mediaService = await MediaFileService.fromMedia(media); + + if (mediaService.encryptedPath.existsSync()) { + await handleEncryptedFile(media.mediaId); return; } - final content = MessageContent.fromJson( - message.kind, - jsonDecode(message.contentJson!) as Map, - ); - - if (content is! MediaMessageContent) { - Log.error('Content of ${message.messageId} is not media file.'); - await handleMediaError(message); - return; - } - - if (content.downloadToken == null) { - Log.error('Download token not defined for ${message.messageId}.'); - await handleMediaError(message); - return; - } - - if (!force && !await isAllowedToDownload(content.isVideo)) { + if (!force && + !await isAllowedToDownload(isVideo: media.type == MediaType.video)) { Log.warn( - 'Download blocked for ${message.messageId} because of network state.', + 'Download blocked for ${media.mediaId} because of network state.', ); return; } final isBlocked = await protectDownload.protect(() async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageId) - .getSingleOrNull(); + final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId); - if (msg == null) return true; - - if (msg.downloadState != DownloadState.pending) { - Log.error( - '${message.messageId} is already downloaded or is downloading.', - ); + if (msg == null || msg.downloadState != DownloadState.pending) { return true; } - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( + await twonlyDB.mediaFilesDao.updateMedia( + msg.mediaId, + const MediaFilesCompanion( downloadState: Value(DownloadState.downloading), ), ); @@ -178,11 +137,16 @@ Future startDownloadMedia(MediaFile media, bool force) async { }); if (isBlocked) { - Log.info('Download for ${message.messageId} already started.'); + Log.info('Download for ${media.mediaId} already started.'); return; } - final downloadToken = uint8ListToHex(content.downloadToken!); + if (media.downloadToken == null) { + Log.info('Download token for ${media.mediaId} not found.'); + return; + } + + final downloadToken = uint8ListToHex(media.downloadToken!); final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; @@ -190,20 +154,20 @@ Future startDownloadMedia(MediaFile media, bool force) async { try { final task = DownloadTask( url: apiUrl, - taskId: 'download_${message.messageId}', - directory: 'media/received/', - baseDirectory: BaseDirectory.applicationSupport, - filename: '${message.messageId}.encrypted', + taskId: 'download_${media.mediaId}', + directory: mediaService.encryptedPath.parent.path, + baseDirectory: BaseDirectory.root, + filename: basename(mediaService.encryptedPath.path), priority: 0, retries: 10, ); Log.info( - 'Got media file. Starting download: ${downloadToken.substring(0, 10)}', + 'Downloading ${media.mediaId} to ${mediaService.encryptedPath}', ); try { - await downloadFileFast(message.messageId, apiUrl); + await downloadFileFast(media, apiUrl, mediaService.encryptedPath); } catch (e) { Log.error('Fast download failed: $e'); await FileDownloader().enqueue(task); @@ -214,269 +178,114 @@ Future startDownloadMedia(MediaFile media, bool force) async { } Future downloadFileFast( - int messageId, + MediaFile media, String apiUrl, + File filePath, ) async { - final directoryPath = - '${(await getApplicationSupportDirectory()).path}/media/received/'; - final filename = '$messageId.encrypted'; - - final directory = Directory(directoryPath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - final filePath = '${directory.path}/$filename'; - final response = await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { - await File(filePath).writeAsBytes(response.bodyBytes); + await filePath.writeAsBytes(response.bodyBytes); Log.info('Fast Download successful: $filePath'); - await handleDownloadStatusUpdateInternal(messageId, false); + await handleEncryptedFile(media.mediaId); return; } else { - if (response.statusCode == 404 || response.statusCode == 403) { - await handleDownloadStatusUpdateInternal(messageId, true); + if (response.statusCode == 404 || + response.statusCode == 403 || + response.statusCode == 400) { + // Message was deleted from the server. Requesting it again from the sender to upload it again... + await requestMediaReupload(media.mediaId); return; } - // can be tried again + // Will be tried again using the slow method... throw Exception('Fast download failed with status: ${response.statusCode}'); } } -Future handleEncryptedFile(int messageId) async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (msg == null) { - Log.error('Not message for downloaded file found: $messageId'); +Future requestMediaReupload(String mediaId) async { + final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + if (messages.length != 1 || messages.first.senderId == null) { + Log.error( + 'Media file has none or more than one sender. That is not possible'); return; } - final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted'); + await sendCipherText( + messages.first.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, + targetMessageId: mediaId, + ), + ), + ); - if (encryptedBytes == null) { - Log.error('encrypted bytes are not found for ${msg.messageId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.reuploadRequested), + ), + ); +} + +Future handleEncryptedFile(String mediaId) async { + final mediaService = await MediaFileService.fromMediaId(mediaId); + if (mediaService == null) { + Log.error('Media file $mediaId not found in database.'); return; } - final content = - MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map); + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.downloaded), + ), + ); + + late Uint8List encryptedBytes; + try { + encryptedBytes = await mediaService.encryptedPath.readAsBytes(); + } catch (e) { + Log.error('Could not read encrypted media file: $mediaId. $e'); + await requestMediaReupload(mediaId); + return; + } try { final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); + final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!); final secretBox = SecretBox( encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), + nonce: mediaService.mediaFile.encryptionNonce!, + mac: Mac(mediaService.mediaFile.encryptionMac!), ); - // try { final plaintextBytes = await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } + final rawMediaBytes = Uint8List.fromList(plaintextBytes); - await writeMediaFile(msg.messageId, 'png', imageBytes); - // } catch (e) { - // Log.error( - // "could not decrypt the media file in the second try. reporting error to user: $e"); - // handleMediaError(msg); - // return; - // } + await mediaService.tempPath.writeAsBytes(rawMediaBytes); } catch (e) { - Log.error('$e'); - - /// legacy support - final chacha20 = Xchacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); - - final secretBox = SecretBox( - encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), + Log.error( + 'Could not decrypt the media file. Requesting a new upload.', ); - - try { - final plaintextBytes = - await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } - - await writeMediaFile(msg.messageId, 'png', imageBytes); - } catch (e) { - Log.error( - 'could not decrypt the media file in the second try. reporting error to user: $e', - ); - await handleMediaError(msg); - return; - } + await requestMediaReupload(mediaId); + return; } - await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId, - const MessagesCompanion(downloadState: Value(DownloadState.downloaded)), + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.ready), + ), ); - Log.info('Download and decryption of ${msg.messageId} was successful'); + Log.info('Decryption of $mediaId was successful'); - await deleteMediaFile(msg.messageId, 'encrypted'); + mediaService.encryptedPath.deleteSync(); - unawaited(apiService.downloadDone(content.downloadToken!)); + unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!)); } - -Future getImageBytes(int mediaId) async { - return readMediaFile(mediaId, 'png'); -} - -Future getVideoPath(int mediaId) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - return File('$basePath.mp4'); -} - -/// --- helper functions --- - -Future readMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - Log.info('Reading: $file'); - if (!file.existsSync()) { - return null; - } - return file.readAsBytes(); -} - -Future existsMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - return file.existsSync(); -} - -Future writeMediaFile(int mediaId, String type, Uint8List data) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); -} - -Future deleteMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - try { - if (file.existsSync()) { - await file.delete(); - } - } catch (e) { - Log.error('Error deleting: $e'); - } -} - -Future purgeReceivedMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'received')); - await purgeMediaFiles(directory); -} - -Future purgeMediaFiles(Directory directory) async { - // Check if the directory exists - if (directory.existsSync()) { - // List all files in the directory - final files = directory.listSync(); - - // Iterate over each file - for (final file in files) { - // Get the filename - final filename = file.uri.pathSegments.last; - - // Use a regular expression to extract the integer part - final match = RegExp(r'(\d+)').firstMatch(filename); - if (match != null) { - // Parse the integer and add it to the list - final fileId = int.parse(match.group(0)!); - - try { - if (directory.path.endsWith('send')) { - final messages = - await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId); - var canBeDeleted = true; - - for (final message in messages) { - try { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - - final oneDayAgo = - DateTime.now().subtract(const Duration(days: 1)); - final twoDaysAgo = - DateTime.now().subtract(const Duration(days: 1)); - - if ((message.openedAt == null || - oneDayAgo.isBefore(message.openedAt!)) && - !message.errorWhileSending) { - canBeDeleted = false; - } else if (message.mediaStored) { - if (!file.path.contains('.original.') && - !file.path.contains('.encrypted')) { - canBeDeleted = false; - } - } - - /// In case the image is not yet opened but successfully uploaded - /// to the server preserve the image for two days in case of an receiving error will happen - /// and then delete them as well. - if (message.acknowledgeByServer && - twoDaysAgo.isAfter(message.sendAt)) { - // Preserve images which can be stored by the other person... - if (content.maxShowTime != gMediaShowInfinite) { - canBeDeleted = true; - } - // Encrypted or upload data can be removed when acknowledgeByServer - if (file.path.contains('.upload') || - file.path.contains('.encrypted')) { - canBeDeleted = true; - } - } - } catch (e) { - Log.error(e); - } - } - if (canBeDeleted) { - Log.info('purged media file ${file.path} '); - file.deleteSync(); - } - } else { - final message = await twonlyDB.messagesDao - .getMessageByMessageId(fileId) - .getSingleOrNull(); - if ((message == null) || - (message.openedAt != null && - !message.mediaStored && - message.acknowledgeByServer) || - message.errorWhileSending) { - file.deleteSync(); - } - } - } catch (e) { - Log.error('$e'); - } - } - } - } -} - -// /data/user/0/eu.twonly.testing/files/media/received/27.encrypted -// /data/user/0/eu.twonly.testing/app_flutter/data/user/0/eu.twonly.testing/files/media/received/27.encrypted diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 741c774..bef08fa 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -8,7 +7,6 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/mediafile.service.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleMedia( @@ -33,7 +31,10 @@ Future handleMedia( } // in case there was already a downloaded file delete it... - await removeMediaFile(message.mediaId!); + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService != null) { + mediaService.tempPath.deleteSync(); + } await twonlyDB.mediaFilesDao.updateMedia( message.mediaId!, @@ -81,6 +82,7 @@ Future handleMedia( ); if (mediaFile == null) { + Log.error('Could not insert media file into database'); return; } @@ -121,7 +123,11 @@ Future handleMediaUpdate( if (message == null || message.mediaId == null) return; final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); - if (mediaFile == null) return; + if (mediaFile == null) { + Log.info( + 'Got media file update, but media file was not found ${message.mediaId}'); + return; + } switch (mediaUpdate.type) { case EncryptedContent_MediaUpdate_Type.REOPENED: @@ -137,11 +143,12 @@ Future handleMediaUpdate( await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, const MediaFilesCompanion( - storedByContact: Value(true), + stored: Value(true), ), ); - unawaited(createThumbnailForMediaFile(mediaFile)); + final mediaService = await MediaFileService.fromMedia(mediaFile); + unawaited(mediaService.createThumbnail()); case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: Log.info('Got media file decryption error ${mediaFile.mediaId}'); diff --git a/lib/src/services/mediafile.service.dart b/lib/src/services/mediafile.service.dart index eea8842..cc2b3f1 100644 --- a/lib/src/services/mediafile.service.dart +++ b/lib/src/services/mediafile.service.dart @@ -1,5 +1,124 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.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/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; -Future removeMediaFile(String mediaId) async { - Log.error('TODO removeMediaFile: $mediaId'); +class MediaFileService { + MediaFileService(this.mediaFile, {required this.applicationSupportDirectory}); + MediaFile mediaFile; + + final Directory applicationSupportDirectory; + + static Future fromMedia(MediaFile media) async { + return MediaFileService( + media, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + static Future fromMediaId(String mediaId) async { + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + if (mediaFile == null) return null; + return MediaFileService( + mediaFile, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + Future updateFromDB() async { + final updated = + await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); + if (updated != null) { + mediaFile = updated; + } + } + + Future createThumbnail() async { + if (!storedPath.existsSync()) { + Log.error('Could not create Thumbnail as stored media does not exists.'); + return; + } + switch (mediaFile.type) { + case MediaType.image: + await createThumbnailsForImage(storedPath, thumbnailPath); + case MediaType.video: + await createThumbnailsForVideo(storedPath, thumbnailPath); + case MediaType.gif: + Log.error('Thumbnail for .gif is not implemented yet'); + } + } + + void fullMediaRemoval() { + if (tempPath.existsSync()) { + tempPath.deleteSync(); + } + if (encryptedPath.existsSync()) { + encryptedPath.deleteSync(); + } + if (storedPath.existsSync()) { + storedPath.deleteSync(); + } + if (thumbnailPath.existsSync()) { + thumbnailPath.deleteSync(); + } + } + + Future storeMediaFile() async { + Log.info('Storing media file ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + stored: Value(true), + ), + ); + await tempPath.copy(storedPath.path); + await updateFromDB(); + } + + File _buildFilePath( + String directory, { + String namePrefix = '', + String extensionParam = '', + }) { + final mediaBaseDir = Directory(join( + applicationSupportDirectory.path, + 'mediafiles', + directory, + )); + if (!mediaBaseDir.existsSync()) { + mediaBaseDir.createSync(recursive: true); + } + var extension = extensionParam; + if (extension == '') { + switch (mediaFile.type) { + case MediaType.image: + extension = 'webp'; + case MediaType.video: + extension = 'mp4'; + case MediaType.gif: + extension = 'gif'; + } + } + return File( + join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), + ); + } + + File get tempPath => _buildFilePath('tmp'); + File get storedPath => _buildFilePath('stored'); + File get thumbnailPath => _buildFilePath( + 'stored', + namePrefix: '.thumbnail', + extensionParam: 'webp', + ); + File get encryptedPath => _buildFilePath( + 'tmp', + namePrefix: '.encrypted', + ); } diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/thumbnail.service.dart index a40d07c..ffaa999 100644 --- a/lib/src/services/thumbnail.service.dart +++ b/lib/src/services/thumbnail.service.dart @@ -1,26 +1,13 @@ import 'dart:io'; - import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:path/path.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; - -Future createThumbnailForMediaFile(MediaFile media) async { - - switch (media.type) { - case MediaType.image: - TODO - break; - default: - } - -} - -Future createThumbnailsForImage(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); +Future createThumbnailsForImage( + File sourceFile, + File destinationFile, +) async { + final fileExtension = sourceFile.path.split('.').last.toLowerCase(); if (fileExtension != 'png') { Log.error('Could not create thumbnail for image. $fileExtension != .png'); return; @@ -30,7 +17,7 @@ Future createThumbnailsForImage(File file) async { final imageBytesCompressed = await FlutterImageCompress.compressWithFile( minHeight: 800, minWidth: 450, - file.path, + sourceFile.path, format: CompressFormat.webp, quality: 50, ); @@ -39,16 +26,17 @@ Future createThumbnailsForImage(File file) async { Log.error('Could not compress the image'); return; } - - final thumbnailFile = getThumbnailPath(file); - await thumbnailFile.writeAsBytes(imageBytesCompressed); + await destinationFile.writeAsBytes(imageBytesCompressed); } catch (e) { Log.error('Could not compress the image got :$e'); } } -Future createThumbnailsForVideo(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); +Future createThumbnailsForVideo( + File sourceFile, + File destinationFile, +) async { + final fileExtension = sourceFile.path.split('.').last.toLowerCase(); if (fileExtension != 'mp4') { Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); return; @@ -56,8 +44,8 @@ Future createThumbnailsForVideo(File file) async { try { await VideoThumbnail.thumbnailFile( - video: file.path, - thumbnailPath: getThumbnailPath(file).path, + video: sourceFile.path, + thumbnailPath: destinationFile.path, maxWidth: 450, quality: 75, ); @@ -65,15 +53,3 @@ Future createThumbnailsForVideo(File file) async { Log.error('Could not create the video thumbnail: $e'); } } - -File getThumbnailPath(File file) { - final originalFileName = file.uri.pathSegments.last; - final fileNameWithoutExtension = originalFileName.split('.').first; - var fileExtension = originalFileName.split('.').last; - if (fileExtension == 'mp4') { - fileExtension = 'png'; - } - final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension'; - Directory(file.parent.path).createSync(); - return File(join(file.parent.path, newFileName)); -} diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 5a93816..b31cc35 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -13,7 +13,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; @@ -48,20 +48,19 @@ Future performTwonlySafeBackup({bool force = false}) async { final backupDir = Directory(join(baseDir, 'backup_twonly_safe/')); await backupDir.create(recursive: true); - final backupDatabaseFile = - File(join(backupDir.path, 'twonly_database.backup.sqlite')); + final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); final backupDatabaseFileCleaned = - File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite')); + File(join(backupDir.path, 'twonly.backup.cleaned.sqlite')); // copy database - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.copy(backupDatabaseFile.path); driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - final backupDB = TwonlyDatabase( + final backupDB = TwonlyDB( driftDatabase( - name: 'twonly_database.backup', + name: 'twonly.backup', native: DriftNativeOptions( databaseDirectory: () async { return backupDir; diff --git a/lib/src/services/twonly_safe/restore.twonly_safe.dart b/lib/src/services/twonly_safe/restore.twonly_safe.dart index 28a14c0..7669090 100644 --- a/lib/src/services/twonly_safe/restore.twonly_safe.dart +++ b/lib/src/services/twonly_safe/restore.twonly_safe.dart @@ -10,10 +10,8 @@ import 'package:http/http.dart' as http; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -90,44 +88,9 @@ Future handleBackupData( ); final baseDir = (await getApplicationSupportDirectory()).path; - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); - /// When restoring the last message ID must be increased otherwise - /// receivers would mark them as duplicates as they where already - /// send. - final database = TwonlyDatabase(); - var lastMessageSend = 0; - int? randomUserId; - - final contacts = await database.contactsDao.getAllNotBlockedContacts(); - for (final contact in contacts) { - randomUserId = contact.userId; - final days = DateTime.now().difference(contact.lastMessageExchange).inDays; - if (days < lastMessageSend) { - lastMessageSend = days; - } - } - - if (randomUserId != null) { - // for each day add 400 message ids - final dummyMessagesCounter = (lastMessageSend + 1) * 400; - Log.info( - 'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.', - ); - for (var i = 0; i < dummyMessagesCounter; i++) { - await database.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(randomUserId), - kind: const Value(MessageKind.ack), - acknowledgeByServer: const Value(true), - errorWhileSending: const Value(true), - ), - ); - } - await database.messagesDao.deleteAllMessagesByContactId(randomUserId); - } - const storage = FlutterSecureStorage(); final secureStorage = jsonDecode(backupContent.secureStorageJson); diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index acd8eaa..3653b9c 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -340,6 +340,10 @@ class _ShareImageEditorView extends State { Future getMergedImage() async { Uint8List? image; + + TODO: When changed then create a new mediaID!!!!!! + As storedMediaId would overwrite it.... + if (layers.length > 1 || widget.videoFilePath != null) { for (final x in layers) { x.showCustomButtons = false; From b2dc384465a7723ecdc01ce0f26dc7d883e7eae7 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 23 Oct 2025 00:35:28 +0200 Subject: [PATCH 06/76] starting with rewriting the upload process --- lib/app.dart | 2 +- lib/globals.dart | 5 + lib/main.dart | 5 +- lib/src/database/daos/messages.dao.dart | 2 +- lib/src/database/tables/mediafiles.table.dart | 22 +- lib/src/database/tables/messages.table.dart | 2 + lib/src/database/twonly.db.g.dart | 77 ++- lib/src/model/memory_item.model.dart | 4 +- lib/src/services/api.service.dart | 4 +- .../download.service.dart} | 4 +- .../mediafiles/media_background.service.dart | 50 ++ .../upload.service.dart} | 472 +++--------------- .../media.server_messages.dart | 6 +- lib/src/services/api/utils.dart | 11 +- .../mediafiles/compression.service.dart | 94 ++++ .../{ => mediafiles}/mediafile.service.dart | 69 ++- .../{ => mediafiles}/thumbnail.service.dart | 0 .../twonly_safe/common.twonly_safe.dart | 2 +- .../create_backup.twonly_safe.dart | 2 +- lib/src/utils/misc.dart | 39 +- lib/src/utils/storage.dart | 5 +- .../save_to_gallery.dart | 4 +- .../camera_preview_controller_view.dart | 28 +- .../views/camera/share_image_editor_view.dart | 253 ++++------ lib/src/views/camera/share_image_view.dart | 13 +- lib/src/views/chats/add_new_user.view.dart | 3 +- lib/src/views/chats/chat_list.view.dart | 2 +- .../chat_media_entry.dart | 3 +- lib/src/views/chats/media_viewer.view.dart | 2 +- lib/src/views/contact/contact.view.dart | 3 +- lib/src/views/memories/memories.view.dart | 4 +- .../memories/memories_photo_slider.view.dart | 5 +- lib/src/views/onboarding/register.view.dart | 2 + .../views/settings/data_and_storage.view.dart | 2 +- .../views/settings/help/contact_us.view.dart | 2 +- test/unit_test.dart | 2 +- 36 files changed, 557 insertions(+), 648 deletions(-) rename lib/src/services/api/{media_download.dart => mediafiles/download.service.dart} (98%) create mode 100644 lib/src/services/api/mediafiles/media_background.service.dart rename lib/src/services/api/{media_upload.dart => mediafiles/upload.service.dart} (55%) create mode 100644 lib/src/services/mediafiles/compression.service.dart rename lib/src/services/{ => mediafiles}/mediafile.service.dart (65%) rename lib/src/services/{ => mediafiles}/thumbnail.service.dart (100%) diff --git a/lib/app.dart b/lib/app.dart index 36c9c2d..96588e9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,7 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; diff --git a/lib/globals.dart b/lib/globals.dart index 809be94..d08f1fa 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,5 +1,6 @@ import 'package:camera/camera.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; late ApiService apiService; @@ -9,6 +10,10 @@ late TwonlyDB twonlyDB; List gCameras = []; +// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called, +// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart +late UserData gUser; + // The following global function can be called from anywhere to update // the UI when something changed. The callbacks will be set by // App widget. diff --git a/lib/main.dart b/lib/main.dart index 8751c01..431626c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,8 +10,8 @@ import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; @@ -28,6 +28,7 @@ void main() async { final user = await getUser(); if (user != null) { + gUser = user; if (user.isDemoUser) { await deleteLocalUserData(); } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index ed29987..07fd0a6 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/groups.table.dart'; 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/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; part 'messages.dao.g.dart'; diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 8b44793..8818f0a 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -10,10 +10,21 @@ enum MediaType { } enum UploadState { - pending, - readyToUpload, - uploadTaskStarted, - receiverNotified, + // Image/Video was taken. A database entry was created to track it... + initialized, + // Image was stored but not send + storedOnly, + // At this point the user is finished with editing, and the media file can be uploaded + compressing, + encrypting, + uploading, + backgroundUploadTaskStarted, + uploaded, + + uploadLimitReached, + // readyToUpload, + // uploadTaskStarted, + // receiverNotified, } enum DownloadState { @@ -33,7 +44,8 @@ class MediaFiles extends Table { TextColumn get uploadState => textEnum().nullable()(); TextColumn get downloadState => textEnum().nullable()(); - BoolColumn get requiresAuthentication => boolean()(); + BoolColumn get requiresAuthentication => + boolean().withDefault(const Constant(false))(); BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index b7ef767..08c28f0 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -18,6 +18,8 @@ class Messages extends Table { TextColumn get mediaId => text().nullable().references(MediaFiles, #mediaId)(); + BlobColumn get downloadToken => blob().nullable()(); + TextColumn get quotesMessageId => text().nullable().references(Messages, #messageId)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index d69d580..f3292b9 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1460,9 +1460,10 @@ class $MediaFilesTable extends MediaFiles late final GeneratedColumn requiresAuthentication = GeneratedColumn('requires_authentication', aliasedName, false, type: DriftSqlType.bool, - requiredDuringInsert: true, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("requires_authentication" IN (0, 1))')); + 'CHECK ("requires_authentication" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _reopenByContactMeta = const VerificationMeta('reopenByContact'); @override @@ -1563,8 +1564,6 @@ class $MediaFilesTable extends MediaFiles _requiresAuthenticationMeta, requiresAuthentication.isAcceptableOrUnknown( data['requires_authentication']!, _requiresAuthenticationMeta)); - } else if (isInserting) { - context.missing(_requiresAuthenticationMeta); } if (data.containsKey('reopen_by_contact')) { context.handle( @@ -2017,7 +2016,7 @@ class MediaFilesCompanion extends UpdateCompanion { required MediaType type, this.uploadState = const Value.absent(), this.downloadState = const Value.absent(), - required bool requiresAuthentication, + this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), @@ -2028,8 +2027,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : type = Value(type), - requiresAuthentication = Value(requiresAuthentication); + }) : type = Value(type); static Insertable custom({ Expression? mediaId, Expression? type, @@ -2233,6 +2231,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES media_files (media_id)')); + static const VerificationMeta _downloadTokenMeta = + const VerificationMeta('downloadToken'); + @override + late final GeneratedColumn downloadToken = + GeneratedColumn('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _quotesMessageIdMeta = const VerificationMeta('quotesMessageId'); @override @@ -2319,6 +2323,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { senderId, content, mediaId, + downloadToken, quotesMessageId, isDeletedFromSender, isEdited, @@ -2361,6 +2366,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_mediaIdMeta, mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); } + if (data.containsKey('download_token')) { + context.handle( + _downloadTokenMeta, + downloadToken.isAcceptableOrUnknown( + data['download_token']!, _downloadTokenMeta)); + } if (data.containsKey('quotes_message_id')) { context.handle( _quotesMessageIdMeta, @@ -2428,6 +2439,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), quotesMessageId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), isDeletedFromSender: attachedDatabase.typeMapping.read( @@ -2461,6 +2474,7 @@ class Message extends DataClass implements Insertable { final int? senderId; final String? content; final String? mediaId; + final Uint8List? downloadToken; final String? quotesMessageId; final bool isDeletedFromSender; final bool isEdited; @@ -2476,6 +2490,7 @@ class Message extends DataClass implements Insertable { this.senderId, this.content, this.mediaId, + this.downloadToken, this.quotesMessageId, required this.isDeletedFromSender, required this.isEdited, @@ -2499,6 +2514,9 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || mediaId != null) { map['media_id'] = Variable(mediaId); } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } if (!nullToAbsent || quotesMessageId != null) { map['quotes_message_id'] = Variable(quotesMessageId); } @@ -2530,6 +2548,9 @@ class Message extends DataClass implements Insertable { mediaId: mediaId == null && nullToAbsent ? const Value.absent() : Value(mediaId), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), quotesMessageId: quotesMessageId == null && nullToAbsent ? const Value.absent() : Value(quotesMessageId), @@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable { senderId: serializer.fromJson(json['senderId']), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), + downloadToken: serializer.fromJson(json['downloadToken']), quotesMessageId: serializer.fromJson(json['quotesMessageId']), isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), @@ -2578,6 +2600,7 @@ class Message extends DataClass implements Insertable { 'senderId': serializer.toJson(senderId), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), + 'downloadToken': serializer.toJson(downloadToken), 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), 'isEdited': serializer.toJson(isEdited), @@ -2596,6 +2619,7 @@ class Message extends DataClass implements Insertable { Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, bool? isEdited, @@ -2611,6 +2635,8 @@ class Message extends DataClass implements Insertable { senderId: senderId.present ? senderId.value : this.senderId, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, quotesMessageId: quotesMessageId.present ? quotesMessageId.value : this.quotesMessageId, @@ -2630,6 +2656,9 @@ class Message extends DataClass implements Insertable { senderId: data.senderId.present ? data.senderId.value : this.senderId, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, quotesMessageId: data.quotesMessageId.present ? data.quotesMessageId.value : this.quotesMessageId, @@ -2658,6 +2687,7 @@ class Message extends DataClass implements Insertable { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') @@ -2678,6 +2708,7 @@ class Message extends DataClass implements Insertable { senderId, content, mediaId, + $driftBlobEquality.hash(downloadToken), quotesMessageId, isDeletedFromSender, isEdited, @@ -2696,6 +2727,7 @@ class Message extends DataClass implements Insertable { other.senderId == this.senderId && other.content == this.content && other.mediaId == this.mediaId && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && other.isEdited == this.isEdited && @@ -2713,6 +2745,7 @@ class MessagesCompanion extends UpdateCompanion { final Value senderId; final Value content; final Value mediaId; + final Value downloadToken; final Value quotesMessageId; final Value isDeletedFromSender; final Value isEdited; @@ -2729,6 +2762,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), @@ -2746,6 +2780,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), @@ -2763,6 +2798,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? senderId, Expression? content, Expression? mediaId, + Expression? downloadToken, Expression? quotesMessageId, Expression? isDeletedFromSender, Expression? isEdited, @@ -2780,6 +2816,7 @@ class MessagesCompanion extends UpdateCompanion { if (senderId != null) 'sender_id': senderId, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, + if (downloadToken != null) 'download_token': downloadToken, if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, @@ -2800,6 +2837,7 @@ class MessagesCompanion extends UpdateCompanion { Value? senderId, Value? content, Value? mediaId, + Value? downloadToken, Value? quotesMessageId, Value? isDeletedFromSender, Value? isEdited, @@ -2816,6 +2854,7 @@ class MessagesCompanion extends UpdateCompanion { senderId: senderId ?? this.senderId, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, + downloadToken: downloadToken ?? this.downloadToken, quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, @@ -2847,6 +2886,9 @@ class MessagesCompanion extends UpdateCompanion { if (mediaId.present) { map['media_id'] = Variable(mediaId.value); } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } if (quotesMessageId.present) { map['quotes_message_id'] = Variable(quotesMessageId.value); } @@ -2888,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') @@ -7100,7 +7143,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ required MediaType type, Value uploadState, Value downloadState, - required bool requiresAuthentication, + Value requiresAuthentication, Value reopenByContact, Value stored, Value?> reuploadRequestedBy, @@ -7433,7 +7476,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< required MediaType type, Value uploadState = const Value.absent(), Value downloadState = const Value.absent(), - required bool requiresAuthentication, + Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), @@ -7513,6 +7556,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, Value isEdited, @@ -7530,6 +7574,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, Value isEdited, @@ -7671,6 +7716,9 @@ class $$MessagesTableFilterComposer ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + ColumnFilters get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnFilters(column)); @@ -7856,6 +7904,10 @@ class $$MessagesTableOrderingComposer ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( + column: $table.downloadToken, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnOrderings(column)); @@ -7978,6 +8030,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => column); + GeneratedColumn get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => column); @@ -8181,6 +8236,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), @@ -8198,6 +8254,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, @@ -8215,6 +8272,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), @@ -8232,6 +8290,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index eafe7a8..de2ce86 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -5,8 +5,8 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; class MemoryItem { MemoryItem({ diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 07b2dcd..64bf2b5 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -23,8 +23,8 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/mediafiles/download.service.dart similarity index 98% rename from lib/src/services/api/media_download.dart rename to lib/src/services/api/mediafiles/download.service.dart index 67bc7f5..4b1b738 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -12,10 +12,10 @@ 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/model/protobuf/client/generated/messages.pbserver.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future tryDownloadAllMediaFiles({bool force = false}) async { diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart new file mode 100644 index 0000000..dce9a1c --- /dev/null +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/foundation.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future initFileDownloader() async { + FileDownloader().updates.listen((update) async { + switch (update) { + case TaskStatusUpdate(): + if (update.task.taskId.contains('upload_')) { + await handleUploadStatusUpdate(update); + } + if (update.task.taskId.contains('download_')) { + await handleDownloadStatusUpdate(update); + } + if (update.task.taskId.contains('backup')) { + await handleBackupStatusUpdate(update); + } + case TaskProgressUpdate(): + Log.info( + 'Progress update for ${update.task} with progress ${update.progress}', + ); + } + }); + + await FileDownloader().start(); + + try { + var androidConfig = []; + if (kDebugMode) { + androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; + } + await FileDownloader().configure(androidConfig: androidConfig); + } catch (e) { + Log.error(e); + } + + if (kDebugMode) { + FileDownloader().configureNotification( + running: const TaskNotification( + 'Uploading/Downloading', + '{filename} ({progress}).', + ), + progressBar: true, + ); + } +} diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/mediafiles/upload.service.dart similarity index 55% rename from lib/src/services/api/media_upload.dart rename to lib/src/services/api/mediafiles/upload.service.dart index 364eb1f..d0f4350 100644 --- a/lib/src/services/api/media_upload.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; - import 'package:background_downloader/background_downloader.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -17,14 +16,13 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; @@ -33,76 +31,6 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:video_compress/video_compress.dart'; -Future isAllowedToSend() async { - final user = await getUser(); - if (user == null) return null; - if (user.subscriptionPlan == 'Free') { - var todaysImageCounter = user.todaysImageCounter; - if (user.lastImageSend != null && user.todaysImageCounter != null) { - if (isToday(user.lastImageSend!)) { - if (user.todaysImageCounter == 10) { - return ErrorCode.PlanLimitReached; - } - todaysImageCounter = user.todaysImageCounter! + 1; - } else { - todaysImageCounter = 1; - } - } else { - todaysImageCounter = 1; - } - await updateUserdata((user) { - user - ..lastImageSend = DateTime.now() - ..todaysImageCounter = todaysImageCounter; - return user; - }); - } - return null; -} - -Future initFileDownloader() async { - FileDownloader().updates.listen((update) async { - switch (update) { - case TaskStatusUpdate(): - if (update.task.taskId.contains('upload_')) { - await handleUploadStatusUpdate(update); - } - if (update.task.taskId.contains('download_')) { - await handleDownloadStatusUpdate(update); - } - if (update.task.taskId.contains('backup')) { - await handleBackupStatusUpdate(update); - } - case TaskProgressUpdate(): - Log.info( - 'Progress update for ${update.task} with progress ${update.progress}', - ); - } - }); - - await FileDownloader().start(); - - try { - var androidConfig = []; - if (kDebugMode) { - androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; - } - await FileDownloader().configure(androidConfig: androidConfig); - } catch (e) { - Log.error(e); - } - - if (kDebugMode) { - FileDownloader().configureNotification( - running: const TaskNotification( - 'Uploading/Downloading', - '{filename} ({progress}).', - ), - progressBar: true, - ); - } -} - /// States: /// when user recorded an video /// 1. Compress video @@ -115,43 +43,43 @@ Future initFileDownloader() async { /// Create a new entry in the database -Future checkForFailedUploads() async { - final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); - final mediaUploadIds = []; - for (final message in messages) { - if (mediaUploadIds.contains(message.mediaUploadId)) { - continue; - } - final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( - message.mediaUploadId!, - const MediaUploadsCompanion( - state: Value(UploadState.pending), - encryptionData: Value( - null, // start from scratch e.q. encrypt the files again if already happen - ), - ), - ); - if (affectedRows == 0) { - Log.error( - 'The media from message ${message.messageId} already deleted.', - ); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } else { - mediaUploadIds.add(message.mediaUploadId!); - } - } - if (messages.isNotEmpty) { - Log.error( - 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', - ); - } - return mediaUploadIds.isNotEmpty; // return true if there are affected -} +// Future checkForFailedUploads() async { +// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); +// final mediaUploadIds = []; +// for (final message in messages) { +// if (mediaUploadIds.contains(message.mediaUploadId)) { +// continue; +// } +// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( +// message.mediaUploadId!, +// const MediaUploadsCompanion( +// state: Value(UploadState.pending), +// encryptionData: Value( +// null, // start from scratch e.q. encrypt the files again if already happen +// ), +// ), +// ); +// if (affectedRows == 0) { +// Log.error( +// 'The media from message ${message.messageId} already deleted.', +// ); +// await twonlyDB.messagesDao.updateMessageByMessageId( +// message.messageId, +// const MessagesCompanion( +// errorWhileSending: Value(true), +// ), +// ); +// } else { +// mediaUploadIds.add(message.mediaUploadId!); +// } +// } +// if (messages.isNotEmpty) { +// Log.error( +// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', +// ); +// } +// return mediaUploadIds.isNotEmpty; // return true if there are affected +// } final lockingHandleMediaFile = Mutex(); Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { @@ -192,82 +120,25 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { } } -Future initMediaUpload() async { - return twonlyDB.mediaUploadsDao - .insertMediaUpload(const MediaUploadsCompanion()); -} - -Future addVideoToUpload(int mediaUploadId, File videoFilePath) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - await videoFilePath.copy('$basePath.original.mp4'); - return compressVideoIfExists(mediaUploadId); -} - -Future addOrModifyImageToUpload( - int mediaUploadId, - Uint8List imageBytes, +Future initializeMediaUpload( + MediaType type, + int? displayLimitInMilliseconds, ) async { - Uint8List imageBytesCompressed; + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionKey = await (await chacha20.newSecretKey()).extract(); + final encryptionNonce = chacha20.newNonce(); - final stopwatch = Stopwatch()..start(); - - Log.info('Raw images size in bytes: ${imageBytes.length}'); - - try { - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - // minHeight: 0, - // minWidth: 0, - imageBytes, - quality: 90, - ); - - if (imageBytesCompressed.length >= 1 * 1000 * 1000) { - // if the media file is over 2MB compress it with 60% - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - imageBytes, - quality: 60, - ); - } - await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed); - } catch (e) { - Log.error('$e'); - // as a fall back use the original image - await writeSendMediaFile(mediaUploadId, 'png', imageBytes); - imageBytesCompressed = imageBytes; - } - - stopwatch.stop(); - - Log.info( - 'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds', - ); - Log.info('Raw images size in bytes: ${imageBytesCompressed.length}'); - - // stopwatch.reset(); - // stopwatch.start(); - - // // var helper = MediaUploadHelper(); - // try { - // final webpBytes = - // await convertAndCompressImage(pngRawImageBytes: imageBytes); - // Log.info( - // 'Compression the image in rust took: ${stopwatch.elapsedMilliseconds} milliseconds'); - // Log.info("Raw images size in bytes using webp: ${webpBytes.length}"); - // } catch (e) { - // Log.error("$e"); - // } - - /// in case the media file was already encrypted of even uploaded - /// remove the data so it will be done again. - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - const MediaUploadsCompanion( - encryptionData: Value(null), + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + uploadState: const Value(UploadState.initialized), + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)), + encryptionNonce: Value(Uint8List.fromList(encryptionNonce)), + type: Value(type), ), ); - return imageBytesCompressed; + if (mediaFile == null) return null; + return MediaFileService.fromMedia(mediaFile); } Future handlePreProcessingState(MediaUpload media) async { @@ -304,7 +175,6 @@ Future encryptMediaFiles( final state = MediaEncryptionData(); final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKey = await (await chacha20.newSecretKey()).extract(); state ..encryptionKey = secretKey.bytes @@ -338,70 +208,37 @@ Future encryptMediaFiles( } Future finalizeUpload( - int mediaUploadId, - List contactIds, - bool isRealTwonly, - bool isVideo, - bool mirrorVideo, - int maxShowTime, + MediaFileService mediaService, + List groupIds, ) async { - final metadata = MediaUploadMetadata() - ..contactIds = contactIds - ..isRealTwonly = isRealTwonly - ..messageSendAt = DateTime.now() - ..isVideo = isVideo - ..maxShowTime = maxShowTime - ..mirrorVideo = mirrorVideo; + final messageIds = []; - final messageIds = []; - - for (final contactId in contactIds) { - final messageId = await twonlyDB.messagesDao.insertMessage( + for (final groupId in groupIds) { + final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( - contactId: Value(contactId), - kind: const Value(MessageKind.media), - sendAt: Value(metadata.messageSendAt), - downloadState: const Value(DownloadState.pending), - mediaUploadId: Value(mediaUploadId), - contentJson: Value( - jsonEncode( - MediaMessageContent( - maxShowTime: maxShowTime, - isRealTwonly: isRealTwonly, - isVideo: isVideo, - mirrorVideo: mirrorVideo, - ).toJson(), - ), + groupId: Value(groupId), + mediaId: Value(mediaService.mediaFile.mediaId), + ), + ); + if (message != null) { + messageIds.add(message); + // de-archive contact when sending a new message + await twonlyDB.groupsDao.updateGroup( + message.groupId, + const GroupsCompanion( + archived: Value(false), ), - ), - ); - // de-archive contact when sending a new message - await twonlyDB.contactsDao.updateContact( - contactId, - const ContactsCompanion( - archived: Value(false), - ), - ); - if (messageId != null) { - messageIds.add(messageId); + ); } else { Log.error('Error inserting media upload message in database.'); } } - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - messageIds: Value(messageIds), - metadata: Value(metadata), - ), - ); - - unawaited(handleNextMediaUploadSteps(mediaUploadId)); + unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId)); } final lockingHandleNextMediaUploadStep = Mutex(); -Future handleNextMediaUploadSteps(int mediaUploadId) async { +Future handleNextMediaUploadSteps(String mediaUploadId) async { await lockingHandleNextMediaUploadStep.protect(() async { final mediaUpload = await twonlyDB.mediaUploadsDao .getMediaUploadById(mediaUploadId) @@ -549,7 +386,7 @@ Future handleMediaUpload(MediaUpload media) async { continue; } - final downloadToken = createDownloadToken(); + final downloadToken = getRandomUint8List(32); final msg = MessageJson( kind: MessageKind.media, @@ -734,159 +571,14 @@ Future uploadFileFast( Log.info('Upload successful!'); await handleUploadSuccess(media); return; + } else if (response.statusCode == 429) { + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploadLimitReached), + ), + ); } else { Log.info('Upload failed with status: ${response.statusCode}'); } } - -Future compressVideoIfExists(int mediaUploadId) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final videoOriginalFile = File('$basePath.original.mp4'); - final videoCompressedFile = File('$basePath.mp4'); - - if (videoCompressedFile.existsSync()) { - // file is already compressed and exists - return true; - } - - if (!videoOriginalFile.existsSync()) { - // media upload does not have a video - return false; - } - - final stopwatch = Stopwatch()..start(); - - MediaInfo? mediaInfo; - try { - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res1280x720Quality, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 - ); - - Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); - - if (mediaInfo.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res960x540Quality, - includeAudio: true, - ); - } - } catch (e) { - Log.error('during video compression: $e'); - } - stopwatch.stop(); - Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); - - if (mediaInfo == null) { - Log.error('could not compress video.'); - // as a fall back use the non compressed version - await videoOriginalFile.copy(videoCompressedFile.path); - await videoOriginalFile.delete(); - } else { - await mediaInfo.file!.copy(videoCompressedFile.path); - await mediaInfo.file!.delete(); - } - return true; -} - -/// --- helper functions --- - -Future readSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (!file.existsSync()) { - throw Exception('$file not found'); - } - return file.readAsBytes(); -} - -Future writeSendMediaFile( - int mediaUploadId, - String type, - Uint8List data, -) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); - return file; -} - -Future deleteSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (file.existsSync()) { - await file.delete(); - } -} - -Future getMediaFilePath(dynamic mediaId, String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return join(mediaSendDir.path, '$mediaId'); -} - -Future getMediaBaseFilePath(String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return mediaSendDir.path; -} - -/// combines two utf8 list -Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) { - final combinedLength = 4 + list1.length + list2.length; - final combinedList = Uint8List(combinedLength); - ByteData.sublistView(combinedList).setInt32(0, list1.length); - combinedList - ..setRange(4, 4 + list1.length, list1) - ..setRange(4 + list1.length, combinedLength, list2); - return combinedList; -} - -List extractUint8Lists(Uint8List combinedList) { - final byteData = ByteData.sublistView(combinedList); - final sizeOfList1 = byteData.getInt32(0); - final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1); - final list2 = Uint8List.view( - combinedList.buffer, - 4 + sizeOfList1, - combinedList.lengthInBytes - 4 - sizeOfList1, - ); - return [list1, list2]; -} - -Future purgeSendMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'send')); - await purgeMediaFiles(directory); -} - -String uint8ListToHex(List bytes) { - return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); -} - -Uint8List hexToUint8List(String hex) => Uint8List.fromList( - List.generate( - hex.length ~/ 2, - (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), - ), - ); - -Uint8List createDownloadToken() { - final random = Random(); - - final token = Uint8List(32); - for (var j = 0; j < 32; j++) { - token[j] = random.nextInt(256); // Generate a random byte (0-255) - } - return token; -} diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index bef08fa..ef7e433 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -4,9 +4,9 @@ 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/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleMedia( @@ -157,7 +157,7 @@ Future handleMediaUpdate( await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, MediaFilesCompanion( - uploadState: const Value(UploadState.pending), + uploadState: const Value(UploadState.uploading), reuploadRequestedBy: Value(reuploadRequestedBy), ), ); diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index af05082..1bc0674 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -57,13 +57,7 @@ ClientToServer createClientToServerFromApplicationData( return ClientToServer()..v0 = v0; } -Future deleteContact(int contactId) async { - await twonlyDB.signalDao.deleteAllByContactId(contactId); - await deleteSessionWithTarget(contactId); - await twonlyDB.contactsDao.deleteContactByUserId(contactId); -} - -Future rejectUser(int contactId) async { +Future rejectAndDeleteContact(int contactId) async { await sendCipherText( contactId, EncryptedContent( @@ -72,6 +66,9 @@ Future rejectUser(int contactId) async { ), ), ); + await twonlyDB.signalDao.deleteAllByContactId(contactId); + await deleteSessionWithTarget(contactId); + await twonlyDB.contactsDao.deleteContactByUserId(contactId); } Future handleMediaError(MediaFile media) async { diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart new file mode 100644 index 0000000..8be5e6f --- /dev/null +++ b/lib/src/services/mediafiles/compression.service.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:video_compress/video_compress.dart'; + +Future compressImage( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + try { + var compressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 90, + ); + + if (compressedBytes == null) { + throw Exception( + 'Could not compress media file: $sourceFile. Sending original file.', + ); + } + + Log.info('Compressed images size in bytes: ${compressedBytes.length}'); + + if (compressedBytes.length >= 1 * 1000 * 1000) { + // if the media file is over 1MB compress it with 60% + final tmpCompressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 60, + ); + if (tmpCompressedBytes != null) { + Log.error( + 'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.', + ); + compressedBytes = tmpCompressedBytes; + } + } + + await destinationFile.writeAsBytes(compressedBytes); + } catch (e) { + Log.error('$e'); + sourceFile.copySync(destinationFile.path); + } + + stopwatch.stop(); + + Log.info( + 'Compression of the image took: ${stopwatch.elapsedMilliseconds} milliseconds.', + ); +} + +Future compressVideo( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + MediaInfo? mediaInfo; + try { + mediaInfo = await VideoCompress.compressVideo( + sourceFile.path, + quality: VideoQuality.Res1280x720Quality, + includeAudio: + true, // https://github.com/jonataslaw/VideoCompress/issues/184 + ); + + Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); + + if (mediaInfo.filesize! >= 30 * 1000 * 1000) { + // if the media file is over 20MB compress it with low quality + mediaInfo = await VideoCompress.compressVideo( + sourceFile.path, + quality: VideoQuality.Res960x540Quality, + includeAudio: true, + ); + } + } catch (e) { + Log.error('during video compression: $e'); + } + stopwatch.stop(); + Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); + + if (mediaInfo == null) { + Log.error('Could not compress video using original video.'); + // as a fall back use the non compressed version + sourceFile.copySync(destinationFile.path); + } else { + await mediaInfo.file!.copy(destinationFile.path); + } +} diff --git a/lib/src/services/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart similarity index 65% rename from lib/src/services/mediafile.service.dart rename to lib/src/services/mediafiles/mediafile.service.dart index cc2b3f1..13ce178 100644 --- a/lib/src/services/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -1,12 +1,12 @@ import 'dart:io'; - import 'package:drift/drift.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.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/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/compression.service.dart'; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; class MediaFileService { @@ -39,6 +39,28 @@ class MediaFileService { } } + Future setDisplayLimit(int? displayLimitInMilliseconds) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + ), + ); + await updateFromDB(); + } + + Future setRequiresAuth(bool requiresAuthentication) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + requiresAuthentication: Value(requiresAuthentication), + displayLimitInMilliseconds: + requiresAuthentication ? const Value(12) : const Value.absent(), + ), + ); + await updateFromDB(); + } + Future createThumbnail() async { if (!storedPath.existsSync()) { Log.error('Could not create Thumbnail as stored media does not exists.'); @@ -54,18 +76,35 @@ class MediaFileService { } } + Future compressMedia() async { + if (!originalPath.existsSync()) { + Log.error('Could not compress as original media does not exists.'); + return; + } + switch (mediaFile.type) { + case MediaType.image: + await compressImage(originalPath, tempPath); + case MediaType.video: + await compressVideo(originalPath, tempPath); + case MediaType.gif: + originalPath.renameSync(tempPath.path); + Log.error('Compression for .gif is not implemented yet.'); + } + } + void fullMediaRemoval() { - if (tempPath.existsSync()) { - tempPath.deleteSync(); - } - if (encryptedPath.existsSync()) { - encryptedPath.deleteSync(); - } - if (storedPath.existsSync()) { - storedPath.deleteSync(); - } - if (thumbnailPath.existsSync()) { - thumbnailPath.deleteSync(); + final pathsToRemove = [ + tempPath, + encryptedPath, + originalPath, + storedPath, + thumbnailPath + ]; + + for (final path in pathsToRemove) { + if (path.existsSync()) { + path.deleteSync(); + } } } @@ -121,4 +160,8 @@ class MediaFileService { 'tmp', namePrefix: '.encrypted', ); + File get originalPath => _buildFilePath( + 'tmp', + namePrefix: '.original', + ); } diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart similarity index 100% rename from lib/src/services/thumbnail.service.dart rename to lib/src/services/mediafiles/thumbnail.service.dart diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 27d2189..5a6426e 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -4,7 +4,7 @@ import 'package:drift/drift.dart'; import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index b31cc35..097af4a 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -14,7 +14,7 @@ import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 1f1973d..ec33119 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,6 +1,4 @@ -import 'dart:convert'; import 'dart:math'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,14 +9,13 @@ import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/log.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; - TwonlyDatabase get db => Provider.of(this); + TwonlyDB get db => Provider.of(this); ColorScheme get color => Theme.of(this).colorScheme; } @@ -245,31 +242,19 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; } -String getMessageText(Message message) { - try { - if (message.contentJson == null) return ''; - return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map) - .text; - } catch (e) { - Log.error(e); - return ''; - } -} - -MediaMessageContent? getMediaContent(Message message) { - try { - if (message.contentJson == null) return null; - return MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - } catch (e) { - Log.error(e); - return null; - } -} - bool isUUIDNewer(String uuid1, String uuid2) { final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); return timestamp1 > timestamp2; } + +String uint8ListToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); +} + +Uint8List hexToUint8List(String hex) => Uint8List.fromList( + List.generate( + hex.length ~/ 2, + (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), + ), + ); diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index b4b2e04..119bdd1 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; @@ -14,6 +15,7 @@ Future isUserCreated() async { if (user == null) { return false; } + gUser = user; return true; } @@ -56,7 +58,8 @@ Future updateUserdata( final updated = updateUser(user); await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); - return user; + gUser = updated; + return updated; }); } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index b522422..b10f65f 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:path/path.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 26f8d0b..20eb318 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -10,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -299,17 +301,35 @@ class _CameraPreviewViewState extends State { File? videoFilePath, { bool sharedFromGallery = false, }) async { + final mediaFileService = await initializeMediaUpload( + (videoFilePath != null) ? MediaType.video : MediaType.image, + gUser.defaultShowTime, + ); + if (!mounted) return true; + + if (mediaFileService == null) { + Log.error('Could not generate media file service'); + return false; + } + + if (videoFilePath != null) { + videoFilePath + ..copySync(mediaFileService.originalPath.path) + ..deleteSync(); + + // Start with compressing the video, to speed up the process in case the video is not changed. + unawaited(mediaFileService.compressMedia()); + } + final shouldReturn = await Navigator.push( context, PageRouteBuilder( opaque: false, pageBuilder: (context, a1, a2) => ShareImageEditorView( - videoFilePath: videoFilePath, - imageBytes: imageBytes, + imageBytesFuture: imageBytes, sharedFromGallery: sharedFromGallery, sendTo: widget.sendTo, - mirrorVideo: isFront && Platform.isAndroid && false, - useHighQuality: true, + mediaFileService: mediaFileService, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 3653b9c..94fbd49 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -3,14 +3,19 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -34,32 +39,26 @@ const gMediaShowInfinite = 999999; class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ - required this.mirrorVideo, - required this.useHighQuality, required this.sharedFromGallery, + required this.mediaFileService, super.key, - this.imageBytes, + this.imageBytesFuture, this.sendTo, - this.videoFilePath, }); - final Future? imageBytes; - final File? videoFilePath; - final Contact? sendTo; - final bool mirrorVideo; - final bool useHighQuality; + final Future? imageBytesFuture; + final Group? sendTo; final bool sharedFromGallery; + final MediaFileService mediaFileService; @override State createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { - bool _isRealTwonly = false; - int maxShowTime = gMediaShowInfinite; double tabDownPosition = 0; bool sendingOrLoadingImage = true; bool loadingImage = true; bool isDisposed = false; - HashSet selectedUserIds = HashSet(); + HashSet selectedGroupIds = HashSet(); double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; @@ -68,26 +67,31 @@ class _ShareImageEditorView extends State { ScreenshotController screenshotController = ScreenshotController(); /// Media upload variables - int? mediaUploadId; Future? videoUploadHandler; + MediaFileService get mediaService => widget.mediaFileService; + MediaFile get media => widget.mediaFileService.mediaFile; + @override void initState() { super.initState(); - unawaited(initAsync()); - unawaited(initMediaFileUpload()); + layers.add(FilterLayerData()); + if (widget.sendTo != null) { - selectedUserIds.add(widget.sendTo!.userId); + selectedGroupIds.add(widget.sendTo!.groupId); } - if (widget.imageBytes != null) { - unawaited(loadImage(widget.imageBytes!)); - } else if (widget.videoFilePath != null) { + + if (widget.imageBytesFuture != null) { + unawaited(loadImage(widget.imageBytesFuture!)); + } + + if (media.type == MediaType.video) { setState(() { sendingOrLoadingImage = false; loadingImage = false; }); - videoController = VideoPlayerController.file(widget.videoFilePath!); + videoController = VideoPlayerController.file(mediaService.originalPath); videoController?.setLooping(true); videoController?.initialize().then((_) async { await videoController!.play(); @@ -97,29 +101,6 @@ class _ShareImageEditorView extends State { } } - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - if (user.defaultShowTime != null) { - setState(() { - maxShowTime = user.defaultShowTime!; - }); - } - } - - Future initMediaFileUpload() async { - // media init was already called... - if (mediaUploadId != null) return; - - mediaUploadId = await initMediaUpload(); - - if (widget.videoFilePath != null && mediaUploadId != null) { - // start with the video compression... - videoUploadHandler = - addVideoToUpload(mediaUploadId!, widget.videoFilePath!); - } - } - @override void dispose() { isDisposed = true; @@ -128,14 +109,14 @@ class _ShareImageEditorView extends State { super.dispose(); } - void updateStatus(int userId, bool checked) { + void updateSelectedGroupIds(String groupId, bool checked) { if (checked) { - if (_isRealTwonly) { - selectedUserIds.clear(); + if (media.requiresAuthentication) { + selectedGroupIds.clear(); } - selectedUserIds.add(userId); + selectedGroupIds.add(groupId); } else { - selectedUserIds.remove(userId); + selectedGroupIds.remove(groupId); } setState(() {}); } @@ -195,38 +176,36 @@ class _ShareImageEditorView extends State { ), const SizedBox(height: 8), NotificationBadge( - count: (widget.videoFilePath != null) + count: (media.type != MediaType.video) ? '0' - : maxShowTime == gMediaShowInfinite + : media.displayLimitInMilliseconds == null ? '∞' - : maxShowTime.toString(), + : media.displayLimitInMilliseconds.toString(), child: ActionButton( - (widget.videoFilePath != null) - ? maxShowTime == gMediaShowInfinite + (media.type != MediaType.video) + ? media.displayLimitInMilliseconds == null ? Icons.repeat_rounded : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, onPressed: () async { - if (widget.videoFilePath != null) { - setState(() { - if (maxShowTime == gMediaShowInfinite) { - maxShowTime = 0; - } else { - maxShowTime = gMediaShowInfinite; - } - }); + if (media.type != MediaType.video) { + await mediaService.setDisplayLimit( + (media.displayLimitInMilliseconds == null) ? 0 : null); + if (!mounted) return; + setState(() {}); return; } - if (maxShowTime == gMediaShowInfinite) { + int? maxShowTime; + if (media.displayLimitInMilliseconds == null) { maxShowTime = 1; - } else if (maxShowTime == 1) { + } else if (media.displayLimitInMilliseconds == 1) { maxShowTime = 5; - } else if (maxShowTime == 5) { + } else if (media.displayLimitInMilliseconds == 5) { maxShowTime = 20; - } else { - maxShowTime = gMediaShowInfinite; } + await mediaService.setDisplayLimit(maxShowTime); + if (!mounted) return; setState(() {}); await updateUserdata((user) { user.defaultShowTime = maxShowTime; @@ -239,15 +218,12 @@ class _ShareImageEditorView extends State { ActionButton( FontAwesomeIcons.shieldHeart, tooltipText: context.lang.protectAsARealTwonly, - color: _isRealTwonly + color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white, onPressed: () async { - _isRealTwonly = !_isRealTwonly; - if (_isRealTwonly) { - maxShowTime = 12; - } - selectedUserIds = HashSet(); + await mediaService.setRequiresAuth(!media.requiresAuthentication); + selectedGroupIds = HashSet(); setState(() {}); }, ), @@ -308,11 +284,7 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - if (mediaUploadId == null) { - await initMediaFileUpload(); - if (mediaUploadId == null) return; - } - final imageBytes = getMergedImage(); + final imageBytes = storeImageAsOriginal(); await videoController?.pause(); if (isDisposed || !mounted) return; final wasSend = await Navigator.push( @@ -320,13 +292,10 @@ class _ShareImageEditorView extends State { MaterialPageRoute( builder: (context) => ShareImageView( imageBytesFuture: imageBytes, - isRealTwonly: _isRealTwonly, - maxShowTime: maxShowTime, - selectedUserIds: selectedUserIds, - updateStatus: updateStatus, + selectedUserIds: selectedGroupIds, + updateStatus: updateSelectedGroupIds, videoUploadHandler: videoUploadHandler, - mediaUploadId: mediaUploadId!, - mirrorVideo: widget.mirrorVideo, + mediaFileService: mediaService, ), ), ) as bool?; @@ -337,36 +306,46 @@ class _ShareImageEditorView extends State { } } - Future getMergedImage() async { - Uint8List? image; + Future storeImageAsOriginal() async { + if (layers.length == 1) { + if (layers.first is BackgroundLayerData) { + final image = (layers.first as BackgroundLayerData).image.bytes; + mediaService.originalPath.writeAsBytesSync(image); + } + } - - TODO: When changed then create a new mediaID!!!!!! - As storedMediaId would overwrite it.... - - if (layers.length > 1 || widget.videoFilePath != null) { + if (layers.length > 1 || media.type != MediaType.video) { for (final x in layers) { x.showCustomButtons = false; } setState(() {}); - image = await screenshotController.capture( - pixelRatio: (widget.useHighQuality) ? pixelRatio : 1, + final image = await screenshotController.capture( + pixelRatio: pixelRatio, ); + if (image == null) { + Log.error('screenshotController did not return image bytes'); + return; + } + + mediaService.originalPath.writeAsBytesSync(image); + + // In case the image was already stored, then rename the stored image. + + if (mediaService.storedPath.existsSync()) { + final newPath = mediaService.storedPath.absolute.path + .replaceFirst(media.mediaId, uuid.v7()); + mediaService.storedPath.renameSync(newPath); + } + for (final x in layers) { x.showCustomButtons = true; } setState(() {}); - } else if (layers.length == 1) { - if (layers.first is BackgroundLayerData) { - image = (layers.first as BackgroundLayerData).image.bytes; - } } - return image; } - Future loadImage(Future imageFile) async { - final imageBytes = await imageFile; - await currentImage.load(imageBytes); + Future loadImage(Future imageBytesFuture) async { + await currentImage.load(await imageBytesFuture); if (isDisposed) return; if (!context.mounted) return; @@ -388,56 +367,29 @@ class _ShareImageEditorView extends State { setState(() { sendingOrLoadingImage = true; }); - final imageBytes = await getMergedImage(); + + if (media.type == MediaType.image) { + await storeImageAsOriginal(); + } + if (media.type == MediaType.video) { + Log.error('TODO: COMBINE VIDEO AND IMAGE!!!'); + } + if (!context.mounted) return; - if (imageBytes == null) { + + // first finalize the upload + await finalizeUpload(mediaService, [widget.sendTo!.groupId]); + + /// then call the upload process in the background + await encryptMediaFiles( + mediaUploadId!, + imageHandler, + videoUploadHandler, + ); + + if (context.mounted) { // ignore: use_build_context_synchronously Navigator.pop(context, true); - return; - } - final err = await isAllowedToSend(); - if (!context.mounted) return; - - if (err != null) { - setState(() { - sendingOrLoadingImage = false; - }); - if (mounted) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SubscriptionView( - redirectError: err, - ); - }, - ), - ); - } - } else { - final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes); - - // first finalize the upload - await finalizeUpload( - mediaUploadId!, - [widget.sendTo!.userId], - _isRealTwonly, - widget.videoFilePath != null, - widget.mirrorVideo, - maxShowTime, - ); - - /// then call the upload process in the background - await encryptMediaFiles( - mediaUploadId!, - imageHandler, - videoUploadHandler, - ); - - if (context.mounted) { - // ignore: use_build_context_synchronously - Navigator.pop(context, true); - } } } @@ -543,10 +495,7 @@ class _ShareImageEditorView extends State { children: [ if (videoController != null) Positioned.fill( - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(videoController!), - ), + child: VideoPlayer(videoController!), ), Screenshot( controller: screenshotController, diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 537a193..22fc367 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -9,7 +9,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -21,25 +22,19 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ required this.imageBytesFuture, - required this.isRealTwonly, - required this.mirrorVideo, - required this.maxShowTime, required this.selectedUserIds, required this.updateStatus, required this.videoUploadHandler, - required this.mediaUploadId, + required this.mediaFileService, super.key, this.enableVideoAudio, }); final Future imageBytesFuture; - final bool isRealTwonly; - final bool mirrorVideo; - final int maxShowTime; final HashSet selectedUserIds; final bool? enableVideoAudio; - final int mediaUploadId; final void Function(int, bool) updateStatus; final Future? videoUploadHandler; + final MediaFileService mediaFileService; @override State createState() => _ShareImageView(); diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index c05331e..64e5089 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -227,8 +227,7 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () async { - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await rejectAndDeleteContact(contact.userId); }, ), ), diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index be2444d..f197cd5 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -12,7 +12,7 @@ import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 9a247f9..4be5049 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -8,7 +8,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; +import 'package:twonly/src/services/api/mediafiles/download.service.dart' + as received; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index e6650e5..887c1b6 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -15,7 +15,7 @@ import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 52d2ef3..d08a914 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -31,8 +31,7 @@ class _ContactViewState extends State { ); if (remove) { // trigger deletion for the other user... - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await rejectAndDeleteContact(contact.userId); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 588fdfe..fd95f31 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -6,8 +6,8 @@ import 'package:intl/intl.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 5489e9b..bb2abf5 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -6,8 +6,9 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; -import 'package:twonly/src/services/api/media_upload.dart' as send; +import 'package:twonly/src/services/api/mediafiles/download.service.dart' + as received; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 84edc97..8e8d2af 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -90,6 +90,8 @@ class _RegisterViewState extends State { await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); + gUser = userData; + await apiService.authenticate(); widget.callbackOnSuccess(); } diff --git a/lib/src/views/settings/data_and_storage.view.dart b/lib/src/views/settings/data_and_storage.view.dart index 4c21c75..5f5b964 100644 --- a/lib/src/views/settings/data_and_storage.view.dart +++ b/lib/src/views/settings/data_and_storage.view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/views/settings/help/contact_us.view.dart b/lib/src/views/settings/help/contact_us.view.dart index 6d30400..ad6a570 100644 --- a/lib/src/views/settings/help/contact_us.view.dart +++ b/lib/src/views/settings/help/contact_us.view.dart @@ -8,7 +8,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/services/api/media_upload.dart' +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' show createDownloadToken, uint8ListToHex; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; diff --git a/test/unit_test.dart b/test/unit_test.dart index 8d4c715..ffe3b19 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; From 645dfe16da5096e7e961b689773f0105f0846927 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 23 Oct 2025 21:31:13 +0200 Subject: [PATCH 07/76] media upload --- lib/main.dart | 4 +- lib/src/database/daos/groups.dao.dart | 5 + lib/src/database/daos/messages.dao.dart | 8 + lib/src/database/tables/groups.table.dart | 2 + lib/src/database/tables/mediafiles.table.dart | 3 +- lib/src/database/tables/messages.table.dart | 2 + lib/src/database/twonly.db.g.dart | 115 +++- lib/src/services/api.service.dart | 2 - .../mediafiles/media_background.service.dart | 61 ++ .../api/mediafiles/upload.service.dart | 599 ++++-------------- lib/src/services/api/messages.dart | 37 +- .../mediafiles/mediafile.service.dart | 27 +- .../background.notifications.dart | 5 +- .../twonly_safe/common.twonly_safe.dart | 2 +- .../create_backup.twonly_safe.dart | 2 +- .../save_to_gallery.dart | 62 +- .../camera_preview_controller_view.dart | 35 +- lib/src/views/camera/camera_send_to_view.dart | 6 +- .../views/camera/share_image_editor_view.dart | 95 ++- lib/src/views/camera/share_image_view.dart | 31 +- 20 files changed, 456 insertions(+), 647 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 431626c..eb2fdb4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,11 +10,9 @@ import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; -import 'package:twonly/src/services/api/mediafiles/download.service.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; -import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b004215..3747ab4 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -26,4 +26,9 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { await (update(groups)..where((c) => c.groupId.equals(groupId))) .write(updates); } + + Future> getGroupMembers(String groupId) async { + return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + .get(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 07fd0a6..0483dd8 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -285,6 +285,14 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .write(updatedValues); } + Future updateMessagesByMediaId( + String mediaId, + MessagesCompanion updatedValues, + ) { + return (update(messages)..where((c) => c.mediaId.equals(mediaId))) + .write(updatedValues); + } + Future insertMessage(MessagesCompanion message) async { try { final rowId = await into(messages).insert(message); diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index e6548e4..27b6438 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -11,6 +11,8 @@ class Groups extends Table { BoolColumn get pinned => boolean().withDefault(const Constant(false))(); BoolColumn get archived => boolean().withDefault(const Constant(false))(); + TextColumn get groupName => text()(); + DateTimeColumn get lastMessageExchange => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 8818f0a..26b9a8d 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -15,8 +15,7 @@ enum UploadState { // Image was stored but not send storedOnly, // At this point the user is finished with editing, and the media file can be uploaded - compressing, - encrypting, + preprocessing, uploading, backgroundUploadTaskStarted, uploaded, diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 08c28f0..28438c8 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -18,6 +18,8 @@ class Messages extends Table { TextColumn get mediaId => text().nullable().references(MediaFiles, #mediaId)(); + BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); + BlobColumn get downloadToken => blob().nullable()(); TextColumn get quotesMessageId => diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index f3292b9..118a6d5 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1061,6 +1061,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _groupNameMeta = + const VerificationMeta('groupName'); + @override + late final GeneratedColumn groupName = GeneratedColumn( + 'group_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _lastMessageExchangeMeta = const VerificationMeta('lastMessageExchange'); @override @@ -1084,6 +1090,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { isGroupOfTwo, pinned, archived, + groupName, lastMessageExchange, createdAt ]; @@ -1125,6 +1132,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { context.handle(_archivedMeta, archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); } + if (data.containsKey('group_name')) { + context.handle(_groupNameMeta, + groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta)); + } else if (isInserting) { + context.missing(_groupNameMeta); + } if (data.containsKey('last_message_exchange')) { context.handle( _lastMessageExchangeMeta, @@ -1154,6 +1167,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, archived: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + groupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, lastMessageExchange: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}last_message_exchange'])!, @@ -1174,6 +1189,7 @@ class Group extends DataClass implements Insertable { final bool isGroupOfTwo; final bool pinned; final bool archived; + final String groupName; final DateTime lastMessageExchange; final DateTime createdAt; const Group( @@ -1182,6 +1198,7 @@ class Group extends DataClass implements Insertable { required this.isGroupOfTwo, required this.pinned, required this.archived, + required this.groupName, required this.lastMessageExchange, required this.createdAt}); @override @@ -1192,6 +1209,7 @@ class Group extends DataClass implements Insertable { map['is_group_of_two'] = Variable(isGroupOfTwo); map['pinned'] = Variable(pinned); map['archived'] = Variable(archived); + map['group_name'] = Variable(groupName); map['last_message_exchange'] = Variable(lastMessageExchange); map['created_at'] = Variable(createdAt); return map; @@ -1204,6 +1222,7 @@ class Group extends DataClass implements Insertable { isGroupOfTwo: Value(isGroupOfTwo), pinned: Value(pinned), archived: Value(archived), + groupName: Value(groupName), lastMessageExchange: Value(lastMessageExchange), createdAt: Value(createdAt), ); @@ -1218,6 +1237,7 @@ class Group extends DataClass implements Insertable { isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), pinned: serializer.fromJson(json['pinned']), archived: serializer.fromJson(json['archived']), + groupName: serializer.fromJson(json['groupName']), lastMessageExchange: serializer.fromJson(json['lastMessageExchange']), createdAt: serializer.fromJson(json['createdAt']), @@ -1232,6 +1252,7 @@ class Group extends DataClass implements Insertable { 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), 'pinned': serializer.toJson(pinned), 'archived': serializer.toJson(archived), + 'groupName': serializer.toJson(groupName), 'lastMessageExchange': serializer.toJson(lastMessageExchange), 'createdAt': serializer.toJson(createdAt), }; @@ -1243,6 +1264,7 @@ class Group extends DataClass implements Insertable { bool? isGroupOfTwo, bool? pinned, bool? archived, + String? groupName, DateTime? lastMessageExchange, DateTime? createdAt}) => Group( @@ -1251,6 +1273,7 @@ class Group extends DataClass implements Insertable { isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, pinned: pinned ?? this.pinned, archived: archived ?? this.archived, + groupName: groupName ?? this.groupName, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, createdAt: createdAt ?? this.createdAt, ); @@ -1265,6 +1288,7 @@ class Group extends DataClass implements Insertable { : this.isGroupOfTwo, pinned: data.pinned.present ? data.pinned.value : this.pinned, archived: data.archived.present ? data.archived.value : this.archived, + groupName: data.groupName.present ? data.groupName.value : this.groupName, lastMessageExchange: data.lastMessageExchange.present ? data.lastMessageExchange.value : this.lastMessageExchange, @@ -1280,6 +1304,7 @@ class Group extends DataClass implements Insertable { ..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('pinned: $pinned, ') ..write('archived: $archived, ') + ..write('groupName: $groupName, ') ..write('lastMessageExchange: $lastMessageExchange, ') ..write('createdAt: $createdAt') ..write(')')) @@ -1288,7 +1313,7 @@ class Group extends DataClass implements Insertable { @override int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, - archived, lastMessageExchange, createdAt); + archived, groupName, lastMessageExchange, createdAt); @override bool operator ==(Object other) => identical(this, other) || @@ -1298,6 +1323,7 @@ class Group extends DataClass implements Insertable { other.isGroupOfTwo == this.isGroupOfTwo && other.pinned == this.pinned && other.archived == this.archived && + other.groupName == this.groupName && other.lastMessageExchange == this.lastMessageExchange && other.createdAt == this.createdAt); } @@ -1308,6 +1334,7 @@ class GroupsCompanion extends UpdateCompanion { final Value isGroupOfTwo; final Value pinned; final Value archived; + final Value groupName; final Value lastMessageExchange; final Value createdAt; final Value rowid; @@ -1317,6 +1344,7 @@ class GroupsCompanion extends UpdateCompanion { this.isGroupOfTwo = const Value.absent(), this.pinned = const Value.absent(), this.archived = const Value.absent(), + this.groupName = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), @@ -1327,17 +1355,20 @@ class GroupsCompanion extends UpdateCompanion { required bool isGroupOfTwo, this.pinned = const Value.absent(), this.archived = const Value.absent(), + required String groupName, this.lastMessageExchange = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }) : isGroupAdmin = Value(isGroupAdmin), - isGroupOfTwo = Value(isGroupOfTwo); + isGroupOfTwo = Value(isGroupOfTwo), + groupName = Value(groupName); static Insertable custom({ Expression? groupId, Expression? isGroupAdmin, Expression? isGroupOfTwo, Expression? pinned, Expression? archived, + Expression? groupName, Expression? lastMessageExchange, Expression? createdAt, Expression? rowid, @@ -1348,6 +1379,7 @@ class GroupsCompanion extends UpdateCompanion { if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, if (pinned != null) 'pinned': pinned, if (archived != null) 'archived': archived, + if (groupName != null) 'group_name': groupName, if (lastMessageExchange != null) 'last_message_exchange': lastMessageExchange, if (createdAt != null) 'created_at': createdAt, @@ -1361,6 +1393,7 @@ class GroupsCompanion extends UpdateCompanion { Value? isGroupOfTwo, Value? pinned, Value? archived, + Value? groupName, Value? lastMessageExchange, Value? createdAt, Value? rowid}) { @@ -1370,6 +1403,7 @@ class GroupsCompanion extends UpdateCompanion { isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, pinned: pinned ?? this.pinned, archived: archived ?? this.archived, + groupName: groupName ?? this.groupName, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, createdAt: createdAt ?? this.createdAt, rowid: rowid ?? this.rowid, @@ -1394,6 +1428,9 @@ class GroupsCompanion extends UpdateCompanion { if (archived.present) { map['archived'] = Variable(archived.value); } + if (groupName.present) { + map['group_name'] = Variable(groupName.value); + } if (lastMessageExchange.present) { map['last_message_exchange'] = Variable(lastMessageExchange.value); @@ -1415,6 +1452,7 @@ class GroupsCompanion extends UpdateCompanion { ..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('pinned: $pinned, ') ..write('archived: $archived, ') + ..write('groupName: $groupName, ') ..write('lastMessageExchange: $lastMessageExchange, ') ..write('createdAt: $createdAt, ') ..write('rowid: $rowid') @@ -2231,6 +2269,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES media_files (media_id)')); + static const VerificationMeta _mediaStoredMeta = + const VerificationMeta('mediaStored'); + @override + late final GeneratedColumn mediaStored = GeneratedColumn( + 'media_stored', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("media_stored" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _downloadTokenMeta = const VerificationMeta('downloadToken'); @override @@ -2323,6 +2371,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { senderId, content, mediaId, + mediaStored, downloadToken, quotesMessageId, isDeletedFromSender, @@ -2366,6 +2415,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_mediaIdMeta, mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); } + if (data.containsKey('media_stored')) { + context.handle( + _mediaStoredMeta, + mediaStored.isAcceptableOrUnknown( + data['media_stored']!, _mediaStoredMeta)); + } if (data.containsKey('download_token')) { context.handle( _downloadTokenMeta, @@ -2439,6 +2494,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + mediaStored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!, downloadToken: attachedDatabase.typeMapping .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), quotesMessageId: attachedDatabase.typeMapping.read( @@ -2474,6 +2531,7 @@ class Message extends DataClass implements Insertable { final int? senderId; final String? content; final String? mediaId; + final bool mediaStored; final Uint8List? downloadToken; final String? quotesMessageId; final bool isDeletedFromSender; @@ -2490,6 +2548,7 @@ class Message extends DataClass implements Insertable { this.senderId, this.content, this.mediaId, + required this.mediaStored, this.downloadToken, this.quotesMessageId, required this.isDeletedFromSender, @@ -2514,6 +2573,7 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || mediaId != null) { map['media_id'] = Variable(mediaId); } + map['media_stored'] = Variable(mediaStored); if (!nullToAbsent || downloadToken != null) { map['download_token'] = Variable(downloadToken); } @@ -2548,6 +2608,7 @@ class Message extends DataClass implements Insertable { mediaId: mediaId == null && nullToAbsent ? const Value.absent() : Value(mediaId), + mediaStored: Value(mediaStored), downloadToken: downloadToken == null && nullToAbsent ? const Value.absent() : Value(downloadToken), @@ -2578,6 +2639,7 @@ class Message extends DataClass implements Insertable { senderId: serializer.fromJson(json['senderId']), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), + mediaStored: serializer.fromJson(json['mediaStored']), downloadToken: serializer.fromJson(json['downloadToken']), quotesMessageId: serializer.fromJson(json['quotesMessageId']), isDeletedFromSender: @@ -2600,6 +2662,7 @@ class Message extends DataClass implements Insertable { 'senderId': serializer.toJson(senderId), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), + 'mediaStored': serializer.toJson(mediaStored), 'downloadToken': serializer.toJson(downloadToken), 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), @@ -2619,6 +2682,7 @@ class Message extends DataClass implements Insertable { Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + bool? mediaStored, Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, @@ -2635,6 +2699,7 @@ class Message extends DataClass implements Insertable { senderId: senderId.present ? senderId.value : this.senderId, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, + mediaStored: mediaStored ?? this.mediaStored, downloadToken: downloadToken.present ? downloadToken.value : this.downloadToken, quotesMessageId: quotesMessageId.present @@ -2656,6 +2721,8 @@ class Message extends DataClass implements Insertable { senderId: data.senderId.present ? data.senderId.value : this.senderId, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + mediaStored: + data.mediaStored.present ? data.mediaStored.value : this.mediaStored, downloadToken: data.downloadToken.present ? data.downloadToken.value : this.downloadToken, @@ -2687,6 +2754,7 @@ class Message extends DataClass implements Insertable { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('mediaStored: $mediaStored, ') ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') @@ -2708,6 +2776,7 @@ class Message extends DataClass implements Insertable { senderId, content, mediaId, + mediaStored, $driftBlobEquality.hash(downloadToken), quotesMessageId, isDeletedFromSender, @@ -2727,6 +2796,7 @@ class Message extends DataClass implements Insertable { other.senderId == this.senderId && other.content == this.content && other.mediaId == this.mediaId && + other.mediaStored == this.mediaStored && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && @@ -2745,6 +2815,7 @@ class MessagesCompanion extends UpdateCompanion { final Value senderId; final Value content; final Value mediaId; + final Value mediaStored; final Value downloadToken; final Value quotesMessageId; final Value isDeletedFromSender; @@ -2762,6 +2833,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.mediaStored = const Value.absent(), this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), @@ -2780,6 +2852,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.mediaStored = const Value.absent(), this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), @@ -2798,6 +2871,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? senderId, Expression? content, Expression? mediaId, + Expression? mediaStored, Expression? downloadToken, Expression? quotesMessageId, Expression? isDeletedFromSender, @@ -2816,6 +2890,7 @@ class MessagesCompanion extends UpdateCompanion { if (senderId != null) 'sender_id': senderId, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, + if (mediaStored != null) 'media_stored': mediaStored, if (downloadToken != null) 'download_token': downloadToken, if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (isDeletedFromSender != null) @@ -2837,6 +2912,7 @@ class MessagesCompanion extends UpdateCompanion { Value? senderId, Value? content, Value? mediaId, + Value? mediaStored, Value? downloadToken, Value? quotesMessageId, Value? isDeletedFromSender, @@ -2854,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion { senderId: senderId ?? this.senderId, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, + mediaStored: mediaStored ?? this.mediaStored, downloadToken: downloadToken ?? this.downloadToken, quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, @@ -2886,6 +2963,9 @@ class MessagesCompanion extends UpdateCompanion { if (mediaId.present) { map['media_id'] = Variable(mediaId.value); } + if (mediaStored.present) { + map['media_stored'] = Variable(mediaStored.value); + } if (downloadToken.present) { map['download_token'] = Variable(downloadToken.value); } @@ -2930,6 +3010,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('mediaStored: $mediaStored, ') ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') @@ -6863,6 +6944,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required bool isGroupOfTwo, Value pinned, Value archived, + required String groupName, Value lastMessageExchange, Value createdAt, Value rowid, @@ -6873,6 +6955,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value isGroupOfTwo, Value pinned, Value archived, + Value groupName, Value lastMessageExchange, Value createdAt, Value rowid, @@ -6921,6 +7004,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnFilters(column)); + ColumnFilters get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnFilters(column)); @@ -6975,6 +7061,9 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnOrderings(column)); @@ -7007,6 +7096,9 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get archived => $composableBuilder(column: $table.archived, builder: (column) => column); + GeneratedColumn get groupName => + $composableBuilder(column: $table.groupName, builder: (column) => column); + GeneratedColumn get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => column); @@ -7063,6 +7155,7 @@ class $$GroupsTableTableManager extends RootTableManager< Value isGroupOfTwo = const Value.absent(), Value pinned = const Value.absent(), Value archived = const Value.absent(), + Value groupName = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), @@ -7073,6 +7166,7 @@ class $$GroupsTableTableManager extends RootTableManager< isGroupOfTwo: isGroupOfTwo, pinned: pinned, archived: archived, + groupName: groupName, lastMessageExchange: lastMessageExchange, createdAt: createdAt, rowid: rowid, @@ -7083,6 +7177,7 @@ class $$GroupsTableTableManager extends RootTableManager< required bool isGroupOfTwo, Value pinned = const Value.absent(), Value archived = const Value.absent(), + required String groupName, Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), @@ -7093,6 +7188,7 @@ class $$GroupsTableTableManager extends RootTableManager< isGroupOfTwo: isGroupOfTwo, pinned: pinned, archived: archived, + groupName: groupName, lastMessageExchange: lastMessageExchange, createdAt: createdAt, rowid: rowid, @@ -7556,6 +7652,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value mediaStored, Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, @@ -7574,6 +7671,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value mediaStored, Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, @@ -7716,6 +7814,9 @@ class $$MessagesTableFilterComposer ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); + ColumnFilters get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnFilters(column)); @@ -7904,6 +8005,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnOrderings(column)); @@ -8030,6 +8134,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); + GeneratedColumn get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => column); @@ -8236,6 +8343,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), @@ -8254,6 +8362,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + mediaStored: mediaStored, downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, @@ -8272,6 +8381,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), @@ -8290,6 +8400,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + mediaStored: mediaStored, downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 64bf2b5..a30e8cc 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -24,7 +24,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; @@ -94,7 +93,6 @@ class ApiService { if (!globalIsAppInBackground) { unawaited(retransmitRawBytes()); unawaited(tryTransmitMessages()); - unawaited(retryMediaUpload(false)); unawaited(tryDownloadAllMediaFiles()); unawaited(notifyContactsAboutProfileChange()); twonlyDB.markUpdated(); diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index dce9a1c..018d17b 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -1,8 +1,13 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.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/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; @@ -48,3 +53,59 @@ Future initFileDownloader() async { ); } } + +Future handleUploadStatusUpdate(TaskStatusUpdate update) async { + final mediaId = update.task.taskId.replaceAll('upload_', ''); + final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + + if (media == null) { + Log.error( + 'Got an upload task but no upload media in the media upload database', + ); + return; + } + + if (update.status == TaskStatus.complete) { + if (update.responseStatusCode == 200) { + Log.info('Upload of ${media.mediaId} success!'); + + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploaded), + ), + ); + + await twonlyDB.messagesDao.updateMessagesByMediaId( + media.mediaId, + const MessagesCompanion( + ackByServer: Value(true), + ), + ); + return; + } + Log.error( + 'Got HTTP error ${update.responseStatusCode} for $mediaId', + ); + + if (update.responseStatusCode == 429) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploadLimitReached), + ), + ); + return; + } + } + + Log.info( + 'Background upload failed for $mediaId with status ${update.status}. Trying again.', + ); + + final mediaService = await MediaFileService.fromMedia(media); + + await mediaService.setUploadState(UploadState.uploading); + // In all other cases just try the upload again... + await startBackgroundMediaUpload(mediaService); +} diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index d0f4350..de6c9f3 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -1,124 +1,21 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; import 'package:background_downloader/background_downloader.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:http/http.dart' as http; -import 'package:mutex/mutex.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:video_compress/video_compress.dart'; - -/// States: -/// when user recorded an video -/// 1. Compress video -/// when user clicked the send button (direct send) or share with -/// 2. Encrypt media files -/// 3. Upload media files -/// click send button -/// 4. Finalize upload by websocket -> get download tokens -/// 5. Send all users the message - -/// Create a new entry in the database - -// Future checkForFailedUploads() async { -// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); -// final mediaUploadIds = []; -// for (final message in messages) { -// if (mediaUploadIds.contains(message.mediaUploadId)) { -// continue; -// } -// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( -// message.mediaUploadId!, -// const MediaUploadsCompanion( -// state: Value(UploadState.pending), -// encryptionData: Value( -// null, // start from scratch e.q. encrypt the files again if already happen -// ), -// ), -// ); -// if (affectedRows == 0) { -// Log.error( -// 'The media from message ${message.messageId} already deleted.', -// ); -// await twonlyDB.messagesDao.updateMessageByMessageId( -// message.messageId, -// const MessagesCompanion( -// errorWhileSending: Value(true), -// ), -// ); -// } else { -// mediaUploadIds.add(message.mediaUploadId!); -// } -// } -// if (messages.isNotEmpty) { -// Log.error( -// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', -// ); -// } -// return mediaUploadIds.isNotEmpty; // return true if there are affected -// } - -final lockingHandleMediaFile = Mutex(); -Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { - if (maxRetries == 0) { - Log.error('retried media upload 3 times. abort retrying'); - return; - } - final retry = await lockingHandleMediaFile.protect(() async { - final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry(); - if (mediaFiles.isEmpty) { - return checkForFailedUploads(); - } - Log.info('re uploading ${mediaFiles.length} media files.'); - for (final mediaFile in mediaFiles) { - if (mediaFile.messageIds == null || mediaFile.metadata == null) { - if (appRestarted) { - /// When the app got restarted and the messageIds or the metadata is not - /// set then the app was closed before the images was send. - await twonlyDB.mediaUploadsDao - .deleteMediaUpload(mediaFile.mediaUploadId); - Log.info( - 'upload can be removed, the finalized function was never called...', - ); - } - continue; - } - - if (mediaFile.state == UploadState.readyToUpload) { - await handleNextMediaUploadSteps(mediaFile.mediaUploadId); - } else { - await handlePreProcessingState(mediaFile); - } - } - return false; - }); - if (retry) { - await retryMediaUpload(false, maxRetries: maxRetries - 1); - } -} Future initializeMediaUpload( MediaType type, @@ -141,78 +38,10 @@ Future initializeMediaUpload( return MediaFileService.fromMedia(mediaFile); } -Future handlePreProcessingState(MediaUpload media) async { - try { - final imageHandler = readSendMediaFile(media.mediaUploadId, 'png'); - final videoHandler = compressVideoIfExists(media.mediaUploadId); - await encryptMediaFiles( - media.mediaUploadId, - imageHandler, - videoHandler, - ); - } catch (e) { - Log.error('${media.mediaUploadId} got error in pre processing: $e'); - await handleUploadError(media); - } -} - -Future encryptMediaFiles( - int mediaUploadId, - Future imageHandler, - Future? videoHandler, -) async { - Log.info('$mediaUploadId encrypting files'); - var dataToEncrypt = await imageHandler; - - /// if there is a video wait until it is finished with compression - if (videoHandler != null) { - if (await videoHandler) { - final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4'); - dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo); - } - } - - final state = MediaEncryptionData(); - - final chacha20 = FlutterChacha20.poly1305Aead(); - - state - ..encryptionKey = secretKey.bytes - ..encryptionNonce = chacha20.newNonce(); - - final secretBox = await chacha20.encrypt( - dataToEncrypt, - secretKey: secretKey, - nonce: state.encryptionNonce, - ); - - state - ..encryptionMac = secretBox.mac.bytes - ..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes; - - final encryptedBytes = Uint8List.fromList(secretBox.cipherText); - await writeSendMediaFile( - mediaUploadId, - 'encrypted', - encryptedBytes, - ); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - state: const Value(UploadState.readyToUpload), - encryptionData: Value(state), - ), - ); - unawaited(handleNextMediaUploadSteps(mediaUploadId)); -} - -Future finalizeUpload( +Future insertMediaFileInMessagesTable( MediaFileService mediaService, List groupIds, ) async { - final messageIds = []; - for (final groupId in groupIds) { final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( @@ -221,7 +50,6 @@ Future finalizeUpload( ), ); if (message != null) { - messageIds.add(message); // de-archive contact when sending a new message await twonlyDB.groupsDao.updateGroup( message.groupId, @@ -234,233 +62,131 @@ Future finalizeUpload( } } - unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId)); + unawaited(startBackgroundMediaUpload(mediaService)); } -final lockingHandleNextMediaUploadStep = Mutex(); -Future handleNextMediaUploadSteps(String mediaUploadId) async { - await lockingHandleNextMediaUploadStep.protect(() async { - final mediaUpload = await twonlyDB.mediaUploadsDao - .getMediaUploadById(mediaUploadId) - .getSingleOrNull(); - - if (mediaUpload == null) return false; - if (mediaUpload.state == UploadState.receiverNotified || - mediaUpload.state == UploadState.uploadTaskStarted) { - /// Upload done and all users are notified :) - Log.info('$mediaUploadId is already done'); - return false; +Future startBackgroundMediaUpload(MediaFileService mediaService) async { + if (mediaService.mediaFile.uploadState == UploadState.initialized) { + await mediaService.setUploadState(UploadState.preprocessing); + if (!mediaService.tempPath.existsSync()) { + await mediaService.compressMedia(); } - try { - /// Stage 1: media files are not yet encrypted... - if (mediaUpload.encryptionData == null) { - // when set this function will be called again by encryptAndPreUploadMediaFiles... - return false; - } - if (mediaUpload.messageIds == null || mediaUpload.metadata == null) { - /// the finalize function was not called yet... - return false; - } - - await handleMediaUpload(mediaUpload); - } catch (e) { - Log.error('Non recoverable error while sending media file: $e'); - await handleUploadError(mediaUpload); + if (!mediaService.encryptedPath.existsSync()) { + await _encryptMediaFiles(mediaService); } - return false; - }); + + if (!mediaService.uploadRequestPath.existsSync()) { + await _createUploadRequest(mediaService); + } + await mediaService.setUploadState(UploadState.uploading); + } + + if (mediaService.mediaFile.uploadState == UploadState.uploading) { + await _uploadUploadRequest(mediaService); + } } -/// -/// -- private functions -- -/// -/// -/// +Future _encryptMediaFiles(MediaFileService mediaService) async { + /// if there is a video wait until it is finished with compression -Future handleUploadStatusUpdate(TaskStatusUpdate update) async { - var failed = false; - final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', '')); + final dataToEncrypt = await mediaService.tempPath.readAsBytes(); - final media = await twonlyDB.mediaUploadsDao - .getMediaUploadById(mediaUploadId) - .getSingleOrNull(); - if (media == null) { - Log.error( - 'Got an upload task but no upload media in the media upload database', - ); - return; - } - if (update.status == TaskStatus.failed || - update.status == TaskStatus.canceled) { - Log.error('Upload failed: ${update.status}'); - failed = true; - } else if (update.status == TaskStatus.complete) { - if (update.responseStatusCode == 200) { - await handleUploadSuccess(media); - return; - } else if (update.responseStatusCode != null) { - if (update.responseStatusCode! >= 400 && - update.responseStatusCode! < 500) { - failed = true; - } - Log.error( - 'Got error while uploading: ${update.responseStatusCode}', - ); - } - } + final chacha20 = FlutterChacha20.poly1305Aead(); - if (failed) { - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(true), - ), - ); - } - } - Log.info( - 'Status update for ${update.task.taskId} with status ${update.status}', - ); -} - -Future handleUploadSuccess(MediaUpload media) async { - Log.info('Upload of ${media.mediaUploadId} success!'); - currentUploadTasks.remove(media.mediaUploadId); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - media.mediaUploadId, - const MediaUploadsCompanion( - state: Value(UploadState.receiverNotified), - ), + final secretBox = await chacha20.encrypt( + dataToEncrypt, + secretKey: SecretKey(mediaService.mediaFile.encryptionKey!), + nonce: mediaService.mediaFile.encryptionNonce, ); - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), - ), - ); - } + await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes)); + + mediaService.encryptedPath + .writeAsBytesSync(Uint8List.fromList(secretBox.cipherText)); + + await mediaService.setUploadState(UploadState.uploading); } -Future handleUploadError(MediaUpload mediaUpload) async { - // if the messageIds are already there notify the user about this error... - if (mediaUpload.messageIds != null) { - for (final messageId in mediaUpload.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } - } - await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId); -} - -Future handleMediaUpload(MediaUpload media) async { - final bytesToUpload = - await readSendMediaFile(media.mediaUploadId, 'encrypted'); - - if (media.messageIds == null) return; - - final messageIds = media.messageIds!; - +Future _createUploadRequest(MediaFileService media) async { final downloadTokens = []; final messagesOnSuccess = []; - for (var i = 0; i < messageIds.length; i++) { - final message = await twonlyDB.messagesDao - .getMessageByMessageId(messageIds[i]) - .getSingleOrNull(); - if (message == null) continue; + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId); - if (message.downloadState == DownloadState.downloaded) { - // only upload message which are not yet uploaded (or in case of an error re-uploaded) - continue; - } + for (final message in messages) { + final groupMembers = + await twonlyDB.groupsDao.getGroupMembers(message.groupId); + for (final groupMember in groupMembers) { + /// only send the upload to the users + if (media.mediaFile.reuploadRequestedBy != null) { + if (!media.mediaFile.reuploadRequestedBy! + .contains(groupMember.contactId)) { + continue; + } + } - final downloadToken = getRandomUint8List(32); - - final msg = MessageJson( - kind: MessageKind.media, - messageSenderId: messageIds[i], - content: MediaMessageContent( - downloadToken: downloadToken, - maxShowTime: media.metadata!.maxShowTime, - isRealTwonly: media.metadata!.isRealTwonly, - isVideo: media.metadata!.isVideo, - mirrorVideo: media.metadata!.mirrorVideo, - encryptionKey: media.encryptionData!.encryptionKey, - encryptionMac: media.encryptionData!.encryptionMac, - encryptionNonce: media.encryptionData!.encryptionNonce, - ), - timestamp: media.metadata!.messageSendAt, - ); - - final plaintextContent = Uint8List.fromList( - gzip.encode(utf8.encode(jsonEncode(msg.toJson()))), - ); - - final contact = await twonlyDB.contactsDao - .getContactByUserId(message.contactId) - .getSingleOrNull(); - - if (contact == null || contact.deleted) { - Log.warn( - 'Contact deleted ${message.contactId} or not found in database.', + await twonlyDB.contactsDao.incFlameCounter( + groupMember.contactId, + false, + message.createdAt, ); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion(errorWhileSending: Value(true)), + + final downloadToken = getRandomUint8List(32); + + var type = EncryptedContent_Media_Type.IMAGE; + if (media.mediaFile.type == MediaType.video) { + type = EncryptedContent_Media_Type.VIDEO; + } else if (media.mediaFile.type == MediaType.gif) { + type = EncryptedContent_Media_Type.GIF; + } + + final notEncryptedContent = EncryptedContent( + media: EncryptedContent_Media( + senderMessageId: message.messageId, + type: type, + requiresAuthentication: media.mediaFile.requiresAuthentication, + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + downloadToken: media.mediaFile.downloadToken, + encryptionKey: media.mediaFile.encryptionKey, + encryptionNonce: media.mediaFile.encryptionNonce, + encryptionMac: media.mediaFile.encryptionMac, + ), ); - continue; + + if (media.mediaFile.displayLimitInMilliseconds != null) { + notEncryptedContent.media.displayLimitInMilliseconds = + Int64(media.mediaFile.displayLimitInMilliseconds!); + } + + final cipherText = await sendCipherText( + groupMember.contactId, + notEncryptedContent, + onlyReturnEncryptedData: true, + ); + + if (cipherText == null) { + Log.error( + 'Could not generate ciphertext message for ${groupMember.contactId}'); + } + + final messageOnSuccess = TextMessage() + ..body = cipherText!.$1 + ..userId = Int64(groupMember.contactId); + + if (cipherText.$2 != null) { + messageOnSuccess.pushData = cipherText.$2!; + } + + messagesOnSuccess.add(messageOnSuccess); + downloadTokens.add(downloadToken); } - - await twonlyDB.contactsDao.incFlameCounter( - message.contactId, - false, - message.sendAt, - ); - - final encryptedBytes = await signalEncryptMessage( - message.contactId, - plaintextContent, - ); - - if (encryptedBytes == null) continue; - - final messageOnSuccess = TextMessage() - ..body = encryptedBytes - ..userId = Int64(message.contactId); - - final pushKind = (media.metadata!.isRealTwonly) - ? PushKind.twonly - : (media.metadata!.isVideo) - ? PushKind.video - : PushKind.image; - - final pushData = await getPushData( - message.contactId, - PushNotification( - messageId: Int64(message.messageId), - kind: pushKind, - ), - ); - if (pushData != null) { - messageOnSuccess.pushData = pushData.toList(); - } - - messagesOnSuccess.add(messageOnSuccess); - downloadTokens.add(downloadToken); } + final bytesToUpload = await media.encryptedPath.readAsBytes(); + final uploadRequest = UploadRequest( messagesOnSuccess: messagesOnSuccess, downloadTokens: downloadTokens, @@ -469,6 +195,10 @@ Future handleMediaUpload(MediaUpload media) async { final uploadRequestBytes = uploadRequest.writeToBuffer(); + await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); +} + +Future _uploadUploadRequest(MediaFileService media) async { final apiAuthTokenRaw = await const FlutterSecureStorage() .read(key: SecureStorageKeys.apiAuthToken); if (apiAuthTokenRaw == null) { @@ -477,108 +207,27 @@ Future handleMediaUpload(MediaUpload media) async { } final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); - final uploadRequestFile = await writeSendMediaFile( - media.mediaUploadId, - 'upload', - uploadRequestBytes, - ); - final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; - try { - Log.info('Starting upload from ${media.mediaUploadId}'); + // try { + Log.info('Starting upload from ${media.mediaFile.mediaId}'); - final task = UploadTask.fromFile( - taskId: 'upload_${media.mediaUploadId}', - displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video', - file: uploadRequestFile, - url: apiUrl, - priority: 0, - retries: 10, - headers: { - 'x-twonly-auth-token': apiAuthToken, - }, - ); - - currentUploadTasks[media.mediaUploadId] = task; - - try { - await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken); - } catch (e) { - Log.error('Fast upload failed: $e. Using slow method directly.'); - await enqueueUploadTask(media.mediaUploadId); - } - } catch (e) { - Log.error('Exception during upload: $e'); - } -} - -Map currentUploadTasks = {}; - -Future enqueueUploadTask(int mediaUploadId) async { - if (currentUploadTasks[mediaUploadId] == null) { - Log.info('could not enqueue upload task: $mediaUploadId'); - return; - } - - Log.info('Enqueue upload task: $mediaUploadId'); - - await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!); - currentUploadTasks.remove(mediaUploadId); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - const MediaUploadsCompanion( - state: Value(UploadState.uploadTaskStarted), - ), - ); -} - -Future handleUploadWhenAppGoesBackground() async { - if (currentUploadTasks.keys.isEmpty) { - return; - } - Log.info('App goes into background. Enqueue uploads to the background.'); - final keys = currentUploadTasks.keys.toList(); - for (final key in keys) { - await enqueueUploadTask(key); - } -} - -Future uploadFileFast( - MediaUpload media, - Uint8List uploadRequestFile, - String apiUrl, - String apiAuthToken, -) async { - final requestMultipart = http.MultipartRequest( - 'POST', - Uri.parse(apiUrl), - ); - requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; - - requestMultipart.files.add( - http.MultipartFile.fromBytes( - 'file', - uploadRequestFile, - filename: 'upload', - ), + final task = UploadTask.fromFile( + taskId: 'upload_${media.mediaFile.mediaId}', + displayName: media.mediaFile.type.name, + file: media.uploadRequestPath, + url: apiUrl, + priority: 0, + retries: 10, + headers: { + 'x-twonly-auth-token': apiAuthToken, + }, ); - final response = await requestMultipart.send(); - if (response.statusCode == 200) { - Log.info('Upload successful!'); - await handleUploadSuccess(media); - return; - } else if (response.statusCode == 429) { - await twonlyDB.mediaFilesDao.updateMedia( - media.mediaId, - const MediaFilesCompanion( - uploadState: Value(UploadState.uploadLimitReached), - ), - ); - } else { - Log.info('Upload failed with status: ${response.statusCode}'); - } + Log.info('Enqueue upload task: ${task.taskId}'); + + await FileDownloader().enqueue(task); + + await media.setUploadState(UploadState.backgroundUploadTaskStarted); } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index a76fcc3..c7a84dd 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -30,18 +30,20 @@ Future tryTransmitMessages() async { }); } -Future tryToSendCompleteMessage({ +// When the ackByServerAt is set this value is written in the receipted +Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ String? receiptId, Receipt? receipt, bool reupload = false, + bool onlyReturnEncryptedData = false, }) async { try { - if (receiptId == null && receipt == null) return; + if (receiptId == null && receipt == null) return null; if (receipt == null) { receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); if (receipt == null) { Log.error('Receipt $receiptId not found.'); - return; + return null; } } receiptId = receipt.receiptId; @@ -55,9 +57,9 @@ Future tryToSendCompleteMessage({ ); } - if (receipt.ackByServerAt != null) { + if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { Log.error('$receiptId message already uploaded!'); - return; + return null; } Log.info('Uploading $receiptId (Message to ${receipt.contactId})'); @@ -86,7 +88,7 @@ Future tryToSendCompleteMessage({ ); if (cipherText == null) { Log.error('Could not encrypt the message. Aborting and trying again.'); - return; + return null; } message.encryptedContent = cipherText.serialize(); switch (cipherText.getType()) { @@ -96,10 +98,14 @@ Future tryToSendCompleteMessage({ message.type = pb.Message_Type.CIPHERTEXT; default: Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); - return; + return null; } } + if (onlyReturnEncryptedData) { + return (message.writeToBuffer(), pushData); + } + final resp = await apiService.sendTextMessage( receipt.contactId, message.writeToBuffer(), @@ -114,7 +120,7 @@ Future tryToSendCompleteMessage({ receipt.contactId, const ContactsCompanion(deleted: Value(true)), ); - return; + return null; } } @@ -149,12 +155,14 @@ Future tryToSendCompleteMessage({ await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); } } + return null; } -Future sendCipherText( +Future<(Uint8List, Uint8List?)?> sendCipherText( int contactId, - pb.EncryptedContent encryptedContent, -) async { + pb.EncryptedContent encryptedContent, { + bool onlyReturnEncryptedData = false, +}) async { final response = pb.Message() ..type = pb.Message_Type.CIPHERTEXT ..encryptedContent = encryptedContent.writeToBuffer(); @@ -163,12 +171,17 @@ Future sendCipherText( ReceiptsCompanion( contactId: Value(contactId), message: Value(response.writeToBuffer()), + ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), ), ); if (receipt != null) { - await tryToSendCompleteMessage(receipt: receipt); + return tryToSendCompleteMessage( + receipt: receipt, + onlyReturnEncryptedData: onlyReturnEncryptedData, + ); } + return null; } Future notifyContactAboutOpeningMessage( diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 13ce178..2a062d2 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -49,6 +49,26 @@ class MediaFileService { await updateFromDB(); } + Future setUploadState(UploadState uploadState) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + uploadState: Value(uploadState), + ), + ); + await updateFromDB(); + } + + Future setEncryptedMac(Uint8List encryptionMac) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + encryptionMac: Value(encryptionMac), + ), + ); + await updateFromDB(); + } + Future setRequiresAuth(bool requiresAuthentication) async { await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, @@ -98,7 +118,8 @@ class MediaFileService { encryptedPath, originalPath, storedPath, - thumbnailPath + thumbnailPath, + uploadRequestPath ]; for (final path in pathsToRemove) { @@ -160,6 +181,10 @@ class MediaFileService { 'tmp', namePrefix: '.encrypted', ); + File get uploadRequestPath => _buildFilePath( + 'tmp', + namePrefix: '.upload', + ); File get originalPath => _buildFilePath( 'tmp', namePrefix: '.original', diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index a675212..360a375 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -8,12 +8,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.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/model/protobuf/client/generated/push_notification.pbenum.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart' - show gMediaShowInfinite; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -34,7 +31,7 @@ Future customLocalPushNotification(String title, String msg) async { ); await flutterLocalNotificationsPlugin.show( - gMediaShowInfinite + Random.secure().nextInt(9999), + Random.secure().nextInt(9999), title, msg, notificationDetails, diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 5a6426e..630ef5d 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -4,9 +4,9 @@ import 'package:drift/drift.dart'; import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future enableTwonlySafe(String password) async { diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 097af4a..138c623 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -14,9 +14,9 @@ import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart'; diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index b10f65f..6824617 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,30 +1,22 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:path/path.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; -import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ required this.getMergedImage, required this.isLoading, required this.displayButtonLabel, + required this.mediaService, super.key, - this.mediaUploadId, - this.videoFilePath, }); final Future Function() getMergedImage; final bool displayButtonLabel; - final File? videoFilePath; - final int? mediaUploadId; + final MediaFileService mediaService; final bool isLoading; @override @@ -54,44 +46,20 @@ class SaveToGalleryButtonState extends State { }); String? res; - var memoryPath = await getMediaBaseFilePath('memories'); - if (widget.mediaUploadId != null) { - memoryPath = join(memoryPath, '${widget.mediaUploadId!}'); - } else { - final random = Random(); - final token = uint8ListToHex( - List.generate(32, (i) => random.nextInt(256)), - ); - memoryPath = join(memoryPath, token); - } - final user = await getUser(); - if (user == null) return; - final storeToGallery = user.storeMediaFilesInGallery; + final storedMediaPath = widget.mediaService.storedPath; - if (widget.videoFilePath != null) { - memoryPath += '.mp4'; - await File(widget.videoFilePath!.path).copy(memoryPath); - unawaited(createThumbnailsForVideo(File(memoryPath))); - if (storeToGallery) { - res = await saveVideoToGallery(widget.videoFilePath!.path); - } - } else { - final imageBytes = await widget.getMergedImage(); - if (imageBytes == null || !mounted) return; - final webPImageBytes = - await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - imageBytes, - quality: 100, - ); - memoryPath += '.png'; - await File(memoryPath).writeAsBytes(webPImageBytes); - unawaited(createThumbnailsForImage(File(memoryPath))); - if (storeToGallery) { - res = await saveImageToGallery(imageBytes); - } + final storeToGallery = gUser.storeMediaFilesInGallery; + + await widget.mediaService.storeMediaFile(); + + if (storeToGallery) { + res = await saveVideoToGallery(storedMediaPath.path); } + + await widget.mediaService.compressMedia(); + await widget.mediaService.createThumbnail(); + if (res == null) { setState(() { _imageSaved = true; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 20eb318..b4bb4e4 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,7 +8,6 @@ import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; @@ -92,9 +90,9 @@ class CameraPreviewControllerView extends StatelessWidget { required this.selectedCameraDetails, required this.screenshotController, super.key, - this.sendTo, + this.sendToGroup, }); - final Contact? sendTo; + final Group? sendToGroup; final Future Function( int sCameraId, bool init, @@ -112,7 +110,7 @@ class CameraPreviewControllerView extends StatelessWidget { if (snap.hasData) { if (snap.data!) { return CameraPreviewView( - sendTo: sendTo, + sendToGroup: sendToGroup, selectCamera: selectCamera, cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, @@ -141,9 +139,9 @@ class CameraPreviewView extends StatefulWidget { required this.selectedCameraDetails, required this.screenshotController, super.key, - this.sendTo, + this.sendToGroup, }); - final Contact? sendTo; + final Group? sendToGroup; final Future Function( int sCameraId, bool init, @@ -328,7 +326,7 @@ class _CameraPreviewViewState extends State { pageBuilder: (context, a1, a2) => ShareImageEditorView( imageBytesFuture: imageBytes, sharedFromGallery: sharedFromGallery, - sendTo: widget.sendTo, + sendToGroup: widget.sendToGroup, mediaFileService: mediaFileService, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { @@ -347,7 +345,7 @@ class _CameraPreviewViewState extends State { if (!mounted) return true; // shouldReturn is null when the user used the back button if (shouldReturn != null && shouldReturn) { - if (widget.sendTo == null) { + if (widget.sendToGroup == null) { globalUpdateOfHomeViewPageIndex(0); } else { Navigator.pop(context); @@ -476,19 +474,10 @@ class _CameraPreviewViewState extends State { }); try { - File? videoPathFile; final videoPath = await widget.cameraController?.stopVideoRecording(); - if (videoPath != null) { - if (Platform.isAndroid) { - // see https://github.com/flutter/flutter/issues/148335 - await File(videoPath.path).rename('${videoPath.path}.mp4'); - videoPathFile = File('${videoPath.path}.mp4'); - } else { - videoPathFile = File(videoPath.path); - } - } + if (videoPath == null) return; await widget.cameraController?.pausePreview(); - if (await pushMediaEditor(null, videoPathFile)) { + if (await pushMediaEditor(null, File(videoPath.path))) { return; } } on CameraException catch (e) { @@ -568,9 +557,9 @@ class _CameraPreviewViewState extends State { ), ), if (!sharePreviewIsShown && - widget.sendTo != null && + widget.sendToGroup != null && !isVideoRecording) - SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), + SendToWidget(sendTo: widget.sendToGroup!.groupName), if (!sharePreviewIsShown && !isVideoRecording) Positioned( right: 5, @@ -722,7 +711,7 @@ class _CameraPreviewViewState extends State { videoRecordingStarted: videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!sharePreviewIsShown && widget.sendTo != null) + if (!sharePreviewIsShown && widget.sendToGroup != null) Positioned( left: 5, top: 10, diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index e00d990..8586b4d 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -8,8 +8,8 @@ import 'package:twonly/src/views/camera/camera_preview_components/camera_preview import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; class CameraSendToView extends StatefulWidget { - const CameraSendToView(this.sendTo, {super.key}); - final Contact sendTo; + const CameraSendToView(this.sendToGroup, {super.key}); + final Group sendToGroup; @override State createState() => CameraSendToViewState(); } @@ -77,7 +77,7 @@ class CameraSendToViewState extends State { ), CameraPreviewControllerView( selectCamera: selectCamera, - sendTo: widget.sendTo, + sendToGroup: widget.sendToGroup, cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 94fbd49..ff66dc6 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,16 +2,11 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:io'; -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; @@ -28,25 +23,22 @@ import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/camera/share_image_view.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:video_player/video_player.dart'; List layers = []; List undoLayers = []; List removedLayers = []; -const gMediaShowInfinite = 999999; - class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ required this.sharedFromGallery, required this.mediaFileService, super.key, this.imageBytesFuture, - this.sendTo, + this.sendToGroup, }); final Future? imageBytesFuture; - final Group? sendTo; + final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; @override @@ -66,9 +58,6 @@ class _ShareImageEditorView extends State { ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); - /// Media upload variables - Future? videoUploadHandler; - MediaFileService get mediaService => widget.mediaFileService; MediaFile get media => widget.mediaFileService.mediaFile; @@ -78,8 +67,8 @@ class _ShareImageEditorView extends State { layers.add(FilterLayerData()); - if (widget.sendTo != null) { - selectedGroupIds.add(widget.sendTo!.groupId); + if (widget.sendToGroup != null) { + selectedGroupIds.add(widget.sendToGroup!.groupId); } if (widget.imageBytesFuture != null) { @@ -284,17 +273,18 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - final imageBytes = storeImageAsOriginal(); + final mediaStoreFuture = + (media.type == MediaType.image) ? storeImageAsOriginal() : null; + await videoController?.pause(); if (isDisposed || !mounted) return; final wasSend = await Navigator.push( context, MaterialPageRoute( builder: (context) => ShareImageView( - imageBytesFuture: imageBytes, - selectedUserIds: selectedGroupIds, - updateStatus: updateSelectedGroupIds, - videoUploadHandler: videoUploadHandler, + selectedGroupIds: selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + mediaStoreFuture: mediaStoreFuture, mediaFileService: mediaService, ), ), @@ -306,11 +296,11 @@ class _ShareImageEditorView extends State { } } - Future storeImageAsOriginal() async { + Future getEditedImageBytes() async { if (layers.length == 1) { if (layers.first is BackgroundLayerData) { final image = (layers.first as BackgroundLayerData).image.bytes; - mediaService.originalPath.writeAsBytesSync(image); + return image; } } @@ -324,24 +314,31 @@ class _ShareImageEditorView extends State { ); if (image == null) { Log.error('screenshotController did not return image bytes'); - return; - } - - mediaService.originalPath.writeAsBytesSync(image); - - // In case the image was already stored, then rename the stored image. - - if (mediaService.storedPath.existsSync()) { - final newPath = mediaService.storedPath.absolute.path - .replaceFirst(media.mediaId, uuid.v7()); - mediaService.storedPath.renameSync(newPath); + return null; } for (final x in layers) { x.showCustomButtons = true; } setState(() {}); + return image; } + + return null; + } + + Future storeImageAsOriginal() async { + final imageBytes = await getEditedImageBytes(); + if (imageBytes == null) return false; + mediaService.originalPath.writeAsBytesSync(imageBytes); + + // In case the image was already stored, then rename the stored image. + if (mediaService.storedPath.existsSync()) { + final newPath = mediaService.storedPath.absolute.path + .replaceFirst(media.mediaId, uuid.v7()); + mediaService.storedPath.renameSync(newPath); + } + return true; } Future loadImage(Future imageBytesFuture) async { @@ -377,14 +374,10 @@ class _ShareImageEditorView extends State { if (!context.mounted) return; - // first finalize the upload - await finalizeUpload(mediaService, [widget.sendTo!.groupId]); - - /// then call the upload process in the background - await encryptMediaFiles( - mediaUploadId!, - imageHandler, - videoUploadHandler, + // Insert media file into the messages database and start uploading process in the background + await insertMediaFileInMessagesTable( + mediaService, + [widget.sendToGroup!.groupId], ); if (context.mounted) { @@ -434,14 +427,13 @@ class _ShareImageEditorView extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SaveToGalleryButton( - getMergedImage: getMergedImage, - mediaUploadId: mediaUploadId, - videoFilePath: widget.videoFilePath, - displayButtonLabel: widget.sendTo == null, + getMergedImage: getEditedImageBytes, + mediaService: mediaService, + displayButtonLabel: widget.sendToGroup == null, isLoading: loadingImage, ), - if (widget.sendTo != null) const SizedBox(width: 10), - if (widget.sendTo != null) + if (widget.sendToGroup != null) const SizedBox(width: 10), + if (widget.sendToGroup != null) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: Theme.of(context).colorScheme.primary, @@ -451,7 +443,7 @@ class _ShareImageEditorView extends State { onPressed: pushShareImageView, child: const FaIcon(FontAwesomeIcons.userPlus), ), - SizedBox(width: widget.sendTo == null ? 20 : 10), + SizedBox(width: widget.sendToGroup == null ? 20 : 10), FilledButton.icon( icon: sendingOrLoadingImage ? SizedBox( @@ -467,7 +459,8 @@ class _ShareImageEditorView extends State { : const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { if (sendingOrLoadingImage) return; - if (widget.sendTo == null) return pushShareImageView(); + if (widget.sendToGroup == null) + return pushShareImageView(); await sendImageToSinglePerson(); }, style: ButtonStyle( @@ -479,9 +472,9 @@ class _ShareImageEditorView extends State { ), ), label: Text( - (widget.sendTo == null) + (widget.sendToGroup == null) ? context.lang.shareImagedEditorShareWith - : getContactDisplayName(widget.sendTo!), + : widget.sendToGroup!.groupName, style: const TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 22fc367..13629ef 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -21,19 +22,15 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ - required this.imageBytesFuture, - required this.selectedUserIds, - required this.updateStatus, - required this.videoUploadHandler, + required this.selectedGroupIds, + required this.updateSelectedGroupIds, + required this.mediaStoreFuture, required this.mediaFileService, super.key, - this.enableVideoAudio, }); - final Future imageBytesFuture; - final HashSet selectedUserIds; - final bool? enableVideoAudio; - final void Function(int, bool) updateStatus; - final Future? videoUploadHandler; + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; + final Future? mediaStoreFuture; final MediaFileService mediaFileService; @override @@ -69,17 +66,11 @@ class _ShareImageView extends State { } Future initAsync() async { - imageBytes = await widget.imageBytesFuture; - if (imageBytes != null) { - final imageHandler = - addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!); - // start with the pre upload of the media file... - await encryptMediaFiles( - widget.mediaUploadId, - imageHandler, - widget.videoUploadHandler, - ); + if (widget.mediaStoreFuture != null) { + await widget.mediaStoreFuture; } + await widget.mediaFileService.setUploadState(UploadState.preprocessing); + unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); } From 1c154e6c6702fc55786e7a127b0c9cfebc1d2ea6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 23 Oct 2025 22:42:15 +0200 Subject: [PATCH 08/76] add message action table --- lib/src/database/daos/contacts.dao.dart | 4 + lib/src/database/daos/groups.dao.dart | 13 + lib/src/database/daos/messages.dao.dart | 48 +- lib/src/database/daos/messages.dao.g.dart | 1 + lib/src/database/daos/receipts.dao.dart | 16 +- lib/src/database/daos/receipts.dao.g.dart | 1 + lib/src/database/tables/messages.table.dart | 42 +- lib/src/database/twonly.db.dart | 1 + lib/src/database/twonly.db.g.dart | 1116 ++++++++++++----- .../mediafiles/media_background.service.dart | 22 +- lib/src/services/api/messages.dart | 39 +- lib/src/services/api/server_messages.dart | 1 - .../media.server_messages.dart | 2 - .../messages.server_messages.dart | 3 +- .../text_message.server_messages.dart | 2 - .../developer/automated_testing.view.dart | 68 +- .../developer/retransmission_data.view.dart | 93 +- .../views/settings/help/contact_us.view.dart | 4 +- 18 files changed, 997 insertions(+), 479 deletions(-) diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 54e70ab..58e6b69 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -97,6 +97,10 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { return select(contacts)..where((t) => t.userId.equals(userId)); } + Future> getContactsByUsername(String username) async { + return (select(contacts)..where((t) => t.username.equals(username))).get(); + } + Future deleteContactByUserId(int userId) { return (delete(contacts)..where((t) => t.userId.equals(userId))).go(); } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 3747ab4..6456434 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -31,4 +31,17 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) .get(); } + + Future> getDirectChat(int userId) async { + final query = (select(groups).join([ + leftOuterJoin( + groupMembers, + groupMembers.groupId.equalsExp(groups.groupId) & + groupMembers.contactId.equals(userId), + ), + ]) + ..where(groups.isGroupOfTwo.equals(true))); + + return query.map((row) => row.readTable(groups)).get(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 0483dd8..7a0726f 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -16,6 +16,7 @@ part 'messages.dao.g.dart'; Contacts, MediaFiles, MessageHistories, + MessageActions, Groups, ], ) @@ -192,11 +193,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), )) .write( - MessagesCompanion( - isDeletedFromSender: const Value(true), - content: const Value(null), - modifiedAt: Value(timestamp), - mediaId: const Value(null), + const MessagesCompanion( + isDeletedFromSender: Value(true), + content: Value(null), + mediaId: Value(null), ), ); } @@ -215,6 +215,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { MessageHistoriesCompanion( messageId: Value(messageId), content: Value(msg.content), + createdAt: Value(timestamp), ), ); await (update(messages) @@ -224,29 +225,36 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .write( MessagesCompanion( content: Value(text), - modifiedAt: Value(timestamp), ), ); } Future handleMessageOpened( - String groupId, + int contactId, String messageId, DateTime timestamp, ) async { - final msg = await getMessageById(messageId).getSingleOrNull(); - if (msg == null) return; - await (update(messages) - ..where( - (t) => - t.groupId.equals(groupId) & - t.messageId.equals(messageId) & - t.senderId.isNull(), - )) - .write( - MessagesCompanion( - openedAt: Value(timestamp), - openedByCounter: Value(msg.openedByCounter + 1), + await into(messageActions).insert( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.ackByUserAt), + actionAt: Value(timestamp), + ), + ); + } + + Future handleMessageAckByServer( + int contactId, + String messageId, + DateTime timestamp, + ) async { + await into(messageActions).insert( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.ackByServerAt), + actionAt: Value(timestamp), ), ); } diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart index e763f72..74bd53f 100644 --- a/lib/src/database/daos/messages.dao.g.dart +++ b/lib/src/database/daos/messages.dao.g.dart @@ -10,4 +10,5 @@ mixin _$MessagesDaoMixin on DatabaseAccessor { $MessagesTable get messages => attachedDatabase.messages; $MessageHistoriesTable get messageHistories => attachedDatabase.messageHistories; + $MessageActionsTable get messageActions => attachedDatabase.messageActions; } diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index df65eb5..01df2f9 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -6,7 +6,7 @@ import 'package:twonly/src/utils/log.dart'; part 'receipts.dao.g.dart'; -@DriftAccessor(tables: [Receipts, Messages]) +@DriftAccessor(tables: [Receipts, Messages, MessageActions]) class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -24,11 +24,11 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { if (receipt == null) return; if (receipt.messageId != null) { - await (update(messages) - ..where((t) => t.messageId.equals(receipt.messageId!))) - .write( - const MessagesCompanion( - ackByUser: Value(true), + await into(messageActions).insert( + MessageActionsCompanion( + messageId: Value(receipt.messageId!), + contactId: Value(fromUserId), + type: const Value(MessageActionType.ackByUserAt), ), ); } @@ -81,6 +81,10 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { .get(); } + Stream> watchAll() { + return select(receipts).watch(); + } + Future updateReceipt( String receiptId, ReceiptsCompanion updates, diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart index 5a06998..d495737 100644 --- a/lib/src/database/daos/receipts.dao.g.dart +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -9,4 +9,5 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor { $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; $ReceiptsTable get receipts => attachedDatabase.receipts; + $MessageActionsTable get messageActions => attachedDatabase.messageActions; } diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 28438c8..faf0053 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -30,28 +30,50 @@ class Messages extends Table { BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); - BoolColumn get ackByUser => boolean().withDefault(const Constant(false))(); - BoolColumn get ackByServer => boolean().withDefault(const Constant(false))(); - - IntColumn get openedByCounter => integer().withDefault(const Constant(0))(); - DateTimeColumn get openedAt => dateTime().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); - DateTimeColumn get modifiedAt => - dateTime().nullable().withDefault(currentDateAndTime)(); @override Set get primaryKey => {messageId}; } -@DataClassName('MessageHistory') -class MessageHistories extends Table { +enum MessageActionType { + openedAt, + modifiedAt, + ackByUserAt, + ackByServerAt, +} + +@DataClassName('MessageAction') +class MessageActions extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get messageId => text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + IntColumn get contactId => + integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + + TextColumn get type => textEnum()(); + DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {id}; +} + +@DataClassName('MessageHistory') +class MessageHistories extends Table { + IntColumn get id => integer().autoIncrement()(); + + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + IntColumn get contactId => + integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + TextColumn get content => text().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @override - Set get primaryKey => {messageId, createdAt}; + Set get primaryKey => {id}; } diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 1594ce8..1e121c1 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -42,6 +42,7 @@ part 'twonly.db.g.dart'; SignalSessionStores, SignalContactPreKeys, SignalContactSignedPreKeys, + MessageActions ], daos: [ MessagesDao, diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 118a6d5..69e7a22 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2314,40 +2314,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("is_edited" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _ackByUserMeta = - const VerificationMeta('ackByUser'); - @override - late final GeneratedColumn ackByUser = GeneratedColumn( - 'ack_by_user', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("ack_by_user" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _ackByServerMeta = - const VerificationMeta('ackByServer'); - @override - late final GeneratedColumn ackByServer = GeneratedColumn( - 'ack_by_server', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("ack_by_server" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _openedByCounterMeta = - const VerificationMeta('openedByCounter'); - @override - late final GeneratedColumn openedByCounter = GeneratedColumn( - 'opened_by_counter', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _openedAtMeta = - const VerificationMeta('openedAt'); - @override - late final GeneratedColumn openedAt = GeneratedColumn( - 'opened_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -2356,14 +2322,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); - static const VerificationMeta _modifiedAtMeta = - const VerificationMeta('modifiedAt'); - @override - late final GeneratedColumn modifiedAt = GeneratedColumn( - 'modified_at', aliasedName, true, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); @override List get $columns => [ groupId, @@ -2376,12 +2334,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { quotesMessageId, isDeletedFromSender, isEdited, - ackByUser, - ackByServer, - openedByCounter, - openedAt, - createdAt, - modifiedAt + createdAt ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2443,38 +2396,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_isEditedMeta, isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta)); } - if (data.containsKey('ack_by_user')) { - context.handle( - _ackByUserMeta, - ackByUser.isAcceptableOrUnknown( - data['ack_by_user']!, _ackByUserMeta)); - } - if (data.containsKey('ack_by_server')) { - context.handle( - _ackByServerMeta, - ackByServer.isAcceptableOrUnknown( - data['ack_by_server']!, _ackByServerMeta)); - } - if (data.containsKey('opened_by_counter')) { - context.handle( - _openedByCounterMeta, - openedByCounter.isAcceptableOrUnknown( - data['opened_by_counter']!, _openedByCounterMeta)); - } - if (data.containsKey('opened_at')) { - context.handle(_openedAtMeta, - openedAt.isAcceptableOrUnknown(data['opened_at']!, _openedAtMeta)); - } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } - if (data.containsKey('modified_at')) { - context.handle( - _modifiedAtMeta, - modifiedAt.isAcceptableOrUnknown( - data['modified_at']!, _modifiedAtMeta)); - } return context; } @@ -2504,18 +2429,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, isEdited: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}is_edited'])!, - ackByUser: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}ack_by_user'])!, - ackByServer: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}ack_by_server'])!, - openedByCounter: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}opened_by_counter'])!, - openedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - modifiedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), ); } @@ -2536,12 +2451,7 @@ class Message extends DataClass implements Insertable { final String? quotesMessageId; final bool isDeletedFromSender; final bool isEdited; - final bool ackByUser; - final bool ackByServer; - final int openedByCounter; - final DateTime? openedAt; final DateTime createdAt; - final DateTime? modifiedAt; const Message( {required this.groupId, required this.messageId, @@ -2553,12 +2463,7 @@ class Message extends DataClass implements Insertable { this.quotesMessageId, required this.isDeletedFromSender, required this.isEdited, - required this.ackByUser, - required this.ackByServer, - required this.openedByCounter, - this.openedAt, - required this.createdAt, - this.modifiedAt}); + required this.createdAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2582,16 +2487,7 @@ class Message extends DataClass implements Insertable { } map['is_deleted_from_sender'] = Variable(isDeletedFromSender); map['is_edited'] = Variable(isEdited); - map['ack_by_user'] = Variable(ackByUser); - map['ack_by_server'] = Variable(ackByServer); - map['opened_by_counter'] = Variable(openedByCounter); - if (!nullToAbsent || openedAt != null) { - map['opened_at'] = Variable(openedAt); - } map['created_at'] = Variable(createdAt); - if (!nullToAbsent || modifiedAt != null) { - map['modified_at'] = Variable(modifiedAt); - } return map; } @@ -2617,16 +2513,7 @@ class Message extends DataClass implements Insertable { : Value(quotesMessageId), isDeletedFromSender: Value(isDeletedFromSender), isEdited: Value(isEdited), - ackByUser: Value(ackByUser), - ackByServer: Value(ackByServer), - openedByCounter: Value(openedByCounter), - openedAt: openedAt == null && nullToAbsent - ? const Value.absent() - : Value(openedAt), createdAt: Value(createdAt), - modifiedAt: modifiedAt == null && nullToAbsent - ? const Value.absent() - : Value(modifiedAt), ); } @@ -2645,12 +2532,7 @@ class Message extends DataClass implements Insertable { isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), isEdited: serializer.fromJson(json['isEdited']), - ackByUser: serializer.fromJson(json['ackByUser']), - ackByServer: serializer.fromJson(json['ackByServer']), - openedByCounter: serializer.fromJson(json['openedByCounter']), - openedAt: serializer.fromJson(json['openedAt']), createdAt: serializer.fromJson(json['createdAt']), - modifiedAt: serializer.fromJson(json['modifiedAt']), ); } @override @@ -2667,12 +2549,7 @@ class Message extends DataClass implements Insertable { 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), 'isEdited': serializer.toJson(isEdited), - 'ackByUser': serializer.toJson(ackByUser), - 'ackByServer': serializer.toJson(ackByServer), - 'openedByCounter': serializer.toJson(openedByCounter), - 'openedAt': serializer.toJson(openedAt), 'createdAt': serializer.toJson(createdAt), - 'modifiedAt': serializer.toJson(modifiedAt), }; } @@ -2687,12 +2564,7 @@ class Message extends DataClass implements Insertable { Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, bool? isEdited, - bool? ackByUser, - bool? ackByServer, - int? openedByCounter, - Value openedAt = const Value.absent(), - DateTime? createdAt, - Value modifiedAt = const Value.absent()}) => + DateTime? createdAt}) => Message( groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, @@ -2707,12 +2579,7 @@ class Message extends DataClass implements Insertable { : this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, - ackByUser: ackByUser ?? this.ackByUser, - ackByServer: ackByServer ?? this.ackByServer, - openedByCounter: openedByCounter ?? this.openedByCounter, - openedAt: openedAt.present ? openedAt.value : this.openedAt, createdAt: createdAt ?? this.createdAt, - modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, ); Message copyWithCompanion(MessagesCompanion data) { return Message( @@ -2733,16 +2600,7 @@ class Message extends DataClass implements Insertable { ? data.isDeletedFromSender.value : this.isDeletedFromSender, isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, - ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, - ackByServer: - data.ackByServer.present ? data.ackByServer.value : this.ackByServer, - openedByCounter: data.openedByCounter.present - ? data.openedByCounter.value - : this.openedByCounter, - openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - modifiedAt: - data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, ); } @@ -2759,12 +2617,7 @@ class Message extends DataClass implements Insertable { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') - ..write('ackByUser: $ackByUser, ') - ..write('ackByServer: $ackByServer, ') - ..write('openedByCounter: $openedByCounter, ') - ..write('openedAt: $openedAt, ') - ..write('createdAt: $createdAt, ') - ..write('modifiedAt: $modifiedAt') + ..write('createdAt: $createdAt') ..write(')')) .toString(); } @@ -2781,12 +2634,7 @@ class Message extends DataClass implements Insertable { quotesMessageId, isDeletedFromSender, isEdited, - ackByUser, - ackByServer, - openedByCounter, - openedAt, - createdAt, - modifiedAt); + createdAt); @override bool operator ==(Object other) => identical(this, other) || @@ -2801,12 +2649,7 @@ class Message extends DataClass implements Insertable { other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && other.isEdited == this.isEdited && - other.ackByUser == this.ackByUser && - other.ackByServer == this.ackByServer && - other.openedByCounter == this.openedByCounter && - other.openedAt == this.openedAt && - other.createdAt == this.createdAt && - other.modifiedAt == this.modifiedAt); + other.createdAt == this.createdAt); } class MessagesCompanion extends UpdateCompanion { @@ -2820,12 +2663,7 @@ class MessagesCompanion extends UpdateCompanion { final Value quotesMessageId; final Value isDeletedFromSender; final Value isEdited; - final Value ackByUser; - final Value ackByServer; - final Value openedByCounter; - final Value openedAt; final Value createdAt; - final Value modifiedAt; final Value rowid; const MessagesCompanion({ this.groupId = const Value.absent(), @@ -2838,12 +2676,7 @@ class MessagesCompanion extends UpdateCompanion { this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), - this.ackByUser = const Value.absent(), - this.ackByServer = const Value.absent(), - this.openedByCounter = const Value.absent(), - this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), - this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), }); MessagesCompanion.insert({ @@ -2857,12 +2690,7 @@ class MessagesCompanion extends UpdateCompanion { this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), - this.ackByUser = const Value.absent(), - this.ackByServer = const Value.absent(), - this.openedByCounter = const Value.absent(), - this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), - this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId); static Insertable custom({ @@ -2876,12 +2704,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? quotesMessageId, Expression? isDeletedFromSender, Expression? isEdited, - Expression? ackByUser, - Expression? ackByServer, - Expression? openedByCounter, - Expression? openedAt, Expression? createdAt, - Expression? modifiedAt, Expression? rowid, }) { return RawValuesInsertable({ @@ -2896,12 +2719,7 @@ class MessagesCompanion extends UpdateCompanion { if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, if (isEdited != null) 'is_edited': isEdited, - if (ackByUser != null) 'ack_by_user': ackByUser, - if (ackByServer != null) 'ack_by_server': ackByServer, - if (openedByCounter != null) 'opened_by_counter': openedByCounter, - if (openedAt != null) 'opened_at': openedAt, if (createdAt != null) 'created_at': createdAt, - if (modifiedAt != null) 'modified_at': modifiedAt, if (rowid != null) 'rowid': rowid, }); } @@ -2917,12 +2735,7 @@ class MessagesCompanion extends UpdateCompanion { Value? quotesMessageId, Value? isDeletedFromSender, Value? isEdited, - Value? ackByUser, - Value? ackByServer, - Value? openedByCounter, - Value? openedAt, Value? createdAt, - Value? modifiedAt, Value? rowid}) { return MessagesCompanion( groupId: groupId ?? this.groupId, @@ -2935,12 +2748,7 @@ class MessagesCompanion extends UpdateCompanion { quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, - ackByUser: ackByUser ?? this.ackByUser, - ackByServer: ackByServer ?? this.ackByServer, - openedByCounter: openedByCounter ?? this.openedByCounter, - openedAt: openedAt ?? this.openedAt, createdAt: createdAt ?? this.createdAt, - modifiedAt: modifiedAt ?? this.modifiedAt, rowid: rowid ?? this.rowid, ); } @@ -2978,24 +2786,9 @@ class MessagesCompanion extends UpdateCompanion { if (isEdited.present) { map['is_edited'] = Variable(isEdited.value); } - if (ackByUser.present) { - map['ack_by_user'] = Variable(ackByUser.value); - } - if (ackByServer.present) { - map['ack_by_server'] = Variable(ackByServer.value); - } - if (openedByCounter.present) { - map['opened_by_counter'] = Variable(openedByCounter.value); - } - if (openedAt.present) { - map['opened_at'] = Variable(openedAt.value); - } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } - if (modifiedAt.present) { - map['modified_at'] = Variable(modifiedAt.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -3015,12 +2808,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') - ..write('ackByUser: $ackByUser, ') - ..write('ackByServer: $ackByServer, ') - ..write('openedByCounter: $openedByCounter, ') - ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') - ..write('modifiedAt: $modifiedAt, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -3033,6 +2821,15 @@ class $MessageHistoriesTable extends MessageHistories final GeneratedDatabase attachedDatabase; final String? _alias; $MessageHistoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @override @@ -3042,6 +2839,12 @@ class $MessageHistoriesTable extends MessageHistories requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -3057,7 +2860,8 @@ class $MessageHistoriesTable extends MessageHistories requiredDuringInsert: false, defaultValue: currentDateAndTime); @override - List get $columns => [messageId, content, createdAt]; + List get $columns => + [id, messageId, contactId, content, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -3068,12 +2872,21 @@ class $MessageHistoriesTable extends MessageHistories {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } if (data.containsKey('message_id')) { context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } else if (isInserting) { context.missing(_messageIdMeta); } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } if (data.containsKey('content')) { context.handle(_contentMeta, content.isAcceptableOrUnknown(data['content']!, _contentMeta)); @@ -3086,13 +2899,17 @@ class $MessageHistoriesTable extends MessageHistories } @override - Set get $primaryKey => {messageId, createdAt}; + Set get $primaryKey => {id}; @override MessageHistory map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MessageHistory( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, messageId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, content: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}content']), createdAt: attachedDatabase.typeMapping @@ -3107,15 +2924,23 @@ class $MessageHistoriesTable extends MessageHistories } class MessageHistory extends DataClass implements Insertable { + final int id; final String messageId; + final int contactId; final String? content; final DateTime createdAt; const MessageHistory( - {required this.messageId, this.content, required this.createdAt}); + {required this.id, + required this.messageId, + required this.contactId, + this.content, + required this.createdAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; + map['id'] = Variable(id); map['message_id'] = Variable(messageId); + map['contact_id'] = Variable(contactId); if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -3125,7 +2950,9 @@ class MessageHistory extends DataClass implements Insertable { MessageHistoriesCompanion toCompanion(bool nullToAbsent) { return MessageHistoriesCompanion( + id: Value(id), messageId: Value(messageId), + contactId: Value(contactId), content: content == null && nullToAbsent ? const Value.absent() : Value(content), @@ -3137,7 +2964,9 @@ class MessageHistory extends DataClass implements Insertable { {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return MessageHistory( + id: serializer.fromJson(json['id']), messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), content: serializer.fromJson(json['content']), createdAt: serializer.fromJson(json['createdAt']), ); @@ -3146,24 +2975,32 @@ class MessageHistory extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'id': serializer.toJson(id), 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), 'content': serializer.toJson(content), 'createdAt': serializer.toJson(createdAt), }; } MessageHistory copyWith( - {String? messageId, + {int? id, + String? messageId, + int? contactId, Value content = const Value.absent(), DateTime? createdAt}) => MessageHistory( + id: id ?? this.id, messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, content: content.present ? content.value : this.content, createdAt: createdAt ?? this.createdAt, ); MessageHistory copyWithCompanion(MessageHistoriesCompanion data) { return MessageHistory( + id: data.id.present ? data.id.value : this.id, messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, content: data.content.present ? data.content.value : this.content, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); @@ -3172,7 +3009,9 @@ class MessageHistory extends DataClass implements Insertable { @override String toString() { return (StringBuffer('MessageHistory(') + ..write('id: $id, ') ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') ..write('content: $content, ') ..write('createdAt: $createdAt') ..write(')')) @@ -3180,85 +3019,99 @@ class MessageHistory extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(messageId, content, createdAt); + int get hashCode => Object.hash(id, messageId, contactId, content, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is MessageHistory && + other.id == this.id && other.messageId == this.messageId && + other.contactId == this.contactId && other.content == this.content && other.createdAt == this.createdAt); } class MessageHistoriesCompanion extends UpdateCompanion { + final Value id; final Value messageId; + final Value contactId; final Value content; final Value createdAt; - final Value rowid; const MessageHistoriesCompanion({ + this.id = const Value.absent(), this.messageId = const Value.absent(), + this.contactId = const Value.absent(), this.content = const Value.absent(), this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), }); MessageHistoriesCompanion.insert({ + this.id = const Value.absent(), required String messageId, + required int contactId, this.content = const Value.absent(), this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), - }) : messageId = Value(messageId); + }) : messageId = Value(messageId), + contactId = Value(contactId); static Insertable custom({ + Expression? id, Expression? messageId, + Expression? contactId, Expression? content, Expression? createdAt, - Expression? rowid, }) { return RawValuesInsertable({ + if (id != null) 'id': id, if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, if (content != null) 'content': content, if (createdAt != null) 'created_at': createdAt, - if (rowid != null) 'rowid': rowid, }); } MessageHistoriesCompanion copyWith( - {Value? messageId, + {Value? id, + Value? messageId, + Value? contactId, Value? content, - Value? createdAt, - Value? rowid}) { + Value? createdAt}) { return MessageHistoriesCompanion( + id: id ?? this.id, messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, content: content ?? this.content, createdAt: createdAt ?? this.createdAt, - rowid: rowid ?? this.rowid, ); } @override Map toColumns(bool nullToAbsent) { final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } if (messageId.present) { map['message_id'] = Variable(messageId.value); } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } if (content.present) { map['content'] = Variable(content.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } return map; } @override String toString() { return (StringBuffer('MessageHistoriesCompanion(') + ..write('id: $id, ') ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') ..write('content: $content, ') - ..write('createdAt: $createdAt, ') - ..write('rowid: $rowid') + ..write('createdAt: $createdAt') ..write(')')) .toString(); } @@ -5928,6 +5781,311 @@ class SignalContactSignedPreKeysCompanion } } +class $MessageActionsTable extends MessageActions + with TableInfo<$MessageActionsTable, MessageAction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessageActionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $MessageActionsTable.$convertertype); + static const VerificationMeta _actionAtMeta = + const VerificationMeta('actionAt'); + @override + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, messageId, contactId, type, actionAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_actions'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('action_at')) { + context.handle(_actionAtMeta, + actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + MessageAction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageAction( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + type: $MessageActionsTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + $MessageActionsTable createAlias(String alias) { + return $MessageActionsTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MessageActionType.values); +} + +class MessageAction extends DataClass implements Insertable { + final int id; + final String messageId; + final int contactId; + final MessageActionType type; + final DateTime actionAt; + const MessageAction( + {required this.id, + required this.messageId, + required this.contactId, + required this.type, + required this.actionAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['message_id'] = Variable(messageId); + map['contact_id'] = Variable(contactId); + { + map['type'] = + Variable($MessageActionsTable.$convertertype.toSql(type)); + } + map['action_at'] = Variable(actionAt); + return map; + } + + MessageActionsCompanion toCompanion(bool nullToAbsent) { + return MessageActionsCompanion( + id: Value(id), + messageId: Value(messageId), + contactId: Value(contactId), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory MessageAction.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageAction( + id: serializer.fromJson(json['id']), + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + type: $MessageActionsTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'type': serializer + .toJson($MessageActionsTable.$convertertype.toJson(type)), + 'actionAt': serializer.toJson(actionAt), + }; + } + + MessageAction copyWith( + {int? id, + String? messageId, + int? contactId, + MessageActionType? type, + DateTime? actionAt}) => + MessageAction( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + MessageAction copyWithCompanion(MessageActionsCompanion data) { + return MessageAction( + id: data.id.present ? data.id.value : this.id, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageAction(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, messageId, contactId, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageAction && + other.id == this.id && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class MessageActionsCompanion extends UpdateCompanion { + final Value id; + final Value messageId; + final Value contactId; + final Value type; + final Value actionAt; + const MessageActionsCompanion({ + this.id = const Value.absent(), + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + }); + MessageActionsCompanion.insert({ + this.id = const Value.absent(), + required String messageId, + required int contactId, + required MessageActionType type, + this.actionAt = const Value.absent(), + }) : messageId = Value(messageId), + contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? id, + Expression? messageId, + Expression? contactId, + Expression? type, + Expression? actionAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + }); + } + + MessageActionsCompanion copyWith( + {Value? id, + Value? messageId, + Value? contactId, + Value? type, + Value? actionAt}) { + return MessageActionsCompanion( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable( + $MessageActionsTable.$convertertype.toSql(type.value)); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageActionsCompanion(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } +} + abstract class _$TwonlyDB extends GeneratedDatabase { _$TwonlyDB(QueryExecutor e) : super(e); $TwonlyDBManager get managers => $TwonlyDBManager(this); @@ -5952,6 +6110,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { $SignalContactPreKeysTable(this); late final $SignalContactSignedPreKeysTable signalContactSignedPreKeys = $SignalContactSignedPreKeysTable(this); + late final $MessageActionsTable messageActions = $MessageActionsTable(this); late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); late final SignalDao signalDao = SignalDao(this as TwonlyDB); @@ -5977,7 +6136,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase { signalSenderKeyStores, signalSessionStores, signalContactPreKeys, - signalContactSignedPreKeys + signalContactSignedPreKeys, + messageActions ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( @@ -6039,6 +6199,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_actions', kind: UpdateKind.delete), + ], + ), ], ); } @@ -7657,12 +7824,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value quotesMessageId, Value isDeletedFromSender, Value isEdited, - Value ackByUser, - Value ackByServer, - Value openedByCounter, - Value openedAt, Value createdAt, - Value modifiedAt, Value rowid, }); typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ @@ -7676,12 +7838,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value quotesMessageId, Value isDeletedFromSender, Value isEdited, - Value ackByUser, - Value ackByServer, - Value openedByCounter, - Value openedAt, Value createdAt, - Value modifiedAt, Value rowid, }); @@ -7797,6 +7954,22 @@ final class $$MessagesTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$MessageActionsTable, List> + _messageActionsRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageActions, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.messageActions.messageId)); + + $$MessageActionsTableProcessedTableManager get messageActionsRefs { + final manager = $$MessageActionsTableTableManager($_db, $_db.messageActions) + .filter((f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_messageActionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$MessagesTableFilterComposer @@ -7827,25 +8000,9 @@ class $$MessagesTableFilterComposer ColumnFilters get isEdited => $composableBuilder( column: $table.isEdited, builder: (column) => ColumnFilters(column)); - ColumnFilters get ackByUser => $composableBuilder( - column: $table.ackByUser, builder: (column) => ColumnFilters(column)); - - ColumnFilters get ackByServer => $composableBuilder( - column: $table.ackByServer, builder: (column) => ColumnFilters(column)); - - ColumnFilters get openedByCounter => $composableBuilder( - column: $table.openedByCounter, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get openedAt => $composableBuilder( - column: $table.openedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get modifiedAt => $composableBuilder( - column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); - $$GroupsTableFilterComposer get groupId { final $$GroupsTableFilterComposer composer = $composerBuilder( composer: this, @@ -7988,6 +8145,27 @@ class $$MessagesTableFilterComposer )); return f(composer); } + + Expression messageActionsRefs( + Expression Function($$MessageActionsTableFilterComposer f) f) { + final $$MessageActionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableFilterComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$MessagesTableOrderingComposer @@ -8019,25 +8197,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get isEdited => $composableBuilder( column: $table.isEdited, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ackByUser => $composableBuilder( - column: $table.ackByUser, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get ackByServer => $composableBuilder( - column: $table.ackByServer, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get openedByCounter => $composableBuilder( - column: $table.openedByCounter, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get openedAt => $composableBuilder( - column: $table.openedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get modifiedAt => $composableBuilder( - column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); - $$GroupsTableOrderingComposer get groupId { final $$GroupsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -8146,24 +8308,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get isEdited => $composableBuilder(column: $table.isEdited, builder: (column) => column); - GeneratedColumn get ackByUser => - $composableBuilder(column: $table.ackByUser, builder: (column) => column); - - GeneratedColumn get ackByServer => $composableBuilder( - column: $table.ackByServer, builder: (column) => column); - - GeneratedColumn get openedByCounter => $composableBuilder( - column: $table.openedByCounter, builder: (column) => column); - - GeneratedColumn get openedAt => - $composableBuilder(column: $table.openedAt, builder: (column) => column); - GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get modifiedAt => $composableBuilder( - column: $table.modifiedAt, builder: (column) => column); - $$GroupsTableAnnotationComposer get groupId { final $$GroupsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -8306,6 +8453,27 @@ class $$MessagesTableAnnotationComposer )); return f(composer); } + + Expression messageActionsRefs( + Expression Function($$MessageActionsTableAnnotationComposer a) f) { + final $$MessageActionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableAnnotationComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$MessagesTableTableManager extends RootTableManager< @@ -8326,7 +8494,8 @@ class $$MessagesTableTableManager extends RootTableManager< bool quotesMessageId, bool messageHistoriesRefs, bool reactionsRefs, - bool receiptsRefs})> { + bool receiptsRefs, + bool messageActionsRefs})> { $$MessagesTableTableManager(_$TwonlyDB db, $MessagesTable table) : super(TableManagerState( db: db, @@ -8348,12 +8517,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), - Value ackByUser = const Value.absent(), - Value ackByServer = const Value.absent(), - Value openedByCounter = const Value.absent(), - Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), - Value modifiedAt = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion( @@ -8367,12 +8531,7 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, - ackByUser: ackByUser, - ackByServer: ackByServer, - openedByCounter: openedByCounter, - openedAt: openedAt, createdAt: createdAt, - modifiedAt: modifiedAt, rowid: rowid, ), createCompanionCallback: ({ @@ -8386,12 +8545,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), - Value ackByUser = const Value.absent(), - Value ackByServer = const Value.absent(), - Value openedByCounter = const Value.absent(), - Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), - Value modifiedAt = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion.insert( @@ -8405,12 +8559,7 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, - ackByUser: ackByUser, - ackByServer: ackByServer, - openedByCounter: openedByCounter, - openedAt: openedAt, createdAt: createdAt, - modifiedAt: modifiedAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -8424,13 +8573,15 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId = false, messageHistoriesRefs = false, reactionsRefs = false, - receiptsRefs = false}) { + receiptsRefs = false, + messageActionsRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (messageHistoriesRefs) db.messageHistories, if (reactionsRefs) db.reactions, - if (receiptsRefs) db.receipts + if (receiptsRefs) db.receipts, + if (messageActionsRefs) db.messageActions ], addJoins: < T extends TableManagerState< @@ -8528,6 +8679,19 @@ class $$MessagesTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.messageId == item.messageId), + typedResults: items), + if (messageActionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences + ._messageActionsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .messageActionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), typedResults: items) ]; }, @@ -8554,20 +8718,23 @@ typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< bool quotesMessageId, bool messageHistoriesRefs, bool reactionsRefs, - bool receiptsRefs})>; + bool receiptsRefs, + bool messageActionsRefs})>; typedef $$MessageHistoriesTableCreateCompanionBuilder = MessageHistoriesCompanion Function({ + Value id, required String messageId, + required int contactId, Value content, Value createdAt, - Value rowid, }); typedef $$MessageHistoriesTableUpdateCompanionBuilder = MessageHistoriesCompanion Function({ + Value id, Value messageId, + Value contactId, Value content, Value createdAt, - Value rowid, }); final class $$MessageHistoriesTableReferences @@ -8600,6 +8767,12 @@ class $$MessageHistoriesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnFilters(column)); + ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); @@ -8636,6 +8809,12 @@ class $$MessageHistoriesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); @@ -8672,6 +8851,12 @@ class $$MessageHistoriesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get contactId => + $composableBuilder(column: $table.contactId, builder: (column) => column); + GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); @@ -8723,28 +8908,32 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< createComputedFieldComposer: () => $$MessageHistoriesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ + Value id = const Value.absent(), Value messageId = const Value.absent(), + Value contactId = const Value.absent(), Value content = const Value.absent(), Value createdAt = const Value.absent(), - Value rowid = const Value.absent(), }) => MessageHistoriesCompanion( + id: id, messageId: messageId, + contactId: contactId, content: content, createdAt: createdAt, - rowid: rowid, ), createCompanionCallback: ({ + Value id = const Value.absent(), required String messageId, + required int contactId, Value content = const Value.absent(), Value createdAt = const Value.absent(), - Value rowid = const Value.absent(), }) => MessageHistoriesCompanion.insert( + id: id, messageId: messageId, + contactId: contactId, content: content, createdAt: createdAt, - rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => ( @@ -10965,6 +11154,279 @@ typedef $$SignalContactSignedPreKeysTableProcessedTableManager ), SignalContactSignedPreKey, PrefetchHooks Function({bool contactId})>; +typedef $$MessageActionsTableCreateCompanionBuilder = MessageActionsCompanion + Function({ + Value id, + required String messageId, + required int contactId, + required MessageActionType type, + Value actionAt, +}); +typedef $$MessageActionsTableUpdateCompanionBuilder = MessageActionsCompanion + Function({ + Value id, + Value messageId, + Value contactId, + Value type, + Value actionAt, +}); + +final class $$MessageActionsTableReferences + extends BaseReferences<_$TwonlyDB, $MessageActionsTable, MessageAction> { + $$MessageActionsTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias($_aliasNameGenerator( + db.messageActions.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MessageActionsTableFilterComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableOrderingComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get contactId => + $composableBuilder(column: $table.contactId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get actionAt => + $composableBuilder(column: $table.actionAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessageActionsTable, + MessageAction, + $$MessageActionsTableFilterComposer, + $$MessageActionsTableOrderingComposer, + $$MessageActionsTableAnnotationComposer, + $$MessageActionsTableCreateCompanionBuilder, + $$MessageActionsTableUpdateCompanionBuilder, + (MessageAction, $$MessageActionsTableReferences), + MessageAction, + PrefetchHooks Function({bool messageId})> { + $$MessageActionsTableTableManager(_$TwonlyDB db, $MessageActionsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessageActionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessageActionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessageActionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value messageId = const Value.absent(), + Value contactId = const Value.absent(), + Value type = const Value.absent(), + Value actionAt = const Value.absent(), + }) => + MessageActionsCompanion( + id: id, + messageId: messageId, + contactId: contactId, + type: type, + actionAt: actionAt, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String messageId, + required int contactId, + required MessageActionType type, + Value actionAt = const Value.absent(), + }) => + MessageActionsCompanion.insert( + id: id, + messageId: messageId, + contactId: contactId, + type: type, + actionAt: actionAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MessageActionsTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$MessageActionsTableReferences._messageIdTable(db), + referencedColumn: $$MessageActionsTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MessageActionsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessageActionsTable, + MessageAction, + $$MessageActionsTableFilterComposer, + $$MessageActionsTableOrderingComposer, + $$MessageActionsTableAnnotationComposer, + $$MessageActionsTableCreateCompanionBuilder, + $$MessageActionsTableUpdateCompanionBuilder, + (MessageAction, $$MessageActionsTableReferences), + MessageAction, + PrefetchHooks Function({bool messageId})>; class $TwonlyDBManager { final _$TwonlyDB _db; @@ -11000,4 +11462,6 @@ class $TwonlyDBManager { get signalContactSignedPreKeys => $$SignalContactSignedPreKeysTableTableManager( _db, _db.signalContactSignedPreKeys); + $$MessageActionsTableTableManager get messageActions => + $$MessageActionsTableTableManager(_db, _db.messageActions); } diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 018d17b..7b9102b 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -76,12 +76,22 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { ), ); - await twonlyDB.messagesDao.updateMessagesByMediaId( - media.mediaId, - const MessagesCompanion( - ackByServer: Value(true), - ), - ); + /// As the messages where send in a bulk acknowledge all messages. + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); + for (final message in messages) { + final contacts = + await twonlyDB.groupsDao.getGroupMembers(message.groupId); + for (final contact in contacts) { + await twonlyDB.messagesDao.handleMessageAckByServer( + contact.contactId, + message.messageId, + DateTime.now(), + ); + } + } + return; } Log.error( diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index c7a84dd..eaab030 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; @@ -126,11 +127,10 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ if (resp.isSuccess) { if (receipt.messageId != null) { - await twonlyDB.messagesDao.updateMessageId( + await twonlyDB.messagesDao.handleMessageAckByServer( + receipt.contactId, receipt.messageId!, - const MessagesCompanion( - ackByServer: Value(true), - ), + DateTime.now(), ); } if (!receipt.contactWillSendsReceipt) { @@ -158,6 +158,37 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ return null; } +Future insertAndSendTextMessage( + String groupId, + String textMessage, +) async { + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + content: Value(textMessage), + ), + ); + if (message == null) { + Log.error('Could not insert message into database'); + return; + } + + final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); + + for (final groupMember in groupMembers) { + unawaited(sendCipherText( + groupMember.contactId, + pb.EncryptedContent( + textMessage: pb.EncryptedContent_TextMessage( + senderMessageId: message.messageId, + text: textMessage, + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + ), + ), + )); + } +} + Future<(Uint8List, Uint8List?)?> sendCipherText( int contactId, pb.EncryptedContent encryptedContent, { diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 58b75ca..74aa8e5 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -160,7 +160,6 @@ Future handleEncryptedMessage( if (content.hasMessageUpdate()) { await handleMessageUpdate( fromUserId, - content.groupId, content.messageUpdate, ); return null; diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index ef7e433..0b03728 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -92,8 +92,6 @@ Future handleMedia( senderId: Value(fromUserId), groupId: Value(groupId), mediaId: Value(mediaFile.mediaId), - ackByServer: const Value(true), - ackByUser: const Value(true), quotesMessageId: Value( media.hasQuoteMessageId() ? media.quoteMessageId : null, ), diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/server_messages/messages.server_messages.dart index 297aa0e..6c7347a 100644 --- a/lib/src/services/api/server_messages/messages.server_messages.dart +++ b/lib/src/services/api/server_messages/messages.server_messages.dart @@ -5,7 +5,6 @@ import 'package:twonly/src/utils/log.dart'; Future handleMessageUpdate( int contactId, - String groupId, EncryptedContent_MessageUpdate messageUpdate, ) async { switch (messageUpdate.type) { @@ -14,7 +13,7 @@ Future handleMessageUpdate( 'Opened message ${messageUpdate.multipleSenderMessageIds.length}'); for (final senderMessageId in messageUpdate.multipleSenderMessageIds) { await twonlyDB.messagesDao.handleMessageOpened( - groupId, + contactId, senderMessageId, fromTimestamp(messageUpdate.timestamp), ); diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index c5cef42..7efbdfd 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -20,8 +20,6 @@ Future handleTextMessage( senderId: Value(fromUserId), groupId: Value(groupId), content: Value(textMessage.text), - ackByServer: const Value(true), - ackByUser: const Value(true), quotesMessageId: Value( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index 2587c7b..d2b55ee 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; class AutomatedTestingView extends StatefulWidget { const AutomatedTestingView({super.key}); @@ -36,25 +36,27 @@ class _AutomatedTestingViewState extends State { title: const Text('Sending a lot of messages.'), subtitle: Text(lotsOfMessagesStatus), onTap: () async { - await twonlyDB.messageRetransmissionDao - .clearRetransmissionTable(); + final username = await showUserNameDialog(context); + if (username == null) return; final contacts = - await twonlyDB.contactsDao.getAllNotBlockedContacts(); + await twonlyDB.contactsDao.getContactsByUsername(username); for (final contact in contacts) { - for (var i = 0; i < 200; i++) { - setState(() { - lotsOfMessagesStatus = - 'At message $i to ${contact.username}.'; - }); - await sendTextMessage( - contact.userId, - TextMessageContent( - text: 'TestMessage $i', - ), - null, - ); + final groups = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + + for (final group in groups) { + for (var i = 0; i < 200; i++) { + setState(() { + lotsOfMessagesStatus = + 'At message $i to ${contact.username}.'; + }); + await insertAndSendTextMessage( + group.groupId, + 'Message $i.', + ); + } } } }, @@ -64,3 +66,37 @@ class _AutomatedTestingViewState extends State { ); } } + +Future showUserNameDialog( + BuildContext context, +) { + final controller = TextEditingController(); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Username'), + content: TextField( + controller: controller, + autofocus: true, + ), + actions: [ + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + TextButton( + child: Text(context.lang.ok), + onPressed: () { + Navigator.of(context) + .pop(controller.text); // Return the input text + }, + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/settings/developer/retransmission_data.view.dart b/lib/src/views/settings/developer/retransmission_data.view.dart index 4889c52..66fe34d 100644 --- a/lib/src/views/settings/developer/retransmission_data.view.dart +++ b/lib/src/views/settings/developer/retransmission_data.view.dart @@ -1,14 +1,7 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/services/api/messages.dart'; class RetransmissionDataView extends StatefulWidget { const RetransmissionDataView({super.key}); @@ -19,33 +12,22 @@ class RetransmissionDataView extends StatefulWidget { class RetransMsg { RetransMsg({ - required this.json, - required this.retrans, + required this.receipt, required this.contact, }); - final MessageJson json; - final MessageRetransmission retrans; + final Receipt receipt; final Contact? contact; static List fromRaw( - List retrans, + List receipts, Map contacts, ) { final res = []; - - for (final retrans in retrans) { - final json = MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(retrans.plaintextContent), - ), - ) as Map, - ); + for (final receipt in receipts) { res.add( RetransMsg( - json: json, - retrans: retrans, - contact: contacts[retrans.contactId], + receipt: receipt, + contact: contacts[receipt.contactId], ), ); } @@ -54,9 +36,9 @@ class RetransMsg { } class _RetransmissionDataViewState extends State { - List retransmissions = []; + List retransmissions = []; Map contacts = {}; - StreamSubscription>? subscriptionRetransmission; + StreamSubscription>? subscriptionRetransmission; StreamSubscription>? subscriptionContacts; List messages = []; @@ -85,7 +67,7 @@ class _RetransmissionDataViewState extends State { setState(() {}); }); subscriptionRetransmission = - twonlyDB.messageRetransmissionDao.watchAllMessages().listen((updated) { + twonlyDB.receiptsDao.watchAll().listen((updated) { retransmissions = updated; if (contacts.isNotEmpty) { messages = RetransMsg.fromRaw(retransmissions, contacts); @@ -108,7 +90,7 @@ class _RetransmissionDataViewState extends State { .map( (retrans) => ListTile( title: Text( - '${retrans.retrans.retransmissionId}: ${retrans.json.kind}', + retrans.receipt.receiptId, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -117,64 +99,13 @@ class _RetransmissionDataViewState extends State { 'To ${retrans.contact?.username}', ), Text( - 'Server-Ack: ${retrans.retrans.acknowledgeByServerAt}', + 'Server-Ack: ${retrans.receipt.ackByServerAt}', ), Text( - 'Retry: ${retrans.retrans.retryCount} : ${retrans.retrans.lastRetry}', + 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', ), ], ), - trailing: SizedBox( - width: 80, - child: Row( - children: [ - SizedBox( - height: 20, - width: 40, - child: Center( - child: GestureDetector( - onDoubleTap: () async { - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById( - retrans.retrans.retransmissionId, - ); - }, - child: const FaIcon( - FontAwesomeIcons.trash, - size: 15, - ), - ), - ), - ), - SizedBox( - width: 40, - child: OutlinedButton( - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero, - ), - ), - onPressed: () async { - await twonlyDB.messageRetransmissionDao - .updateRetransmission( - retrans.retrans.retransmissionId, - const MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(null), - ), - ); - await sendRetransmitMessage( - retrans.retrans.retransmissionId, - ); - }, - child: const FaIcon( - FontAwesomeIcons.arrowRotateLeft, - size: 15, - ), - ), - ), - ], - ), - ), ), ) .toList(), diff --git a/lib/src/views/settings/help/contact_us.view.dart b/lib/src/views/settings/help/contact_us.view.dart index ad6a570..52d644a 100644 --- a/lib/src/views/settings/help/contact_us.view.dart +++ b/lib/src/views/settings/help/contact_us.view.dart @@ -8,8 +8,6 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart' - show createDownloadToken, uint8ListToHex; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/help/contact_us/submit_message.view.dart'; @@ -32,7 +30,7 @@ class _ContactUsState extends State { Future uploadDebugLog() async { if (debugLogDownloadToken != null) return debugLogDownloadToken; - final downloadToken = createDownloadToken(); + final downloadToken = getRandomUint8List(32); final debugLog = await loadLogFile(); From 84828cd820a9d3fa308bb402329556122956aaa5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 24 Oct 2025 00:03:42 +0200 Subject: [PATCH 09/76] fixing memories --- lib/app.dart | 4 - lib/src/database/daos/mediafiles.dao.dart | 12 ++ lib/src/database/tables/messages.table.dart | 9 +- lib/src/database/twonly.db.g.dart | 68 ++++++- lib/src/model/memory_item.model.dart | 80 ++------ lib/src/utils/misc.dart | 26 +++ .../views/camera/share_image_editor_view.dart | 10 +- lib/src/views/chats/chat_list.view.dart | 3 +- lib/src/views/chats/chat_messages.view.dart | 6 +- lib/src/views/chats/start_new_chat.view.dart | 2 +- .../group_context_menu.component.dart | 96 +++++++++ .../user_context_menu.component.dart | 45 +++++ .../views/components/user_context_menu.dart | 186 ------------------ .../components/video_player_wrapper.dart | 7 +- lib/src/views/contact/contact.view.dart | 40 ++-- lib/src/views/home.view.dart | 1 - lib/src/views/memories/memories.view.dart | 91 +++------ .../memories/memories_item_thumbnail.dart | 8 +- .../memories/memories_photo_slider.view.dart | 53 ++--- .../settings/privacy_view_block.users.dart | 2 +- test/unit_test.dart | 11 -- 21 files changed, 347 insertions(+), 413 deletions(-) create mode 100644 lib/src/views/components/group_context_menu.component.dart create mode 100644 lib/src/views/components/user_context_menu.component.dart delete mode 100644 lib/src/views/components/user_context_menu.dart diff --git a/lib/app.dart b/lib/app.dart index 96588e9..48cc497 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,7 +6,6 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; @@ -68,8 +67,6 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); await apiService.connect(force: true); await apiService.listenToNetworkChanges(); - // call this function so invalid media files are get purged - await retryMediaUpload(true); } @override @@ -84,7 +81,6 @@ class _AppState extends State with WidgetsBindingObserver { } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; - unawaited(handleUploadWhenAppGoesBackground()); } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index a6832f7..72d5062 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -25,6 +25,14 @@ class MediaFilesDao extends DatabaseAccessor } } + Future deleteMediaFile(String mediaId) async { + await (delete(mediaFiles) + ..where( + (t) => t.mediaId.equals(mediaId), + )) + .go(); + } + Future updateMedia( String mediaId, MediaFilesCompanion updates, @@ -57,4 +65,8 @@ class MediaFilesDao extends DatabaseAccessor ..where((t) => t.downloadState.equals(DownloadState.pending.name))) .get(); } + + Stream> watchAllStoredMediaFiles() { + return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); + } } diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index faf0053..11651c8 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -4,6 +4,8 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +enum MessageType { media, text } + @DataClassName('Message') class Messages extends Table { TextColumn get groupId => @@ -14,9 +16,12 @@ class Messages extends Table { IntColumn get senderId => integer().nullable().references(Contacts, #userId)(); + TextColumn get type => textEnum()(); + TextColumn get content => text().nullable()(); - TextColumn get mediaId => - text().nullable().references(MediaFiles, #mediaId)(); + TextColumn get mediaId => text() + .nullable() + .references(MediaFiles, #mediaId, onDelete: KeyAction.cascade)(); BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 69e7a22..a285163 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2254,6 +2254,11 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($MessagesTable.$convertertype); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -2268,7 +2273,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES media_files (media_id)')); + 'REFERENCES media_files (media_id) ON DELETE CASCADE')); static const VerificationMeta _mediaStoredMeta = const VerificationMeta('mediaStored'); @override @@ -2327,6 +2332,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { groupId, messageId, senderId, + type, content, mediaId, mediaStored, @@ -2415,6 +2421,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, senderId: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + type: $MessagesTable.$convertertype.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), content: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping @@ -2438,12 +2446,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { $MessagesTable createAlias(String alias) { return $MessagesTable(attachedDatabase, alias); } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MessageType.values); } class Message extends DataClass implements Insertable { final String groupId; final String messageId; final int? senderId; + final MessageType type; final String? content; final String? mediaId; final bool mediaStored; @@ -2456,6 +2468,7 @@ class Message extends DataClass implements Insertable { {required this.groupId, required this.messageId, this.senderId, + required this.type, this.content, this.mediaId, required this.mediaStored, @@ -2472,6 +2485,9 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || senderId != null) { map['sender_id'] = Variable(senderId); } + { + map['type'] = Variable($MessagesTable.$convertertype.toSql(type)); + } if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -2498,6 +2514,7 @@ class Message extends DataClass implements Insertable { senderId: senderId == null && nullToAbsent ? const Value.absent() : Value(senderId), + type: Value(type), content: content == null && nullToAbsent ? const Value.absent() : Value(content), @@ -2524,6 +2541,8 @@ class Message extends DataClass implements Insertable { groupId: serializer.fromJson(json['groupId']), messageId: serializer.fromJson(json['messageId']), senderId: serializer.fromJson(json['senderId']), + type: $MessagesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), mediaStored: serializer.fromJson(json['mediaStored']), @@ -2542,6 +2561,8 @@ class Message extends DataClass implements Insertable { 'groupId': serializer.toJson(groupId), 'messageId': serializer.toJson(messageId), 'senderId': serializer.toJson(senderId), + 'type': + serializer.toJson($MessagesTable.$convertertype.toJson(type)), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), 'mediaStored': serializer.toJson(mediaStored), @@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable { {String? groupId, String? messageId, Value senderId = const Value.absent(), + MessageType? type, Value content = const Value.absent(), Value mediaId = const Value.absent(), bool? mediaStored, @@ -2569,6 +2591,7 @@ class Message extends DataClass implements Insertable { groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, mediaStored: mediaStored ?? this.mediaStored, @@ -2586,6 +2609,7 @@ class Message extends DataClass implements Insertable { groupId: data.groupId.present ? data.groupId.value : this.groupId, messageId: data.messageId.present ? data.messageId.value : this.messageId, senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, mediaStored: @@ -2610,6 +2634,7 @@ class Message extends DataClass implements Insertable { ..write('groupId: $groupId, ') ..write('messageId: $messageId, ') ..write('senderId: $senderId, ') + ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') ..write('mediaStored: $mediaStored, ') @@ -2627,6 +2652,7 @@ class Message extends DataClass implements Insertable { groupId, messageId, senderId, + type, content, mediaId, mediaStored, @@ -2642,6 +2668,7 @@ class Message extends DataClass implements Insertable { other.groupId == this.groupId && other.messageId == this.messageId && other.senderId == this.senderId && + other.type == this.type && other.content == this.content && other.mediaId == this.mediaId && other.mediaStored == this.mediaStored && @@ -2656,6 +2683,7 @@ class MessagesCompanion extends UpdateCompanion { final Value groupId; final Value messageId; final Value senderId; + final Value type; final Value content; final Value mediaId; final Value mediaStored; @@ -2669,6 +2697,7 @@ class MessagesCompanion extends UpdateCompanion { this.groupId = const Value.absent(), this.messageId = const Value.absent(), this.senderId = const Value.absent(), + this.type = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), this.mediaStored = const Value.absent(), @@ -2683,6 +2712,7 @@ class MessagesCompanion extends UpdateCompanion { required String groupId, this.messageId = const Value.absent(), this.senderId = const Value.absent(), + required MessageType type, this.content = const Value.absent(), this.mediaId = const Value.absent(), this.mediaStored = const Value.absent(), @@ -2692,11 +2722,13 @@ class MessagesCompanion extends UpdateCompanion { this.isEdited = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : groupId = Value(groupId); + }) : groupId = Value(groupId), + type = Value(type); static Insertable custom({ Expression? groupId, Expression? messageId, Expression? senderId, + Expression? type, Expression? content, Expression? mediaId, Expression? mediaStored, @@ -2711,6 +2743,7 @@ class MessagesCompanion extends UpdateCompanion { if (groupId != null) 'group_id': groupId, if (messageId != null) 'message_id': messageId, if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, if (mediaStored != null) 'media_stored': mediaStored, @@ -2728,6 +2761,7 @@ class MessagesCompanion extends UpdateCompanion { {Value? groupId, Value? messageId, Value? senderId, + Value? type, Value? content, Value? mediaId, Value? mediaStored, @@ -2741,6 +2775,7 @@ class MessagesCompanion extends UpdateCompanion { groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, senderId: senderId ?? this.senderId, + type: type ?? this.type, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, mediaStored: mediaStored ?? this.mediaStored, @@ -2765,6 +2800,10 @@ class MessagesCompanion extends UpdateCompanion { if (senderId.present) { map['sender_id'] = Variable(senderId.value); } + if (type.present) { + map['type'] = + Variable($MessagesTable.$convertertype.toSql(type.value)); + } if (content.present) { map['content'] = Variable(content.value); } @@ -2801,6 +2840,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('groupId: $groupId, ') ..write('messageId: $messageId, ') ..write('senderId: $senderId, ') + ..write('type: $type, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') ..write('mediaStored: $mediaStored, ') @@ -6149,6 +6189,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('messages', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('media_files', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('messages', limitUpdateKind: UpdateKind.delete), @@ -7817,6 +7864,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String groupId, Value messageId, Value senderId, + required MessageType type, Value content, Value mediaId, Value mediaStored, @@ -7831,6 +7879,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value groupId, Value messageId, Value senderId, + Value type, Value content, Value mediaId, Value mediaStored, @@ -7984,6 +8033,11 @@ class $$MessagesTableFilterComposer ColumnFilters get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); @@ -8180,6 +8234,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get messageId => $composableBuilder( column: $table.messageId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); @@ -8293,6 +8350,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get messageId => $composableBuilder(column: $table.messageId, builder: (column) => column); + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); @@ -8510,6 +8570,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value groupId = const Value.absent(), Value messageId = const Value.absent(), Value senderId = const Value.absent(), + Value type = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), Value mediaStored = const Value.absent(), @@ -8524,6 +8585,7 @@ class $$MessagesTableTableManager extends RootTableManager< groupId: groupId, messageId: messageId, senderId: senderId, + type: type, content: content, mediaId: mediaId, mediaStored: mediaStored, @@ -8538,6 +8600,7 @@ class $$MessagesTableTableManager extends RootTableManager< required String groupId, Value messageId = const Value.absent(), Value senderId = const Value.absent(), + required MessageType type, Value content = const Value.absent(), Value mediaId = const Value.absent(), Value mediaStored = const Value.absent(), @@ -8552,6 +8615,7 @@ class $$MessagesTableTableManager extends RootTableManager< groupId: groupId, messageId: messageId, senderId: senderId, + type: type, content: content, mediaId: mediaId, mediaStored: mediaStored, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index de2ce86..2f17535 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -1,88 +1,30 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:drift/drift.dart'; -import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; -import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; class MemoryItem { MemoryItem({ - required this.id, + required this.mediaService, required this.messages, - required this.date, - required this.mirrorVideo, - required this.thumbnailPath, - this.imagePath, - this.videoPath, }); - final int id; - final bool mirrorVideo; final List messages; - final DateTime date; - final File thumbnailPath; - final File? imagePath; - final File? videoPath; + final MediaFileService mediaService; - static Future> convertFromMessages( + static Future> convertFromMessages( List messages, ) async { - final items = {}; + final items = {}; for (final message in messages) { - final isSend = message.messageOtherId == null; - final id = message.mediaUploadId ?? message.messageId; - final basePath = await send.getMediaFilePath( - id, - isSend ? 'send' : 'received', - ); - File? imagePath; - late File thumbnailFile; - File? videoPath; - if (File('$basePath.mp4').existsSync()) { - videoPath = File('$basePath.mp4'); - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else if (File('$basePath.png').existsSync()) { - imagePath = File('$basePath.png'); - thumbnailFile = getThumbnailPath(imagePath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForImage(imagePath); - } - } else { - if (message.mediaStored) { - /// media file was deleted, ... remove the file - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - mediaStored: Value(false), - ), - ); - } - continue; - } - var mirrorVideo = false; - if (videoPath != null) { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - mirrorVideo = content.mirrorVideo; - } + if (message.mediaId == null) continue; + + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService == null) continue; items .putIfAbsent( - id, + message.mediaId!, () => MemoryItem( - id: id, + mediaService: mediaService, messages: [], - date: message.sendAt, - mirrorVideo: mirrorVideo, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, ), ) .messages diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index ec33119..e217869 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -6,6 +6,7 @@ import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:pie_menu/pie_menu.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; @@ -258,3 +259,28 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList( (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), ), ); + +PieTheme getPieCanvasTheme(BuildContext context) { + return PieTheme( + brightness: Theme.of(context).brightness, + rightClickShowsMenu: true, + radius: 70, + buttonTheme: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.tertiary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + buttonThemeHovered: PieButtonTheme( + backgroundColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.surfaceBright, + ), + tooltipPadding: const EdgeInsets.all(20), + overlayColor: isDarkMode(context) + ? const Color.fromARGB(69, 0, 0, 0) + : const Color.fromARGB(40, 0, 0, 0), + // spacing: 0, + tooltipTextStyle: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index ff66dc6..ba13f90 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -71,8 +71,14 @@ class _ShareImageEditorView extends State { selectedGroupIds.add(widget.sendToGroup!.groupId); } - if (widget.imageBytesFuture != null) { - unawaited(loadImage(widget.imageBytesFuture!)); + if (widget.mediaFileService.mediaFile.type == MediaType.image) { + if (widget.imageBytesFuture != null) { + loadImage(widget.imageBytesFuture!); + } else { + if (widget.mediaFileService.storedPath.existsSync()) { + loadImage(widget.mediaFileService.storedPath.readAsBytes()); + } + } } if (media.type == MediaType.video) { diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index f197cd5..302ad66 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -8,7 +8,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; @@ -28,7 +27,7 @@ import 'package:twonly/src/views/chats/start_new_chat.view.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 8d12d25..603c4cb 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -23,7 +23,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry. import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; @@ -61,9 +61,9 @@ class ChatItem { /// Displays detailed information about a SampleItem. class ChatMessagesView extends StatefulWidget { - const ChatMessagesView(this.contact, {super.key}); + const ChatMessagesView(this.group, {super.key}); - final Contact contact; + final Group group; @override State createState() => _ChatMessagesViewState(); diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index affe878..d4dbcac 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -12,7 +12,7 @@ import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; class StartNewChatView extends StatefulWidget { const StartNewChatView({super.key}); diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart new file mode 100644 index 0000000..67605f7 --- /dev/null +++ b/lib/src/views/components/group_context_menu.component.dart @@ -0,0 +1,96 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; + +class GroupContextMenu extends StatefulWidget { + const GroupContextMenu({ + required this.group, + required this.child, + super.key, + }); + final Widget child; + final Group group; + + @override + State createState() => _GroupContextMenuState(); +} + +class _GroupContextMenuState extends State { + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => (), + onToggle: (menuOpen) async { + if (menuOpen) { + await HapticFeedback.heavyImpact(); + } + }, + actions: [ + if (!widget.group.archived) + PieAction( + tooltip: Text(context.lang.contextMenuArchiveUser), + onSelect: () async { + const update = GroupsCompanion(archived: Value(true)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: const FaIcon(FontAwesomeIcons.boxArchive), + ), + if (widget.group.archived) + PieAction( + tooltip: Text(context.lang.contextMenuUndoArchiveUser), + onSelect: () async { + const update = GroupsCompanion(archived: Value(false)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: const FaIcon(FontAwesomeIcons.boxOpen), + ), + PieAction( + tooltip: Text(context.lang.contextMenuOpenChat), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(widget.group); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.solidComments), + ), + PieAction( + tooltip: Text( + widget.group.pinned + ? context.lang.contextMenuUnpin + : context.lang.contextMenuPin, + ), + onSelect: () async { + final update = GroupsCompanion(pinned: Value(!widget.group.pinned)); + if (context.mounted) { + await twonlyDB.groupsDao + .updateGroup(widget.group.groupId, update); + } + }, + child: FaIcon( + widget.group.pinned + ? FontAwesomeIcons.thumbtackSlash + : FontAwesomeIcons.thumbtack, + ), + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/views/components/user_context_menu.component.dart b/lib/src/views/components/user_context_menu.component.dart new file mode 100644 index 0000000..46c8716 --- /dev/null +++ b/lib/src/views/components/user_context_menu.component.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; + +class UserContextMenuBlocked extends StatefulWidget { + const UserContextMenuBlocked({ + required this.contact, + required this.child, + super.key, + }); + final Widget child; + final Contact contact; + + @override + State createState() => _UserContextMenuBlocked(); +} + +class _UserContextMenuBlocked extends State { + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => (), + actions: [ + PieAction( + tooltip: Text(context.lang.contextMenuUserProfile), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(widget.contact.userId); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.user), + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart deleted file mode 100644 index ac834b3..0000000 --- a/lib/src/views/components/user_context_menu.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/contact/contact.view.dart'; - -class UserContextMenu extends StatefulWidget { - const UserContextMenu({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuState(); -} - -class _UserContextMenuState extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuOpenChat), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(widget.contact); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.solidComments), - ), - PieAction( - tooltip: Text( - widget.contact.pinned - ? context.lang.contextMenuUnpin - : context.lang.contextMenuPin, - ), - onSelect: () async { - final update = - ContactsCompanion(pinned: Value(!widget.contact.pinned)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: FaIcon( - widget.contact.pinned - ? FontAwesomeIcons.thumbtackSlash - : FontAwesomeIcons.thumbtack, - ), - ), - ], - child: widget.child, - ); - } -} - -class UserContextMenuBlocked extends StatefulWidget { - const UserContextMenuBlocked({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuBlocked(); -} - -class _UserContextMenuBlocked extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuUserProfile), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(widget.contact.userId); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.user), - ), - ], - child: widget.child, - ); - } -} - -PieTheme getPieCanvasTheme(BuildContext context) { - return PieTheme( - brightness: Theme.of(context).brightness, - rightClickShowsMenu: true, - radius: 70, - buttonTheme: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.tertiary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - buttonThemeHovered: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - tooltipPadding: const EdgeInsets.all(20), - overlayColor: isDarkMode(context) - ? const Color.fromARGB(69, 0, 0, 0) - : const Color.fromARGB(40, 0, 0, 0), - // spacing: 0, - tooltipTextStyle: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), - ); -} diff --git a/lib/src/views/components/video_player_wrapper.dart b/lib/src/views/components/video_player_wrapper.dart index b35edc8..80833d0 100644 --- a/lib/src/views/components/video_player_wrapper.dart +++ b/lib/src/views/components/video_player_wrapper.dart @@ -7,11 +7,9 @@ import 'package:video_player/video_player.dart'; class VideoPlayerWrapper extends StatefulWidget { const VideoPlayerWrapper({ required this.videoPath, - required this.mirrorVideo, super.key, }); final File videoPath; - final bool mirrorVideo; @override State createState() => _VideoPlayerWrapperState(); @@ -48,10 +46,7 @@ class _VideoPlayerWrapperState extends State { child: _controller.value.isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(_controller), - ), + child: VideoPlayer(_controller), ) : const CircularProgressIndicator(), // Show loading indicator while initializing ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index d08a914..20d6a46 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -163,26 +163,26 @@ class _ContactViewState extends State { ); }, ), - BetterListTile( - icon: FontAwesomeIcons.eraser, - iconSize: 16, - text: context.lang.deleteAllContactMessages, - onTap: () async { - final block = await showAlertDialog( - context, - context.lang.deleteAllContactMessages, - context.lang.deleteAllContactMessagesBody( - getContactDisplayName(contact), - ), - ); - if (block) { - if (context.mounted) { - await twonlyDB.messagesDao - .deleteMessagesByContactId(contact.userId); - } - } - }, - ), + // BetterListTile( + // icon: FontAwesomeIcons.eraser, + // iconSize: 16, + // text: context.lang.deleteAllContactMessages, + // onTap: () async { + // final block = await showAlertDialog( + // context, + // context.lang.deleteAllContactMessages, + // context.lang.deleteAllContactMessagesBody( + // getContactDisplayName(contact), + // ), + // ); + // if (block) { + // if (context.mounted) { + // await twonlyDB.messagesDao + // .deleteMessagesByContactId(contact.userId); + // } + // } + // }, + // ), BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 3463ea0..0c24c4a 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -10,7 +10,6 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index fd95f31..2438382 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -1,13 +1,11 @@ import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; -import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -24,7 +22,7 @@ class MemoriesViewState extends State { List galleryItems = []; Map> orderedByMonth = {}; List months = []; - StreamSubscription>? messageSub; + StreamSubscription>? messageSub; @override void initState() { @@ -38,76 +36,37 @@ class MemoriesViewState extends State { super.dispose(); } - Future> loadMemoriesDirectory() async { - final directoryPath = await send.getMediaBaseFilePath('memories'); - final directory = Directory(directoryPath); - - final items = []; - if (directory.existsSync()) { - final files = directory.listSync(); - - for (final file in files) { - if (file is File) { - final fileName = file.uri.pathSegments.last; - File? imagePath; - File? videoPath; - late File thumbnailFile; - if (fileName.contains('.thumbnail.')) { - continue; - } - if (fileName.contains('.png')) { - imagePath = file; - thumbnailFile = file; - // if (!await thumbnailFile.exists()) { - // await createThumbnailsForImage(imagePath); - // } - } else if (fileName.contains('.mp4')) { - videoPath = file; - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else { - break; - } - final creationDate = file.lastModifiedSync(); - items.add( - MemoryItem( - id: int.parse(fileName.split('.')[0]), - messages: [], - date: creationDate, - mirrorVideo: false, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, - ), - ); - } - } - } - return items; - } - Future initAsync() async { await messageSub?.cancel(); - final msgStream = twonlyDB.messagesDao.getAllStoredMediaFiles(); + final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles(); - messageSub = msgStream.listen((msgs) async { - final items = await MemoryItem.convertFromMessages(msgs); + messageSub = msgStream.listen((mediaFiles) async { // Group items by month orderedByMonth = {}; months = []; var lastMonth = ''; - galleryItems = await loadMemoriesDirectory(); - for (final item in galleryItems) { - items.remove( - item.id, - ); // prefer the stored one and not the saved on in the chat.... + galleryItems = []; + final applicationSupportDirectory = + await getApplicationSupportDirectory(); + for (final mediaFile in mediaFiles) { + galleryItems.add( + MemoryItem( + mediaService: MediaFileService( + mediaFile, + applicationSupportDirectory: applicationSupportDirectory, + ), + messages: [], + ), + ); } - galleryItems += items.values.toList(); - galleryItems.sort((a, b) => b.date.compareTo(a.date)); + galleryItems.sort( + (a, b) => b.mediaService.mediaFile.createdAt.compareTo( + a.mediaService.mediaFile.createdAt, + ), + ); for (var i = 0; i < galleryItems.length; i++) { - final month = DateFormat('MMMM yyyy').format(galleryItems[i].date); + final month = DateFormat('MMMM yyyy') + .format(galleryItems[i].mediaService.mediaFile.createdAt); if (lastMonth != month) { lastMonth = month; months.add(month); diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index efcd0f6..cab777e 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; class MemoriesItemThumbnail extends StatefulWidget { @@ -39,11 +40,12 @@ class _MemoriesItemThumbnailState extends State { return GestureDetector( onTap: widget.onTap, child: Hero( - tag: widget.galleryItem.id.toString(), + tag: widget.galleryItem.mediaService.mediaFile.mediaId, child: Stack( children: [ - Image.file(widget.galleryItem.thumbnailPath), - if (widget.galleryItem.videoPath != null) + Image.file(widget.galleryItem.mediaService.thumbnailPath), + if (widget.galleryItem.mediaService.mediaFile.type == + MediaType.video) const Positioned.fill( child: Center( child: FaIcon(FontAwesomeIcons.circlePlay), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index bb2abf5..5d9f1b7 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -1,14 +1,10 @@ -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/mediafiles/download.service.dart' - as received; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; @@ -51,7 +47,6 @@ class _MemoriesPhotoSliderViewState extends State { } Future deleteFile() async { - final messages = widget.galleryItems[currentIndex].messages; final confirmed = await showAlertDialog( context, context.lang.deleteImageTitle, @@ -60,32 +55,26 @@ class _MemoriesPhotoSliderViewState extends State { if (!confirmed) return; - widget.galleryItems[currentIndex].imagePath?.deleteSync(); - widget.galleryItems[currentIndex].videoPath?.deleteSync(); - for (final message in messages) { - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion(mediaStored: Value(false)), - ); - } + widget.galleryItems[currentIndex].mediaService.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile( + widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId, + ); widget.galleryItems.removeAt(currentIndex); setState(() {}); - await send.purgeSendMediaFiles(); - await received.purgeReceivedMediaFiles(); if (mounted) { Navigator.pop(context, true); } } Future exportFile() async { - final item = widget.galleryItems[currentIndex]; + final item = widget.galleryItems[currentIndex].mediaService; try { - if (item.videoPath != null) { - await saveVideoToGallery(item.videoPath!.path); - } else if (item.imagePath != null) { - final imageBytes = await item.imagePath!.readAsBytes(); + if (item.mediaFile.type == MediaType.video) { + await saveVideoToGallery(item.storedPath.path); + } else if (item.mediaFile.type == MediaType.image) { + final imageBytes = await item.storedPath.readAsBytes(); await saveImageToGallery(imageBytes); } if (!mounted) return; @@ -132,14 +121,9 @@ class _MemoriesPhotoSliderViewState extends State { context, MaterialPageRoute( builder: (context) => ShareImageEditorView( - videoFilePath: - widget.galleryItems[currentIndex].videoPath, - imageBytes: widget - .galleryItems[currentIndex].imagePath - ?.readAsBytes(), - mirrorVideo: false, + mediaFileService: widget + .galleryItems[currentIndex].mediaService, sharedFromGallery: true, - useHighQuality: true, ), ), ); @@ -214,24 +198,25 @@ class _MemoriesPhotoSliderViewState extends State { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { final item = widget.galleryItems[index]; - return item.videoPath != null + return item.mediaService.mediaFile.type == MediaType.video ? PhotoViewGalleryPageOptions.customChild( child: VideoPlayerWrapper( - videoPath: item.videoPath!, - mirrorVideo: item.mirrorVideo, + videoPath: item.mediaService.storedPath, ), // childSize: const Size(300, 300), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId), ) : PhotoViewGalleryPageOptions( - imageProvider: FileImage(item.imagePath!), + imageProvider: FileImage(item.mediaService.storedPath), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId), ); } } diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 394f5e3..01c2b17 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -6,7 +6,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; class PrivacyViewBlockUsers extends StatefulWidget { const PrivacyViewBlockUsers({super.key}); diff --git a/test/unit_test.dart b/test/unit_test.dart index ffe3b19..603e6ed 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,10 +1,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; - import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -22,15 +20,6 @@ void main() { expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33); }); - test('test utils', () async { - final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); - final list2 = Uint8List.fromList([42, 42, 42]); - final combined = combineUint8Lists(list1, list2); - final lists = extractUint8Lists(combined); - expect(list1, lists[0]); - expect(list2, lists[1]); - }); - test('encode hex', () async { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1))); From 4260c63ce26839bb38d533705fe26404c1985c5d Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 25 Oct 2025 01:08:59 +0200 Subject: [PATCH 10/76] fixing all compile errors --- lib/app.dart | 11 - lib/globals.dart | 1 - lib/main.dart | 1 - lib/src/database/daos/contacts.dao.dart | 147 +- lib/src/database/daos/groups.dao.dart | 141 +- lib/src/database/daos/mediafiles.dao.dart | 5 + lib/src/database/daos/messages.dao.dart | 164 +- lib/src/database/daos/messages.dao.g.dart | 2 + lib/src/database/daos/reactions.dao.dart | 11 + lib/src/database/tables/contacts.table.dart | 16 - lib/src/database/tables/groups.table.dart | 20 +- lib/src/database/tables/messages.table.dart | 15 +- lib/src/database/twonly.db.dart | 2 +- lib/src/database/twonly.db.g.dart | 2538 ++++++++--------- lib/src/model/json/message_old.dart | 331 --- lib/src/model/json/userdata.dart | 2 +- lib/src/model/json/userdata.g.dart | 4 +- .../client/generated/messages.pb.dart | 64 +- .../client/generated/messages.pbjson.dart | 121 +- lib/src/model/protobuf/client/messages.proto | 7 +- .../api/mediafiles/download.service.dart | 5 +- .../api/mediafiles/upload.service.dart | 18 +- lib/src/services/api/messages.dart | 41 +- .../contact.server_messages.dart | 16 +- .../media.server_messages.dart | 16 +- .../messages.server_messages.dart | 3 +- lib/src/services/api/utils.dart | 2 +- lib/src/services/flame.service.dart | 51 +- .../mediafiles/mediafile.service.dart | 14 +- lib/src/utils/misc.dart | 29 +- .../best_friends_selector.dart | 89 +- .../views/camera/share_image_editor_view.dart | 6 +- lib/src/views/camera/share_image_view.dart | 207 +- lib/src/views/chats/add_new_user.view.dart | 43 +- lib/src/views/chats/chat_list.view.dart | 300 +- .../chat_list_components/group_list_item.dart | 227 ++ .../last_message_time.dart | 8 +- lib/src/views/chats/chat_messages.view.dart | 332 +-- .../chat_list_entry.dart | 73 +- .../chat_media_entry.dart | 100 +- .../chat_reaction_row.dart | 117 +- .../chat_text_entry.dart | 20 +- .../in_chat_media_viewer.dart | 13 +- .../message_context_menu.dart | 59 +- .../message_send_state_icon.dart | 230 +- .../response_container.dart | 102 +- lib/src/views/chats/media_viewer.view.dart | 529 +--- .../emoji_reactions_row.component.dart | 92 + .../reaction_buttons.component.dart | 112 + lib/src/views/chats/start_new_chat.view.dart | 49 +- .../components/avatar_icon.component.dart | 94 + lib/src/views/components/flame.dart | 61 +- lib/src/views/components/initialsavatar.dart | 64 - .../user_context_menu.component.dart | 8 +- lib/src/views/contact/contact.view.dart | 19 +- lib/src/views/groups/group.view.dart | 18 + .../memories/memories_photo_slider.view.dart | 6 +- .../developer/automated_testing.view.dart | 24 +- .../settings/developer/developer.view.dart | 19 +- .../settings/privacy_view_block.users.dart | 6 +- .../views/settings/settings_main.view.dart | 4 +- 61 files changed, 3223 insertions(+), 3606 deletions(-) delete mode 100644 lib/src/model/json/message_old.dart create mode 100644 lib/src/views/chats/chat_list_components/group_list_item.dart create mode 100644 lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart create mode 100644 lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart create mode 100644 lib/src/views/components/avatar_icon.component.dart delete mode 100644 lib/src/views/components/initialsavatar.dart create mode 100644 lib/src/views/groups/group.view.dart diff --git a/lib/app.dart b/lib/app.dart index 48cc497..bc9f76b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -43,18 +43,7 @@ class _AppState extends State with WidgetsBindingObserver { Future setUserPlan() async { final user = await getUser(); - globalBestFriendUserId = -1; if (user != null && mounted) { - if (user.myBestFriendContactId != null) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(user.myBestFriendContactId!) - .getSingleOrNull(); - if (contact != null) { - if (contact.alsoBestFriend) { - globalBestFriendUserId = user.myBestFriendContactId ?? 0; - } - } - } if (mounted) { await context .read() diff --git a/lib/globals.dart b/lib/globals.dart index d08f1fa..36562dd 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -27,4 +27,3 @@ void Function() globalCallbackNewDeviceRegistered = () {}; void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; bool globalIsAppInBackground = true; -int globalBestFriendUserId = -1; diff --git a/lib/main.dart b/lib/main.dart index eb2fdb4..19fee5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,7 +47,6 @@ void main() async { await initFileDownloader(); // await twonlyDB.messagesDao.resetPendingDownloadState(); - // await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.signalDao.purgeOutDatedPreKeys(); diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 58e6b69..de674dc 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -20,79 +20,6 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { } } - Future incFlameCounter( - int contactId, - bool received, - DateTime timestamp, - ) async { - final contact = (await (select(contacts) - ..where((t) => t.userId.equals(contactId))) - .get()) - .first; - - final totalMediaCounter = contact.totalMediaCounter + 1; - var flameCounter = contact.flameCounter; - - if (contact.lastMessageReceived != null && - contact.lastMessageSend != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (contact.lastMessageSend!.isBefore(twoDaysAgo) || - contact.lastMessageReceived!.isBefore(twoDaysAgo)) { - flameCounter = 0; - } - } - - var lastMessageSend = const Value.absent(); - var lastMessageReceived = const Value.absent(); - var lastFlameCounterChange = const Value.absent(); - - if (contact.lastFlameCounterChange != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - - if (contact.lastFlameCounterChange!.isBefore(startOfToday)) { - // last flame update was yesterday. check if it can be updated. - var updateFlame = false; - if (received) { - if (contact.lastMessageSend != null && - contact.lastMessageSend!.isAfter(startOfToday)) { - // today a message was already send -> update flame - updateFlame = true; - } - } else if (contact.lastMessageReceived != null && - contact.lastMessageReceived!.isAfter(startOfToday)) { - // today a message was already received -> update flame - updateFlame = true; - } - if (updateFlame) { - flameCounter += 1; - lastFlameCounterChange = Value(timestamp); - } - } - } else { - // There where no message until no... - lastFlameCounterChange = Value(timestamp); - } - - if (received) { - lastMessageReceived = Value(timestamp); - } else { - lastMessageSend = Value(timestamp); - } - - return (update(contacts)..where((t) => t.userId.equals(contactId))).write( - ContactsCompanion( - totalMediaCounter: Value(totalMediaCounter), - lastFlameCounterChange: lastFlameCounterChange, - lastMessageReceived: lastMessageReceived, - lastMessageSend: lastMessageSend, - flameCounter: Value(flameCounter), - ), - ); - } - SingleOrNullSelectable getContactByUserId(int userId) { return select(contacts)..where((t) => t.userId.equals(userId)); } @@ -135,37 +62,6 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { .watchSingleOrNull(); } - // Stream> watchContactsForShareView() { - // return (select(contacts) - // ..where( - // (t) => - // t.accepted.equals(true) & - // t.blocked.equals(false) & - // t.deleted.equals(false), - // ) - // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - // .watch(); - // } - - // Stream> watchContactsForStartNewChat() { - // return (select(contacts) - // ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) - // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - // .watch(); - // } - - // Stream> watchContactsForChatList() { - // return (select(contacts) - // ..where( - // (t) => - // t.accepted.equals(true) & - // t.blocked.equals(false) & - // t.archived.equals(false), - // ) - // ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - // .watch(); - // } - Future> getAllNotBlockedContacts() { return (select(contacts)..where((t) => t.blocked.equals(false))).get(); } @@ -188,31 +84,15 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { return query.map((row) => row.read(count)).watchSingle(); } + Stream> watchAllAcceptedContacts() { + return (select(contacts) + ..where((t) => t.blocked.equals(false) & t.accepted.equals(true))) + .watch(); + } + Stream> watchAllContacts() { return select(contacts).watch(); } - - Future modifyFlameCounterForTesting() async { - await update(contacts).write( - ContactsCompanion( - lastFlameCounterChange: Value(DateTime.now()), - flameCounter: const Value(1337), - lastFlameSync: const Value(null), - ), - ); - } - - Stream watchFlameCounter(int userId) { - return (select(contacts) - ..where( - (u) => - u.userId.equals(userId) & - u.lastMessageReceived.isNotNull() & - u.lastMessageSend.isNotNull(), - )) - .watchSingle() - .asyncMap(getFlameCounterFromContact); - } } String getContactDisplayName(Contact user) { @@ -234,18 +114,3 @@ String getContactDisplayName(Contact user) { String applyStrikethrough(String text) { return text.split('').map((char) => '$char\u0336').join(); } - -int getFlameCounterFromContact(Contact contact) { - if (contact.lastMessageSend == null || contact.lastMessageReceived == null) { - return 0; - } - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (contact.lastMessageSend!.isAfter(twoDaysAgo) && - contact.lastMessageReceived!.isAfter(twoDaysAgo)) { - return contact.flameCounter + 1; - } else { - return 0; - } -} diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 6456434..cfd2157 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -32,7 +32,56 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .get(); } - Future> getDirectChat(int userId) async { + Future insertGroup(GroupsCompanion group) async { + await into(groups).insert(group); + } + + Future> getGroupContact(String groupId) async { + final query = select(contacts).join([ + leftOuterJoin( + groupMembers, + groupMembers.contactId.equalsExp(contacts.userId) & + groupMembers.groupId.equals(groupId), + ), + ]); + return query.map((row) => row.readTable(contacts)).get(); + } + + Stream> watchGroups() { + return select(groups).watch(); + } + + Stream watchGroup(String groupId) { + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .watchSingleOrNull(); + } + + Stream> watchGroupsForChatList() { + return (select(groups)..where((t) => t.archived.equals(false))).watch(); + } + + Future getGroup(String groupId) { + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .getSingleOrNull(); + } + + Stream watchFlameCounter(String groupId) { + return (select(groups) + ..where( + (u) => + u.groupId.equals(groupId) & + u.lastMessageReceived.isNotNull() & + u.lastMessageSend.isNotNull(), + )) + .watchSingle() + .asyncMap(getFlameCounterFromGroup); + } + + Future> getAllDirectChats() { + return (select(groups)..where((t) => t.isDirectChat.equals(true))).get(); + } + + Future getDirectChat(int userId) async { final query = (select(groups).join([ leftOuterJoin( groupMembers, @@ -40,8 +89,94 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { groupMembers.contactId.equals(userId), ), ]) - ..where(groups.isGroupOfTwo.equals(true))); + ..where(groups.isDirectChat.equals(true))); - return query.map((row) => row.readTable(groups)).get(); + return query.map((row) => row.readTable(groups)).getSingleOrNull(); + } + + Future incFlameCounter( + String groupId, + bool received, + DateTime timestamp, + ) async { + final group = await (select(groups) + ..where((t) => t.groupId.equals(groupId))) + .getSingle(); + + final totalMediaCounter = group.totalMediaCounter + 1; + var flameCounter = group.flameCounter; + + if (group.lastMessageReceived != null && group.lastMessageSend != null) { + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + if (group.lastMessageSend!.isBefore(twoDaysAgo) || + group.lastMessageReceived!.isBefore(twoDaysAgo)) { + flameCounter = 0; + } + } + + var lastMessageSend = const Value.absent(); + var lastMessageReceived = const Value.absent(); + var lastFlameCounterChange = const Value.absent(); + + if (group.lastFlameCounterChange != null) { + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + + if (group.lastFlameCounterChange!.isBefore(startOfToday)) { + // last flame update was yesterday. check if it can be updated. + var updateFlame = false; + if (received) { + if (group.lastMessageSend != null && + group.lastMessageSend!.isAfter(startOfToday)) { + // today a message was already send -> update flame + updateFlame = true; + } + } else if (group.lastMessageReceived != null && + group.lastMessageReceived!.isAfter(startOfToday)) { + // today a message was already received -> update flame + updateFlame = true; + } + if (updateFlame) { + flameCounter += 1; + lastFlameCounterChange = Value(timestamp); + } + } + } else { + // There where no message until no... + lastFlameCounterChange = Value(timestamp); + } + + if (received) { + lastMessageReceived = Value(timestamp); + } else { + lastMessageSend = Value(timestamp); + } + + await (update(groups)..where((t) => t.groupId.equals(groupId))).write( + GroupsCompanion( + totalMediaCounter: Value(totalMediaCounter), + lastFlameCounterChange: lastFlameCounterChange, + lastMessageReceived: lastMessageReceived, + lastMessageSend: lastMessageSend, + flameCounter: Value(flameCounter), + ), + ); + } +} + +int getFlameCounterFromGroup(Group group) { + if (group.lastMessageSend == null || group.lastMessageReceived == null) { + return 0; + } + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + if (group.lastMessageSend!.isAfter(twoDaysAgo) && + group.lastMessageReceived!.isAfter(twoDaysAgo)) { + return group.flameCounter + 1; + } else { + return 0; } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 72d5062..872f73c 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -46,6 +46,11 @@ class MediaFilesDao extends DatabaseAccessor .getSingleOrNull(); } + Stream watchMedia(String mediaId) { + return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) + .watchSingleOrNull(); + } + Future resetPendingDownloadState() async { await (update(mediaFiles) ..where( diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7a0726f..9a7829a 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/tables/reactions.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'; @@ -15,7 +16,9 @@ part 'messages.dao.g.dart'; Messages, Contacts, MediaFiles, + Reactions, MessageHistories, + GroupMembers, MessageActions, Groups, ], @@ -26,55 +29,39 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // ignore: matching_super_parameters MessagesDao(super.db); - // Stream> watchMessageNotOpened(int contactId) { - // return (select(messages) - // ..where( - // (t) => - // t.openedAt.isNull() & - // t.contactId.equals(contactId) & - // t.errorWhileSending.equals(false), - // ) - // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - // .watch(); - // } + Stream> watchMessageNotOpened(String groupId) { + return (select(messages) + ..where((t) => t.openedAt.isNull() & t.groupId.equals(groupId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } - // Stream> watchMediaMessageNotOpened(int contactId) { - // return (select(messages) - // ..where( - // (t) => - // t.openedAt.isNull() & - // t.contactId.equals(contactId) & - // t.errorWhileSending.equals(false) & - // t.messageOtherId.isNotNull() & - // t.kind.equals(MessageKind.media.name), - // ) - // ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - // .watch(); - // } + Stream> watchMediaNotOpened(String groupId) { + return (select(messages) + ..where( + (t) => + t.openedAt.isNull() & + t.groupId.equals(groupId) & + t.senderId.isNotNull() & + t.type.equals(MessageType.media.name), + ) + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + .watch(); + } - // Stream> watchLastMessage(int contactId) { - // return (select(messages) - // ..where((t) => t.contactId.equals(contactId)) - // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) - // ..limit(1)) - // .watch(); - // } + Stream> watchLastMessage(String groupId) { + return (select(messages) + ..where((t) => t.groupId.equals(groupId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(1)) + .watch(); + } - // Stream> watchAllMessagesFrom(int contactId) { - // return (select(messages) - // ..where( - // (t) => - // t.contactId.equals(contactId) & - // t.contentJson.isNotNull() & - // (t.openedAt.isNull() | - // t.mediaStored.equals(true) | - // t.openedAt.isBiggerThanValue( - // DateTime.now().subtract(const Duration(days: 1)), - // )), - // ) - // ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - // .watch(); - // } + Stream> watchByGroupId(String groupId) { + return ((select(messages)..where((t) => t.groupId.equals(groupId))) + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + .watch(); + } // Future removeOldMessages() { // return (update(messages) @@ -92,22 +79,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // .write(const MessagesCompanion(contentJson: Value(null))); // } - // Future handleMediaFilesOlderThan30Days() { - // /// media files will be deleted by the server after 30 days, so delete them here also - // return (update(messages) - // ..where( - // (t) => (t.kind.equals(MessageKind.media.name) & - // t.openedAt.isNull() & - // t.messageOtherId.isNull() & - // (t.sendAt.isSmallerThanValue( - // DateTime.now().subtract( - // const Duration(days: 30), - // ), - // ))), - // )) - // .write(const MessagesCompanion(errorWhileSending: Value(true))); - // } - // Future> getAllMessagesPendingDownloading() { // return (select(messages) // ..where( @@ -155,18 +126,18 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // .get(); // } - // Future openedAllNonMediaMessages(int contactId) { - // final updates = MessagesCompanion(openedAt: Value(DateTime.now())); - // return (update(messages) - // ..where( - // (t) => - // t.contactId.equals(contactId) & - // t.messageOtherId.isNotNull() & - // t.openedAt.isNull() & - // t.kind.equals(MessageKind.media.name).not(), - // )) - // .write(updates); - // } + Future openedAllTextMessages(String groupId) { + final updates = MessagesCompanion(openedAt: Value(DateTime.now())); + return (update(messages) + ..where( + (t) => + t.groupId.equals(groupId) & + t.senderId.isNotNull() & + t.openedAt.isNull() & + t.type.equals(MessageType.text.name), + )) + .write(updates); + } Future handleMessageDeletion( int contactId, @@ -259,6 +230,22 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); } + Future haveAllMembers( + String groupId, + String messageId, + MessageActionType action, + ) async { + final members = await twonlyDB.groupsDao.getGroupMembers(groupId); + + final actions = await (select(messageActions) + ..where( + (t) => t.type.equals(action.name) & t.messageId.equals(messageId), + )) + .get(); + + return members.length == actions.length; + } + // Future updateMessageByOtherUser( // int userId, // int messageId, @@ -321,6 +308,29 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } } + Future getLastMessageAction(String messageId) async { + return (((select(messageActions) + ..where( + (t) => t.messageId.equals(messageId), + )) + ..orderBy([(t) => OrderingTerm.desc(t.actionAt)])) + ..limit(1)) + .getSingleOrNull(); + } + + Future reopenedMedia(String messageId) async { + await (delete(messageActions) + ..where( + (t) => + t.messageId.equals(messageId) & + t.contactId.isNull() & + t.type.equals( + MessageActionType.openedAt.name, + ), + )) + .go(); + } + // Future deleteMessagesByContactId(int contactId) { // return (delete(messages) // ..where( @@ -342,9 +352,9 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // .go(); // } - // Future deleteMessagesByMessageId(int messageId) { - // return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); - // } + Future deleteMessagesById(String messageId) { + return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); + } // Future deleteAllMessagesByContactId(int contactId) { // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart index 74bd53f..feb9283 100644 --- a/lib/src/database/daos/messages.dao.g.dart +++ b/lib/src/database/daos/messages.dao.g.dart @@ -8,7 +8,9 @@ mixin _$MessagesDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; + $ReactionsTable get reactions => attachedDatabase.reactions; $MessageHistoriesTable get messageHistories => attachedDatabase.messageHistories; + $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; $MessageActionsTable get messageActions => attachedDatabase.messageActions; } diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 1325d30..93f02a4 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -43,4 +43,15 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { Log.error(e); } } + + Stream> watchReactions(String messageId) { + return (select(reactions) + ..where((t) => t.messageId.equals(messageId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + + Future insertReaction(ReactionsCompanion reaction) async { + await into(reactions).insert(reaction); + } } diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 0aa40c0..2fa8096 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -13,28 +13,12 @@ class Contacts extends Table { BoolColumn get accepted => boolean().withDefault(const Constant(false))(); BoolColumn get requested => boolean().withDefault(const Constant(false))(); - BoolColumn get hidden => boolean().withDefault(const Constant(false))(); BoolColumn get blocked => boolean().withDefault(const Constant(false))(); BoolColumn get verified => boolean().withDefault(const Constant(false))(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); - BoolColumn get alsoBestFriend => - boolean().withDefault(const Constant(false))(); - - IntColumn get deleteMessagesAfterXMinutes => - integer().withDefault(const Constant(60 * 24))(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); - IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))(); - - DateTimeColumn get lastMessageSend => dateTime().nullable()(); - DateTimeColumn get lastMessageReceived => dateTime().nullable()(); - DateTimeColumn get lastFlameCounterChange => dateTime().nullable()(); - DateTimeColumn get lastFlameSync => dateTime().nullable()(); - - IntColumn get flameCounter => integer().withDefault(const Constant(0))(); - @override Set get primaryKey => {userId}; } diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 27b6438..674f2ac 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -7,15 +7,31 @@ class Groups extends Table { TextColumn get groupId => text().clientDefault(() => uuid.v4())(); BoolColumn get isGroupAdmin => boolean()(); - BoolColumn get isGroupOfTwo => boolean()(); + BoolColumn get isDirectChat => boolean()(); BoolColumn get pinned => boolean().withDefault(const Constant(false))(); BoolColumn get archived => boolean().withDefault(const Constant(false))(); TextColumn get groupName => text()(); + IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))(); + + BoolColumn get alsoBestFriend => + boolean().withDefault(const Constant(false))(); + + IntColumn get deleteMessagesAfterMilliseconds => + integer().withDefault(const Constant(1000 * 60 * 24))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get lastMessageSend => dateTime().nullable()(); + DateTimeColumn get lastMessageReceived => dateTime().nullable()(); + DateTimeColumn get lastFlameCounterChange => dateTime().nullable()(); + DateTimeColumn get lastFlameSync => dateTime().nullable()(); + + IntColumn get flameCounter => integer().withDefault(const Constant(0))(); + DateTimeColumn get lastMessageExchange => dateTime().withDefault(currentDateAndTime)(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {groupId}; diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 11651c8..0e1c70d 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -33,9 +33,9 @@ class Messages extends Table { BoolColumn get isDeletedFromSender => boolean().withDefault(const Constant(false))(); - BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); - + DateTimeColumn get openedAt => dateTime().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get modifiedAt => dateTime().nullable()(); @override Set get primaryKey => {messageId}; @@ -43,15 +43,12 @@ class Messages extends Table { enum MessageActionType { openedAt, - modifiedAt, ackByUserAt, ackByServerAt, } @DataClassName('MessageAction') class MessageActions extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get messageId => text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); @@ -59,10 +56,11 @@ class MessageActions extends Table { integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); TextColumn get type => textEnum()(); + DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); @override - Set get primaryKey => {id}; + Set get primaryKey => {messageId, contactId, type}; } @DataClassName('MessageHistory') @@ -72,8 +70,9 @@ class MessageHistories extends Table { TextColumn get messageId => text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); - IntColumn get contactId => - integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + IntColumn get contactId => integer() + .nullable() + .references(Contacts, #contactId, onDelete: KeyAction.cascade)(); TextColumn get content => text().nullable()(); diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 1e121c1..22e3907 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -42,7 +42,7 @@ part 'twonly.db.g.dart'; SignalSessionStores, SignalContactPreKeys, SignalContactSignedPreKeys, - MessageActions + MessageActions, ], daos: [ MessagesDao, diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index a285163..de4a722 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -65,15 +65,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); - @override - late final GeneratedColumn hidden = GeneratedColumn( - 'hidden', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("hidden" IN (0, 1))'), - defaultValue: const Constant(false)); static const VerificationMeta _blockedMeta = const VerificationMeta('blocked'); @override @@ -104,25 +95,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _alsoBestFriendMeta = - const VerificationMeta('alsoBestFriend'); - @override - late final GeneratedColumn alsoBestFriend = GeneratedColumn( - 'also_best_friend', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("also_best_friend" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _deleteMessagesAfterXMinutesMeta = - const VerificationMeta('deleteMessagesAfterXMinutes'); - @override - late final GeneratedColumn deleteMessagesAfterXMinutes = - GeneratedColumn( - 'delete_messages_after_x_minutes', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(60 * 24)); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -131,46 +103,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); - static const VerificationMeta _totalMediaCounterMeta = - const VerificationMeta('totalMediaCounter'); - @override - late final GeneratedColumn totalMediaCounter = GeneratedColumn( - 'total_media_counter', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _lastMessageSendMeta = - const VerificationMeta('lastMessageSend'); - @override - late final GeneratedColumn lastMessageSend = - GeneratedColumn('last_message_send', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastMessageReceivedMeta = - const VerificationMeta('lastMessageReceived'); - @override - late final GeneratedColumn lastMessageReceived = - GeneratedColumn('last_message_received', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastFlameCounterChangeMeta = - const VerificationMeta('lastFlameCounterChange'); - @override - late final GeneratedColumn lastFlameCounterChange = - GeneratedColumn('last_flame_counter_change', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastFlameSyncMeta = - const VerificationMeta('lastFlameSync'); - @override - late final GeneratedColumn lastFlameSync = - GeneratedColumn('last_flame_sync', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _flameCounterMeta = - const VerificationMeta('flameCounter'); - @override - late final GeneratedColumn flameCounter = GeneratedColumn( - 'flame_counter', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); @override List get $columns => [ userId, @@ -181,19 +113,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { senderProfileCounter, accepted, requested, - hidden, blocked, verified, deleted, - alsoBestFriend, - deleteMessagesAfterXMinutes, - createdAt, - totalMediaCounter, - lastMessageSend, - lastMessageReceived, - lastFlameCounterChange, - lastFlameSync, - flameCounter + createdAt ]; @override String get aliasedName => _alias ?? actualTableName; @@ -243,10 +166,6 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_requestedMeta, requested.isAcceptableOrUnknown(data['requested']!, _requestedMeta)); } - if (data.containsKey('hidden')) { - context.handle(_hiddenMeta, - hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta)); - } if (data.containsKey('blocked')) { context.handle(_blockedMeta, blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); @@ -259,29 +178,641 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_deletedMeta, deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); } - if (data.containsKey('also_best_friend')) { - context.handle( - _alsoBestFriendMeta, - alsoBestFriend.isAcceptableOrUnknown( - data['also_best_friend']!, _alsoBestFriendMeta)); - } - if (data.containsKey('delete_messages_after_x_minutes')) { - context.handle( - _deleteMessagesAfterXMinutesMeta, - deleteMessagesAfterXMinutes.isAcceptableOrUnknown( - data['delete_messages_after_x_minutes']!, - _deleteMessagesAfterXMinutesMeta)); - } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } + return context; + } + + @override + Set get $primaryKey => {userId}; + @override + Contact map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Contact( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + displayName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}display_name']), + nickName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), + avatarSvg: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}avatar_svg']), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, + accepted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, + requested: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + verified: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + deleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ContactsTable createAlias(String alias) { + return $ContactsTable(attachedDatabase, alias); + } +} + +class Contact extends DataClass implements Insertable { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final String? avatarSvg; + final int senderProfileCounter; + final bool accepted; + final bool requested; + final bool blocked; + final bool verified; + final bool deleted; + final DateTime createdAt; + const Contact( + {required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvg, + required this.senderProfileCounter, + required this.accepted, + required this.requested, + required this.blocked, + required this.verified, + required this.deleted, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['username'] = Variable(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable(nickName); + } + if (!nullToAbsent || avatarSvg != null) { + map['avatar_svg'] = Variable(avatarSvg); + } + map['sender_profile_counter'] = Variable(senderProfileCounter); + map['accepted'] = Variable(accepted); + map['requested'] = Variable(requested); + map['blocked'] = Variable(blocked); + map['verified'] = Variable(verified); + map['deleted'] = Variable(deleted); + map['created_at'] = Variable(createdAt); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvg: avatarSvg == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvg), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + requested: Value(requested), + blocked: Value(blocked), + verified: Value(verified), + deleted: Value(deleted), + createdAt: Value(createdAt), + ); + } + + factory Contact.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Contact( + userId: serializer.fromJson(json['userId']), + username: serializer.fromJson(json['username']), + displayName: serializer.fromJson(json['displayName']), + nickName: serializer.fromJson(json['nickName']), + avatarSvg: serializer.fromJson(json['avatarSvg']), + senderProfileCounter: + serializer.fromJson(json['senderProfileCounter']), + accepted: serializer.fromJson(json['accepted']), + requested: serializer.fromJson(json['requested']), + blocked: serializer.fromJson(json['blocked']), + verified: serializer.fromJson(json['verified']), + deleted: serializer.fromJson(json['deleted']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'username': serializer.toJson(username), + 'displayName': serializer.toJson(displayName), + 'nickName': serializer.toJson(nickName), + 'avatarSvg': serializer.toJson(avatarSvg), + 'senderProfileCounter': serializer.toJson(senderProfileCounter), + 'accepted': serializer.toJson(accepted), + 'requested': serializer.toJson(requested), + 'blocked': serializer.toJson(blocked), + 'verified': serializer.toJson(verified), + 'deleted': serializer.toJson(deleted), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Contact copyWith( + {int? userId, + String? username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + int? senderProfileCounter, + bool? accepted, + bool? requested, + bool? blocked, + bool? verified, + bool? deleted, + DateTime? createdAt}) => + Contact( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + deleted: deleted ?? this.deleted, + createdAt: createdAt ?? this.createdAt, + ); + Contact copyWithCompanion(ContactsCompanion data) { + return Contact( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvg: data.avatarSvg.present ? data.avatarSvg.value : this.avatarSvg, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + requested: data.requested.present ? data.requested.value : this.requested, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Contact(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('deleted: $deleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + userId, + username, + displayName, + nickName, + avatarSvg, + senderProfileCounter, + accepted, + requested, + blocked, + verified, + deleted, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Contact && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + other.avatarSvg == this.avatarSvg && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.requested == this.requested && + other.blocked == this.blocked && + other.verified == this.verified && + other.deleted == this.deleted && + other.createdAt == this.createdAt); +} + +class ContactsCompanion extends UpdateCompanion { + final Value userId; + final Value username; + final Value displayName; + final Value nickName; + final Value avatarSvg; + final Value senderProfileCounter; + final Value accepted; + final Value requested; + final Value blocked; + final Value verified; + final Value deleted; + final Value createdAt; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.deleted = const Value.absent(), + this.createdAt = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.deleted = const Value.absent(), + this.createdAt = const Value.absent(), + }) : username = Value(username); + static Insertable custom({ + Expression? userId, + Expression? username, + Expression? displayName, + Expression? nickName, + Expression? avatarSvg, + Expression? senderProfileCounter, + Expression? accepted, + Expression? requested, + Expression? blocked, + Expression? verified, + Expression? deleted, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvg != null) 'avatar_svg': avatarSvg, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (requested != null) 'requested': requested, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (deleted != null) 'deleted': deleted, + if (createdAt != null) 'created_at': createdAt, + }); + } + + ContactsCompanion copyWith( + {Value? userId, + Value? username, + Value? displayName, + Value? nickName, + Value? avatarSvg, + Value? senderProfileCounter, + Value? accepted, + Value? requested, + Value? blocked, + Value? verified, + Value? deleted, + Value? createdAt}) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvg: avatarSvg ?? this.avatarSvg, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + deleted: deleted ?? this.deleted, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable(nickName.value); + } + if (avatarSvg.present) { + map['avatar_svg'] = Variable(avatarSvg.value); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable(accepted.value); + } + if (requested.present) { + map['requested'] = Variable(requested.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (deleted.present) { + map['deleted'] = Variable(deleted.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('deleted: $deleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + clientDefault: () => uuid.v4()); + static const VerificationMeta _isGroupAdminMeta = + const VerificationMeta('isGroupAdmin'); + @override + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_admin" IN (0, 1))')); + static const VerificationMeta _isDirectChatMeta = + const VerificationMeta('isDirectChat'); + @override + late final GeneratedColumn isDirectChat = GeneratedColumn( + 'is_direct_chat', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_direct_chat" IN (0, 1))')); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _archivedMeta = + const VerificationMeta('archived'); + @override + late final GeneratedColumn archived = GeneratedColumn( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _groupNameMeta = + const VerificationMeta('groupName'); + @override + late final GeneratedColumn groupName = GeneratedColumn( + 'group_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _totalMediaCounterMeta = + const VerificationMeta('totalMediaCounter'); + @override + late final GeneratedColumn totalMediaCounter = GeneratedColumn( + 'total_media_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _alsoBestFriendMeta = + const VerificationMeta('alsoBestFriend'); + @override + late final GeneratedColumn alsoBestFriend = GeneratedColumn( + 'also_best_friend', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("also_best_friend" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deleteMessagesAfterMillisecondsMeta = + const VerificationMeta('deleteMessagesAfterMilliseconds'); + @override + late final GeneratedColumn deleteMessagesAfterMilliseconds = + GeneratedColumn( + 'delete_messages_after_milliseconds', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1000 * 60 * 24)); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _lastMessageSendMeta = + const VerificationMeta('lastMessageSend'); + @override + late final GeneratedColumn lastMessageSend = + GeneratedColumn('last_message_send', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastMessageReceivedMeta = + const VerificationMeta('lastMessageReceived'); + @override + late final GeneratedColumn lastMessageReceived = + GeneratedColumn('last_message_received', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameCounterChangeMeta = + const VerificationMeta('lastFlameCounterChange'); + @override + late final GeneratedColumn lastFlameCounterChange = + GeneratedColumn('last_flame_counter_change', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameSyncMeta = + const VerificationMeta('lastFlameSync'); + @override + late final GeneratedColumn lastFlameSync = + GeneratedColumn('last_flame_sync', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _flameCounterMeta = + const VerificationMeta('flameCounter'); + @override + late final GeneratedColumn flameCounter = GeneratedColumn( + 'flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _lastMessageExchangeMeta = + const VerificationMeta('lastMessageExchange'); + @override + late final GeneratedColumn lastMessageExchange = + GeneratedColumn('last_message_exchange', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + groupName, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + lastMessageExchange + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } + if (data.containsKey('is_group_admin')) { + context.handle( + _isGroupAdminMeta, + isGroupAdmin.isAcceptableOrUnknown( + data['is_group_admin']!, _isGroupAdminMeta)); + } else if (isInserting) { + context.missing(_isGroupAdminMeta); + } + if (data.containsKey('is_direct_chat')) { + context.handle( + _isDirectChatMeta, + isDirectChat.isAcceptableOrUnknown( + data['is_direct_chat']!, _isDirectChatMeta)); + } else if (isInserting) { + context.missing(_isDirectChatMeta); + } + if (data.containsKey('pinned')) { + context.handle(_pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + } + if (data.containsKey('archived')) { + context.handle(_archivedMeta, + archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); + } + if (data.containsKey('group_name')) { + context.handle(_groupNameMeta, + groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta)); + } else if (isInserting) { + context.missing(_groupNameMeta); + } if (data.containsKey('total_media_counter')) { context.handle( _totalMediaCounterMeta, totalMediaCounter.isAcceptableOrUnknown( data['total_media_counter']!, _totalMediaCounterMeta)); } + if (data.containsKey('also_best_friend')) { + context.handle( + _alsoBestFriendMeta, + alsoBestFriend.isAcceptableOrUnknown( + data['also_best_friend']!, _alsoBestFriendMeta)); + } + if (data.containsKey('delete_messages_after_milliseconds')) { + context.handle( + _deleteMessagesAfterMillisecondsMeta, + deleteMessagesAfterMilliseconds.isAcceptableOrUnknown( + data['delete_messages_after_milliseconds']!, + _deleteMessagesAfterMillisecondsMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } if (data.containsKey('last_message_send')) { context.handle( _lastMessageSendMeta, @@ -312,48 +843,42 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { flameCounter.isAcceptableOrUnknown( data['flame_counter']!, _flameCounterMeta)); } + if (data.containsKey('last_message_exchange')) { + context.handle( + _lastMessageExchangeMeta, + lastMessageExchange.isAcceptableOrUnknown( + data['last_message_exchange']!, _lastMessageExchangeMeta)); + } return context; } @override - Set get $primaryKey => {userId}; + Set get $primaryKey => {groupId}; @override - Contact map(Map data, {String? tablePrefix}) { + Group map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Contact( - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - displayName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}display_name']), - nickName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), - avatarSvg: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}avatar_svg']), - senderProfileCounter: attachedDatabase.typeMapping.read( - DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, - accepted: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, - requested: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, - hidden: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}hidden'])!, - blocked: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, - verified: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, - deleted: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, - alsoBestFriend: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}also_best_friend'])!, - deleteMessagesAfterXMinutes: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}delete_messages_after_x_minutes'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + return Group( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + isGroupAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, + isDirectChat: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_direct_chat'])!, + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + groupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, totalMediaCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}total_media_counter'])!, + alsoBestFriend: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}also_best_friend'])!, + deleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_milliseconds'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, lastMessageSend: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}last_message_send']), lastMessageReceived: attachedDatabase.typeMapping.read( @@ -366,85 +891,66 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { DriftSqlType.dateTime, data['${effectivePrefix}last_flame_sync']), flameCounter: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}flame_counter'])!, + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_exchange'])!, ); } @override - $ContactsTable createAlias(String alias) { - return $ContactsTable(attachedDatabase, alias); + $GroupsTable createAlias(String alias) { + return $GroupsTable(attachedDatabase, alias); } } -class Contact extends DataClass implements Insertable { - final int userId; - final String username; - final String? displayName; - final String? nickName; - final String? avatarSvg; - final int senderProfileCounter; - final bool accepted; - final bool requested; - final bool hidden; - final bool blocked; - final bool verified; - final bool deleted; - final bool alsoBestFriend; - final int deleteMessagesAfterXMinutes; - final DateTime createdAt; +class Group extends DataClass implements Insertable { + final String groupId; + final bool isGroupAdmin; + final bool isDirectChat; + final bool pinned; + final bool archived; + final String groupName; final int totalMediaCounter; + final bool alsoBestFriend; + final int deleteMessagesAfterMilliseconds; + final DateTime createdAt; final DateTime? lastMessageSend; final DateTime? lastMessageReceived; final DateTime? lastFlameCounterChange; final DateTime? lastFlameSync; final int flameCounter; - const Contact( - {required this.userId, - required this.username, - this.displayName, - this.nickName, - this.avatarSvg, - required this.senderProfileCounter, - required this.accepted, - required this.requested, - required this.hidden, - required this.blocked, - required this.verified, - required this.deleted, - required this.alsoBestFriend, - required this.deleteMessagesAfterXMinutes, - required this.createdAt, + final DateTime lastMessageExchange; + const Group( + {required this.groupId, + required this.isGroupAdmin, + required this.isDirectChat, + required this.pinned, + required this.archived, + required this.groupName, required this.totalMediaCounter, + required this.alsoBestFriend, + required this.deleteMessagesAfterMilliseconds, + required this.createdAt, this.lastMessageSend, this.lastMessageReceived, this.lastFlameCounterChange, this.lastFlameSync, - required this.flameCounter}); + required this.flameCounter, + required this.lastMessageExchange}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = Variable(userId); - map['username'] = Variable(username); - if (!nullToAbsent || displayName != null) { - map['display_name'] = Variable(displayName); - } - if (!nullToAbsent || nickName != null) { - map['nick_name'] = Variable(nickName); - } - if (!nullToAbsent || avatarSvg != null) { - map['avatar_svg'] = Variable(avatarSvg); - } - map['sender_profile_counter'] = Variable(senderProfileCounter); - map['accepted'] = Variable(accepted); - map['requested'] = Variable(requested); - map['hidden'] = Variable(hidden); - map['blocked'] = Variable(blocked); - map['verified'] = Variable(verified); - map['deleted'] = Variable(deleted); - map['also_best_friend'] = Variable(alsoBestFriend); - map['delete_messages_after_x_minutes'] = - Variable(deleteMessagesAfterXMinutes); - map['created_at'] = Variable(createdAt); + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_direct_chat'] = Variable(isDirectChat); + map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); + map['group_name'] = Variable(groupName); map['total_media_counter'] = Variable(totalMediaCounter); + map['also_best_friend'] = Variable(alsoBestFriend); + map['delete_messages_after_milliseconds'] = + Variable(deleteMessagesAfterMilliseconds); + map['created_at'] = Variable(createdAt); if (!nullToAbsent || lastMessageSend != null) { map['last_message_send'] = Variable(lastMessageSend); } @@ -459,33 +965,22 @@ class Contact extends DataClass implements Insertable { map['last_flame_sync'] = Variable(lastFlameSync); } map['flame_counter'] = Variable(flameCounter); + map['last_message_exchange'] = Variable(lastMessageExchange); return map; } - ContactsCompanion toCompanion(bool nullToAbsent) { - return ContactsCompanion( - userId: Value(userId), - username: Value(username), - displayName: displayName == null && nullToAbsent - ? const Value.absent() - : Value(displayName), - nickName: nickName == null && nullToAbsent - ? const Value.absent() - : Value(nickName), - avatarSvg: avatarSvg == null && nullToAbsent - ? const Value.absent() - : Value(avatarSvg), - senderProfileCounter: Value(senderProfileCounter), - accepted: Value(accepted), - requested: Value(requested), - hidden: Value(hidden), - blocked: Value(blocked), - verified: Value(verified), - deleted: Value(deleted), - alsoBestFriend: Value(alsoBestFriend), - deleteMessagesAfterXMinutes: Value(deleteMessagesAfterXMinutes), - createdAt: Value(createdAt), + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isDirectChat: Value(isDirectChat), + pinned: Value(pinned), + archived: Value(archived), + groupName: Value(groupName), totalMediaCounter: Value(totalMediaCounter), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds), + createdAt: Value(createdAt), lastMessageSend: lastMessageSend == null && nullToAbsent ? const Value.absent() : Value(lastMessageSend), @@ -499,31 +994,25 @@ class Contact extends DataClass implements Insertable { ? const Value.absent() : Value(lastFlameSync), flameCounter: Value(flameCounter), + lastMessageExchange: Value(lastMessageExchange), ); } - factory Contact.fromJson(Map json, + factory Group.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return Contact( - userId: serializer.fromJson(json['userId']), - username: serializer.fromJson(json['username']), - displayName: serializer.fromJson(json['displayName']), - nickName: serializer.fromJson(json['nickName']), - avatarSvg: serializer.fromJson(json['avatarSvg']), - senderProfileCounter: - serializer.fromJson(json['senderProfileCounter']), - accepted: serializer.fromJson(json['accepted']), - requested: serializer.fromJson(json['requested']), - hidden: serializer.fromJson(json['hidden']), - blocked: serializer.fromJson(json['blocked']), - verified: serializer.fromJson(json['verified']), - deleted: serializer.fromJson(json['deleted']), - alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), - deleteMessagesAfterXMinutes: - serializer.fromJson(json['deleteMessagesAfterXMinutes']), - createdAt: serializer.fromJson(json['createdAt']), + return Group( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isDirectChat: serializer.fromJson(json['isDirectChat']), + pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), + groupName: serializer.fromJson(json['groupName']), totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), + alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), + deleteMessagesAfterMilliseconds: + serializer.fromJson(json['deleteMessagesAfterMilliseconds']), + createdAt: serializer.fromJson(json['createdAt']), lastMessageSend: serializer.fromJson(json['lastMessageSend']), lastMessageReceived: serializer.fromJson(json['lastMessageReceived']), @@ -531,78 +1020,64 @@ class Contact extends DataClass implements Insertable { serializer.fromJson(json['lastFlameCounterChange']), lastFlameSync: serializer.fromJson(json['lastFlameSync']), flameCounter: serializer.fromJson(json['flameCounter']), + lastMessageExchange: + serializer.fromJson(json['lastMessageExchange']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), - 'username': serializer.toJson(username), - 'displayName': serializer.toJson(displayName), - 'nickName': serializer.toJson(nickName), - 'avatarSvg': serializer.toJson(avatarSvg), - 'senderProfileCounter': serializer.toJson(senderProfileCounter), - 'accepted': serializer.toJson(accepted), - 'requested': serializer.toJson(requested), - 'hidden': serializer.toJson(hidden), - 'blocked': serializer.toJson(blocked), - 'verified': serializer.toJson(verified), - 'deleted': serializer.toJson(deleted), - 'alsoBestFriend': serializer.toJson(alsoBestFriend), - 'deleteMessagesAfterXMinutes': - serializer.toJson(deleteMessagesAfterXMinutes), - 'createdAt': serializer.toJson(createdAt), + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isDirectChat': serializer.toJson(isDirectChat), + 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), + 'groupName': serializer.toJson(groupName), 'totalMediaCounter': serializer.toJson(totalMediaCounter), + 'alsoBestFriend': serializer.toJson(alsoBestFriend), + 'deleteMessagesAfterMilliseconds': + serializer.toJson(deleteMessagesAfterMilliseconds), + 'createdAt': serializer.toJson(createdAt), 'lastMessageSend': serializer.toJson(lastMessageSend), 'lastMessageReceived': serializer.toJson(lastMessageReceived), 'lastFlameCounterChange': serializer.toJson(lastFlameCounterChange), 'lastFlameSync': serializer.toJson(lastFlameSync), 'flameCounter': serializer.toJson(flameCounter), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), }; } - Contact copyWith( - {int? userId, - String? username, - Value displayName = const Value.absent(), - Value nickName = const Value.absent(), - Value avatarSvg = const Value.absent(), - int? senderProfileCounter, - bool? accepted, - bool? requested, - bool? hidden, - bool? blocked, - bool? verified, - bool? deleted, - bool? alsoBestFriend, - int? deleteMessagesAfterXMinutes, - DateTime? createdAt, + Group copyWith( + {String? groupId, + bool? isGroupAdmin, + bool? isDirectChat, + bool? pinned, + bool? archived, + String? groupName, int? totalMediaCounter, + bool? alsoBestFriend, + int? deleteMessagesAfterMilliseconds, + DateTime? createdAt, Value lastMessageSend = const Value.absent(), Value lastMessageReceived = const Value.absent(), Value lastFlameCounterChange = const Value.absent(), Value lastFlameSync = const Value.absent(), - int? flameCounter}) => - Contact( - userId: userId ?? this.userId, - username: username ?? this.username, - displayName: displayName.present ? displayName.value : this.displayName, - nickName: nickName.present ? nickName.value : this.nickName, - avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg, - senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, - accepted: accepted ?? this.accepted, - requested: requested ?? this.requested, - hidden: hidden ?? this.hidden, - blocked: blocked ?? this.blocked, - verified: verified ?? this.verified, - deleted: deleted ?? this.deleted, - alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, - deleteMessagesAfterXMinutes: - deleteMessagesAfterXMinutes ?? this.deleteMessagesAfterXMinutes, - createdAt: createdAt ?? this.createdAt, + int? flameCounter, + DateTime? lastMessageExchange}) => + Group( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + groupName: groupName ?? this.groupName, totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, lastMessageSend: lastMessageSend.present ? lastMessageSend.value : this.lastMessageSend, @@ -615,34 +1090,31 @@ class Contact extends DataClass implements Insertable { lastFlameSync: lastFlameSync.present ? lastFlameSync.value : this.lastFlameSync, flameCounter: flameCounter ?? this.flameCounter, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, ); - Contact copyWithCompanion(ContactsCompanion data) { - return Contact( - userId: data.userId.present ? data.userId.value : this.userId, - username: data.username.present ? data.username.value : this.username, - displayName: - data.displayName.present ? data.displayName.value : this.displayName, - nickName: data.nickName.present ? data.nickName.value : this.nickName, - avatarSvg: data.avatarSvg.present ? data.avatarSvg.value : this.avatarSvg, - senderProfileCounter: data.senderProfileCounter.present - ? data.senderProfileCounter.value - : this.senderProfileCounter, - accepted: data.accepted.present ? data.accepted.value : this.accepted, - requested: data.requested.present ? data.requested.value : this.requested, - hidden: data.hidden.present ? data.hidden.value : this.hidden, - blocked: data.blocked.present ? data.blocked.value : this.blocked, - verified: data.verified.present ? data.verified.value : this.verified, - deleted: data.deleted.present ? data.deleted.value : this.deleted, - alsoBestFriend: data.alsoBestFriend.present - ? data.alsoBestFriend.value - : this.alsoBestFriend, - deleteMessagesAfterXMinutes: data.deleteMessagesAfterXMinutes.present - ? data.deleteMessagesAfterXMinutes.value - : this.deleteMessagesAfterXMinutes, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + Group copyWithCompanion(GroupsCompanion data) { + return Group( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isDirectChat: data.isDirectChat.present + ? data.isDirectChat.value + : this.isDirectChat, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + groupName: data.groupName.present ? data.groupName.value : this.groupName, totalMediaCounter: data.totalMediaCounter.present ? data.totalMediaCounter.value : this.totalMediaCounter, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + data.deleteMessagesAfterMilliseconds.present + ? data.deleteMessagesAfterMilliseconds.value + : this.deleteMessagesAfterMilliseconds, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, lastMessageSend: data.lastMessageSend.present ? data.lastMessageSend.value : this.lastMessageSend, @@ -658,199 +1130,166 @@ class Contact extends DataClass implements Insertable { flameCounter: data.flameCounter.present ? data.flameCounter.value : this.flameCounter, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, ); } @override String toString() { - return (StringBuffer('Contact(') - ..write('userId: $userId, ') - ..write('username: $username, ') - ..write('displayName: $displayName, ') - ..write('nickName: $nickName, ') - ..write('avatarSvg: $avatarSvg, ') - ..write('senderProfileCounter: $senderProfileCounter, ') - ..write('accepted: $accepted, ') - ..write('requested: $requested, ') - ..write('hidden: $hidden, ') - ..write('blocked: $blocked, ') - ..write('verified: $verified, ') - ..write('deleted: $deleted, ') - ..write('alsoBestFriend: $alsoBestFriend, ') - ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') - ..write('createdAt: $createdAt, ') + return (StringBuffer('Group(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('groupName: $groupName, ') ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') + ..write('createdAt: $createdAt, ') ..write('lastMessageSend: $lastMessageSend, ') ..write('lastMessageReceived: $lastMessageReceived, ') ..write('lastFlameCounterChange: $lastFlameCounterChange, ') ..write('lastFlameSync: $lastFlameSync, ') - ..write('flameCounter: $flameCounter') + ..write('flameCounter: $flameCounter, ') + ..write('lastMessageExchange: $lastMessageExchange') ..write(')')) .toString(); } @override - int get hashCode => Object.hashAll([ - userId, - username, - displayName, - nickName, - avatarSvg, - senderProfileCounter, - accepted, - requested, - hidden, - blocked, - verified, - deleted, - alsoBestFriend, - deleteMessagesAfterXMinutes, - createdAt, - totalMediaCounter, - lastMessageSend, - lastMessageReceived, - lastFlameCounterChange, - lastFlameSync, - flameCounter - ]); + int get hashCode => Object.hash( + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + groupName, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + lastMessageExchange); @override bool operator ==(Object other) => identical(this, other) || - (other is Contact && - other.userId == this.userId && - other.username == this.username && - other.displayName == this.displayName && - other.nickName == this.nickName && - other.avatarSvg == this.avatarSvg && - other.senderProfileCounter == this.senderProfileCounter && - other.accepted == this.accepted && - other.requested == this.requested && - other.hidden == this.hidden && - other.blocked == this.blocked && - other.verified == this.verified && - other.deleted == this.deleted && - other.alsoBestFriend == this.alsoBestFriend && - other.deleteMessagesAfterXMinutes == - this.deleteMessagesAfterXMinutes && - other.createdAt == this.createdAt && + (other is Group && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isDirectChat == this.isDirectChat && + other.pinned == this.pinned && + other.archived == this.archived && + other.groupName == this.groupName && other.totalMediaCounter == this.totalMediaCounter && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterMilliseconds == + this.deleteMessagesAfterMilliseconds && + other.createdAt == this.createdAt && other.lastMessageSend == this.lastMessageSend && other.lastMessageReceived == this.lastMessageReceived && other.lastFlameCounterChange == this.lastFlameCounterChange && other.lastFlameSync == this.lastFlameSync && - other.flameCounter == this.flameCounter); + other.flameCounter == this.flameCounter && + other.lastMessageExchange == this.lastMessageExchange); } -class ContactsCompanion extends UpdateCompanion { - final Value userId; - final Value username; - final Value displayName; - final Value nickName; - final Value avatarSvg; - final Value senderProfileCounter; - final Value accepted; - final Value requested; - final Value hidden; - final Value blocked; - final Value verified; - final Value deleted; - final Value alsoBestFriend; - final Value deleteMessagesAfterXMinutes; - final Value createdAt; +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isDirectChat; + final Value pinned; + final Value archived; + final Value groupName; final Value totalMediaCounter; + final Value alsoBestFriend; + final Value deleteMessagesAfterMilliseconds; + final Value createdAt; final Value lastMessageSend; final Value lastMessageReceived; final Value lastFlameCounterChange; final Value lastFlameSync; final Value flameCounter; - const ContactsCompanion({ - this.userId = const Value.absent(), - this.username = const Value.absent(), - this.displayName = const Value.absent(), - this.nickName = const Value.absent(), - this.avatarSvg = const Value.absent(), - this.senderProfileCounter = const Value.absent(), - this.accepted = const Value.absent(), - this.requested = const Value.absent(), - this.hidden = const Value.absent(), - this.blocked = const Value.absent(), - this.verified = const Value.absent(), - this.deleted = const Value.absent(), - this.alsoBestFriend = const Value.absent(), - this.deleteMessagesAfterXMinutes = const Value.absent(), - this.createdAt = const Value.absent(), + final Value lastMessageExchange; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.groupName = const Value.absent(), this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), this.lastMessageSend = const Value.absent(), this.lastMessageReceived = const Value.absent(), this.lastFlameCounterChange = const Value.absent(), this.lastFlameSync = const Value.absent(), this.flameCounter = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), }); - ContactsCompanion.insert({ - this.userId = const Value.absent(), - required String username, - this.displayName = const Value.absent(), - this.nickName = const Value.absent(), - this.avatarSvg = const Value.absent(), - this.senderProfileCounter = const Value.absent(), - this.accepted = const Value.absent(), - this.requested = const Value.absent(), - this.hidden = const Value.absent(), - this.blocked = const Value.absent(), - this.verified = const Value.absent(), - this.deleted = const Value.absent(), - this.alsoBestFriend = const Value.absent(), - this.deleteMessagesAfterXMinutes = const Value.absent(), - this.createdAt = const Value.absent(), + GroupsCompanion.insert({ + this.groupId = const Value.absent(), + required bool isGroupAdmin, + required bool isDirectChat, + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + required String groupName, this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), this.lastMessageSend = const Value.absent(), this.lastMessageReceived = const Value.absent(), this.lastFlameCounterChange = const Value.absent(), this.lastFlameSync = const Value.absent(), this.flameCounter = const Value.absent(), - }) : username = Value(username); - static Insertable custom({ - Expression? userId, - Expression? username, - Expression? displayName, - Expression? nickName, - Expression? avatarSvg, - Expression? senderProfileCounter, - Expression? accepted, - Expression? requested, - Expression? hidden, - Expression? blocked, - Expression? verified, - Expression? deleted, - Expression? alsoBestFriend, - Expression? deleteMessagesAfterXMinutes, - Expression? createdAt, + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }) : isGroupAdmin = Value(isGroupAdmin), + isDirectChat = Value(isDirectChat), + groupName = Value(groupName); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isDirectChat, + Expression? pinned, + Expression? archived, + Expression? groupName, Expression? totalMediaCounter, + Expression? alsoBestFriend, + Expression? deleteMessagesAfterMilliseconds, + Expression? createdAt, Expression? lastMessageSend, Expression? lastMessageReceived, Expression? lastFlameCounterChange, Expression? lastFlameSync, Expression? flameCounter, + Expression? lastMessageExchange, + Expression? rowid, }) { return RawValuesInsertable({ - if (userId != null) 'user_id': userId, - if (username != null) 'username': username, - if (displayName != null) 'display_name': displayName, - if (nickName != null) 'nick_name': nickName, - if (avatarSvg != null) 'avatar_svg': avatarSvg, - if (senderProfileCounter != null) - 'sender_profile_counter': senderProfileCounter, - if (accepted != null) 'accepted': accepted, - if (requested != null) 'requested': requested, - if (hidden != null) 'hidden': hidden, - if (blocked != null) 'blocked': blocked, - if (verified != null) 'verified': verified, - if (deleted != null) 'deleted': deleted, - if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, - if (deleteMessagesAfterXMinutes != null) - 'delete_messages_after_x_minutes': deleteMessagesAfterXMinutes, - if (createdAt != null) 'created_at': createdAt, + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isDirectChat != null) 'is_direct_chat': isDirectChat, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (groupName != null) 'group_name': groupName, if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterMilliseconds != null) + 'delete_messages_after_milliseconds': deleteMessagesAfterMilliseconds, + if (createdAt != null) 'created_at': createdAt, if (lastMessageSend != null) 'last_message_send': lastMessageSend, if (lastMessageReceived != null) 'last_message_received': lastMessageReceived, @@ -858,110 +1297,87 @@ class ContactsCompanion extends UpdateCompanion { 'last_flame_counter_change': lastFlameCounterChange, if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, if (flameCounter != null) 'flame_counter': flameCounter, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (rowid != null) 'rowid': rowid, }); } - ContactsCompanion copyWith( - {Value? userId, - Value? username, - Value? displayName, - Value? nickName, - Value? avatarSvg, - Value? senderProfileCounter, - Value? accepted, - Value? requested, - Value? hidden, - Value? blocked, - Value? verified, - Value? deleted, - Value? alsoBestFriend, - Value? deleteMessagesAfterXMinutes, - Value? createdAt, + GroupsCompanion copyWith( + {Value? groupId, + Value? isGroupAdmin, + Value? isDirectChat, + Value? pinned, + Value? archived, + Value? groupName, Value? totalMediaCounter, + Value? alsoBestFriend, + Value? deleteMessagesAfterMilliseconds, + Value? createdAt, Value? lastMessageSend, Value? lastMessageReceived, Value? lastFlameCounterChange, Value? lastFlameSync, - Value? flameCounter}) { - return ContactsCompanion( - userId: userId ?? this.userId, - username: username ?? this.username, - displayName: displayName ?? this.displayName, - nickName: nickName ?? this.nickName, - avatarSvg: avatarSvg ?? this.avatarSvg, - senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, - accepted: accepted ?? this.accepted, - requested: requested ?? this.requested, - hidden: hidden ?? this.hidden, - blocked: blocked ?? this.blocked, - verified: verified ?? this.verified, - deleted: deleted ?? this.deleted, - alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, - deleteMessagesAfterXMinutes: - deleteMessagesAfterXMinutes ?? this.deleteMessagesAfterXMinutes, - createdAt: createdAt ?? this.createdAt, + Value? flameCounter, + Value? lastMessageExchange, + Value? rowid}) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + groupName: groupName ?? this.groupName, totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, lastMessageSend: lastMessageSend ?? this.lastMessageSend, lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, lastFlameCounterChange: lastFlameCounterChange ?? this.lastFlameCounterChange, lastFlameSync: lastFlameSync ?? this.lastFlameSync, flameCounter: flameCounter ?? this.flameCounter, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + rowid: rowid ?? this.rowid, ); } @override Map toColumns(bool nullToAbsent) { final map = {}; - if (userId.present) { - map['user_id'] = Variable(userId.value); + if (groupId.present) { + map['group_id'] = Variable(groupId.value); } - if (username.present) { - map['username'] = Variable(username.value); + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); } - if (displayName.present) { - map['display_name'] = Variable(displayName.value); + if (isDirectChat.present) { + map['is_direct_chat'] = Variable(isDirectChat.value); } - if (nickName.present) { - map['nick_name'] = Variable(nickName.value); + if (pinned.present) { + map['pinned'] = Variable(pinned.value); } - if (avatarSvg.present) { - map['avatar_svg'] = Variable(avatarSvg.value); + if (archived.present) { + map['archived'] = Variable(archived.value); } - if (senderProfileCounter.present) { - map['sender_profile_counter'] = Variable(senderProfileCounter.value); + if (groupName.present) { + map['group_name'] = Variable(groupName.value); } - if (accepted.present) { - map['accepted'] = Variable(accepted.value); - } - if (requested.present) { - map['requested'] = Variable(requested.value); - } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); - } - if (blocked.present) { - map['blocked'] = Variable(blocked.value); - } - if (verified.present) { - map['verified'] = Variable(verified.value); - } - if (deleted.present) { - map['deleted'] = Variable(deleted.value); + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable(totalMediaCounter.value); } if (alsoBestFriend.present) { map['also_best_friend'] = Variable(alsoBestFriend.value); } - if (deleteMessagesAfterXMinutes.present) { - map['delete_messages_after_x_minutes'] = - Variable(deleteMessagesAfterXMinutes.value); + if (deleteMessagesAfterMilliseconds.present) { + map['delete_messages_after_milliseconds'] = + Variable(deleteMessagesAfterMilliseconds.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } - if (totalMediaCounter.present) { - map['total_media_counter'] = Variable(totalMediaCounter.value); - } if (lastMessageSend.present) { map['last_message_send'] = Variable(lastMessageSend.value); } @@ -979,465 +1395,10 @@ class ContactsCompanion extends UpdateCompanion { if (flameCounter.present) { map['flame_counter'] = Variable(flameCounter.value); } - return map; - } - - @override - String toString() { - return (StringBuffer('ContactsCompanion(') - ..write('userId: $userId, ') - ..write('username: $username, ') - ..write('displayName: $displayName, ') - ..write('nickName: $nickName, ') - ..write('avatarSvg: $avatarSvg, ') - ..write('senderProfileCounter: $senderProfileCounter, ') - ..write('accepted: $accepted, ') - ..write('requested: $requested, ') - ..write('hidden: $hidden, ') - ..write('blocked: $blocked, ') - ..write('verified: $verified, ') - ..write('deleted: $deleted, ') - ..write('alsoBestFriend: $alsoBestFriend, ') - ..write('deleteMessagesAfterXMinutes: $deleteMessagesAfterXMinutes, ') - ..write('createdAt: $createdAt, ') - ..write('totalMediaCounter: $totalMediaCounter, ') - ..write('lastMessageSend: $lastMessageSend, ') - ..write('lastMessageReceived: $lastMessageReceived, ') - ..write('lastFlameCounterChange: $lastFlameCounterChange, ') - ..write('lastFlameSync: $lastFlameSync, ') - ..write('flameCounter: $flameCounter') - ..write(')')) - .toString(); - } -} - -class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $GroupsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _groupIdMeta = - const VerificationMeta('groupId'); - @override - late final GeneratedColumn groupId = GeneratedColumn( - 'group_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v4()); - static const VerificationMeta _isGroupAdminMeta = - const VerificationMeta('isGroupAdmin'); - @override - late final GeneratedColumn isGroupAdmin = GeneratedColumn( - 'is_group_admin', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_group_admin" IN (0, 1))')); - static const VerificationMeta _isGroupOfTwoMeta = - const VerificationMeta('isGroupOfTwo'); - @override - late final GeneratedColumn isGroupOfTwo = GeneratedColumn( - 'is_group_of_two', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_group_of_two" IN (0, 1))')); - static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); - @override - late final GeneratedColumn pinned = GeneratedColumn( - 'pinned', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _archivedMeta = - const VerificationMeta('archived'); - @override - late final GeneratedColumn archived = GeneratedColumn( - 'archived', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _groupNameMeta = - const VerificationMeta('groupName'); - @override - late final GeneratedColumn groupName = GeneratedColumn( - 'group_name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _lastMessageExchangeMeta = - const VerificationMeta('lastMessageExchange'); - @override - late final GeneratedColumn lastMessageExchange = - GeneratedColumn('last_message_exchange', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); - @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - @override - List get $columns => [ - groupId, - isGroupAdmin, - isGroupOfTwo, - pinned, - archived, - groupName, - lastMessageExchange, - createdAt - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'groups'; - @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('group_id')) { - context.handle(_groupIdMeta, - groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); - } - if (data.containsKey('is_group_admin')) { - context.handle( - _isGroupAdminMeta, - isGroupAdmin.isAcceptableOrUnknown( - data['is_group_admin']!, _isGroupAdminMeta)); - } else if (isInserting) { - context.missing(_isGroupAdminMeta); - } - if (data.containsKey('is_group_of_two')) { - context.handle( - _isGroupOfTwoMeta, - isGroupOfTwo.isAcceptableOrUnknown( - data['is_group_of_two']!, _isGroupOfTwoMeta)); - } else if (isInserting) { - context.missing(_isGroupOfTwoMeta); - } - if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, - pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); - } - if (data.containsKey('archived')) { - context.handle(_archivedMeta, - archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); - } - if (data.containsKey('group_name')) { - context.handle(_groupNameMeta, - groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta)); - } else if (isInserting) { - context.missing(_groupNameMeta); - } - if (data.containsKey('last_message_exchange')) { - context.handle( - _lastMessageExchangeMeta, - lastMessageExchange.isAcceptableOrUnknown( - data['last_message_exchange']!, _lastMessageExchangeMeta)); - } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - return context; - } - - @override - Set get $primaryKey => {groupId}; - @override - Group map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Group( - groupId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, - isGroupAdmin: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, - isGroupOfTwo: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_group_of_two'])!, - pinned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, - archived: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, - groupName: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, - lastMessageExchange: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}last_message_exchange'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - ); - } - - @override - $GroupsTable createAlias(String alias) { - return $GroupsTable(attachedDatabase, alias); - } -} - -class Group extends DataClass implements Insertable { - final String groupId; - final bool isGroupAdmin; - final bool isGroupOfTwo; - final bool pinned; - final bool archived; - final String groupName; - final DateTime lastMessageExchange; - final DateTime createdAt; - const Group( - {required this.groupId, - required this.isGroupAdmin, - required this.isGroupOfTwo, - required this.pinned, - required this.archived, - required this.groupName, - required this.lastMessageExchange, - required this.createdAt}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['group_id'] = Variable(groupId); - map['is_group_admin'] = Variable(isGroupAdmin); - map['is_group_of_two'] = Variable(isGroupOfTwo); - map['pinned'] = Variable(pinned); - map['archived'] = Variable(archived); - map['group_name'] = Variable(groupName); - map['last_message_exchange'] = Variable(lastMessageExchange); - map['created_at'] = Variable(createdAt); - return map; - } - - GroupsCompanion toCompanion(bool nullToAbsent) { - return GroupsCompanion( - groupId: Value(groupId), - isGroupAdmin: Value(isGroupAdmin), - isGroupOfTwo: Value(isGroupOfTwo), - pinned: Value(pinned), - archived: Value(archived), - groupName: Value(groupName), - lastMessageExchange: Value(lastMessageExchange), - createdAt: Value(createdAt), - ); - } - - factory Group.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return Group( - groupId: serializer.fromJson(json['groupId']), - isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), - isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), - pinned: serializer.fromJson(json['pinned']), - archived: serializer.fromJson(json['archived']), - groupName: serializer.fromJson(json['groupName']), - lastMessageExchange: - serializer.fromJson(json['lastMessageExchange']), - createdAt: serializer.fromJson(json['createdAt']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'groupId': serializer.toJson(groupId), - 'isGroupAdmin': serializer.toJson(isGroupAdmin), - 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), - 'pinned': serializer.toJson(pinned), - 'archived': serializer.toJson(archived), - 'groupName': serializer.toJson(groupName), - 'lastMessageExchange': serializer.toJson(lastMessageExchange), - 'createdAt': serializer.toJson(createdAt), - }; - } - - Group copyWith( - {String? groupId, - bool? isGroupAdmin, - bool? isGroupOfTwo, - bool? pinned, - bool? archived, - String? groupName, - DateTime? lastMessageExchange, - DateTime? createdAt}) => - Group( - groupId: groupId ?? this.groupId, - isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, - isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, - pinned: pinned ?? this.pinned, - archived: archived ?? this.archived, - groupName: groupName ?? this.groupName, - lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, - createdAt: createdAt ?? this.createdAt, - ); - Group copyWithCompanion(GroupsCompanion data) { - return Group( - groupId: data.groupId.present ? data.groupId.value : this.groupId, - isGroupAdmin: data.isGroupAdmin.present - ? data.isGroupAdmin.value - : this.isGroupAdmin, - isGroupOfTwo: data.isGroupOfTwo.present - ? data.isGroupOfTwo.value - : this.isGroupOfTwo, - pinned: data.pinned.present ? data.pinned.value : this.pinned, - archived: data.archived.present ? data.archived.value : this.archived, - groupName: data.groupName.present ? data.groupName.value : this.groupName, - lastMessageExchange: data.lastMessageExchange.present - ? data.lastMessageExchange.value - : this.lastMessageExchange, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - ); - } - - @override - String toString() { - return (StringBuffer('Group(') - ..write('groupId: $groupId, ') - ..write('isGroupAdmin: $isGroupAdmin, ') - ..write('isGroupOfTwo: $isGroupOfTwo, ') - ..write('pinned: $pinned, ') - ..write('archived: $archived, ') - ..write('groupName: $groupName, ') - ..write('lastMessageExchange: $lastMessageExchange, ') - ..write('createdAt: $createdAt') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, - archived, groupName, lastMessageExchange, createdAt); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is Group && - other.groupId == this.groupId && - other.isGroupAdmin == this.isGroupAdmin && - other.isGroupOfTwo == this.isGroupOfTwo && - other.pinned == this.pinned && - other.archived == this.archived && - other.groupName == this.groupName && - other.lastMessageExchange == this.lastMessageExchange && - other.createdAt == this.createdAt); -} - -class GroupsCompanion extends UpdateCompanion { - final Value groupId; - final Value isGroupAdmin; - final Value isGroupOfTwo; - final Value pinned; - final Value archived; - final Value groupName; - final Value lastMessageExchange; - final Value createdAt; - final Value rowid; - const GroupsCompanion({ - this.groupId = const Value.absent(), - this.isGroupAdmin = const Value.absent(), - this.isGroupOfTwo = const Value.absent(), - this.pinned = const Value.absent(), - this.archived = const Value.absent(), - this.groupName = const Value.absent(), - this.lastMessageExchange = const Value.absent(), - this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), - }); - GroupsCompanion.insert({ - this.groupId = const Value.absent(), - required bool isGroupAdmin, - required bool isGroupOfTwo, - this.pinned = const Value.absent(), - this.archived = const Value.absent(), - required String groupName, - this.lastMessageExchange = const Value.absent(), - this.createdAt = const Value.absent(), - this.rowid = const Value.absent(), - }) : isGroupAdmin = Value(isGroupAdmin), - isGroupOfTwo = Value(isGroupOfTwo), - groupName = Value(groupName); - static Insertable custom({ - Expression? groupId, - Expression? isGroupAdmin, - Expression? isGroupOfTwo, - Expression? pinned, - Expression? archived, - Expression? groupName, - Expression? lastMessageExchange, - Expression? createdAt, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (groupId != null) 'group_id': groupId, - if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, - if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, - if (pinned != null) 'pinned': pinned, - if (archived != null) 'archived': archived, - if (groupName != null) 'group_name': groupName, - if (lastMessageExchange != null) - 'last_message_exchange': lastMessageExchange, - if (createdAt != null) 'created_at': createdAt, - if (rowid != null) 'rowid': rowid, - }); - } - - GroupsCompanion copyWith( - {Value? groupId, - Value? isGroupAdmin, - Value? isGroupOfTwo, - Value? pinned, - Value? archived, - Value? groupName, - Value? lastMessageExchange, - Value? createdAt, - Value? rowid}) { - return GroupsCompanion( - groupId: groupId ?? this.groupId, - isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, - isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, - pinned: pinned ?? this.pinned, - archived: archived ?? this.archived, - groupName: groupName ?? this.groupName, - lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, - createdAt: createdAt ?? this.createdAt, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (groupId.present) { - map['group_id'] = Variable(groupId.value); - } - if (isGroupAdmin.present) { - map['is_group_admin'] = Variable(isGroupAdmin.value); - } - if (isGroupOfTwo.present) { - map['is_group_of_two'] = Variable(isGroupOfTwo.value); - } - if (pinned.present) { - map['pinned'] = Variable(pinned.value); - } - if (archived.present) { - map['archived'] = Variable(archived.value); - } - if (groupName.present) { - map['group_name'] = Variable(groupName.value); - } if (lastMessageExchange.present) { map['last_message_exchange'] = Variable(lastMessageExchange.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -1449,12 +1410,21 @@ class GroupsCompanion extends UpdateCompanion { return (StringBuffer('GroupsCompanion(') ..write('groupId: $groupId, ') ..write('isGroupAdmin: $isGroupAdmin, ') - ..write('isGroupOfTwo: $isGroupOfTwo, ') + ..write('isDirectChat: $isDirectChat, ') ..write('pinned: $pinned, ') ..write('archived: $archived, ') ..write('groupName: $groupName, ') - ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('lastMessageExchange: $lastMessageExchange, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2309,16 +2279,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("is_deleted_from_sender" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _isEditedMeta = - const VerificationMeta('isEdited'); + static const VerificationMeta _openedAtMeta = + const VerificationMeta('openedAt'); @override - late final GeneratedColumn isEdited = GeneratedColumn( - 'is_edited', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("is_edited" IN (0, 1))'), - defaultValue: const Constant(false)); + late final GeneratedColumn openedAt = GeneratedColumn( + 'opened_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -2327,6 +2293,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); + static const VerificationMeta _modifiedAtMeta = + const VerificationMeta('modifiedAt'); + @override + late final GeneratedColumn modifiedAt = GeneratedColumn( + 'modified_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); @override List get $columns => [ groupId, @@ -2339,8 +2311,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { downloadToken, quotesMessageId, isDeletedFromSender, - isEdited, - createdAt + openedAt, + createdAt, + modifiedAt ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2398,14 +2371,20 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { isDeletedFromSender.isAcceptableOrUnknown( data['is_deleted_from_sender']!, _isDeletedFromSenderMeta)); } - if (data.containsKey('is_edited')) { - context.handle(_isEditedMeta, - isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta)); + if (data.containsKey('opened_at')) { + context.handle(_openedAtMeta, + openedAt.isAcceptableOrUnknown(data['opened_at']!, _openedAtMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } + if (data.containsKey('modified_at')) { + context.handle( + _modifiedAtMeta, + modifiedAt.isAcceptableOrUnknown( + data['modified_at']!, _modifiedAtMeta)); + } return context; } @@ -2435,10 +2414,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), isDeletedFromSender: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, - isEdited: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_edited'])!, + openedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + modifiedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), ); } @@ -2462,8 +2443,9 @@ class Message extends DataClass implements Insertable { final Uint8List? downloadToken; final String? quotesMessageId; final bool isDeletedFromSender; - final bool isEdited; + final DateTime? openedAt; final DateTime createdAt; + final DateTime? modifiedAt; const Message( {required this.groupId, required this.messageId, @@ -2475,8 +2457,9 @@ class Message extends DataClass implements Insertable { this.downloadToken, this.quotesMessageId, required this.isDeletedFromSender, - required this.isEdited, - required this.createdAt}); + this.openedAt, + required this.createdAt, + this.modifiedAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2502,8 +2485,13 @@ class Message extends DataClass implements Insertable { map['quotes_message_id'] = Variable(quotesMessageId); } map['is_deleted_from_sender'] = Variable(isDeletedFromSender); - map['is_edited'] = Variable(isEdited); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable(openedAt); + } map['created_at'] = Variable(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable(modifiedAt); + } return map; } @@ -2529,8 +2517,13 @@ class Message extends DataClass implements Insertable { ? const Value.absent() : Value(quotesMessageId), isDeletedFromSender: Value(isDeletedFromSender), - isEdited: Value(isEdited), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), ); } @@ -2550,8 +2543,9 @@ class Message extends DataClass implements Insertable { quotesMessageId: serializer.fromJson(json['quotesMessageId']), isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), - isEdited: serializer.fromJson(json['isEdited']), + openedAt: serializer.fromJson(json['openedAt']), createdAt: serializer.fromJson(json['createdAt']), + modifiedAt: serializer.fromJson(json['modifiedAt']), ); } @override @@ -2569,8 +2563,9 @@ class Message extends DataClass implements Insertable { 'downloadToken': serializer.toJson(downloadToken), 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), - 'isEdited': serializer.toJson(isEdited), + 'openedAt': serializer.toJson(openedAt), 'createdAt': serializer.toJson(createdAt), + 'modifiedAt': serializer.toJson(modifiedAt), }; } @@ -2585,8 +2580,9 @@ class Message extends DataClass implements Insertable { Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, - bool? isEdited, - DateTime? createdAt}) => + Value openedAt = const Value.absent(), + DateTime? createdAt, + Value modifiedAt = const Value.absent()}) => Message( groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, @@ -2601,8 +2597,9 @@ class Message extends DataClass implements Insertable { ? quotesMessageId.value : this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, - isEdited: isEdited ?? this.isEdited, + openedAt: openedAt.present ? openedAt.value : this.openedAt, createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, ); Message copyWithCompanion(MessagesCompanion data) { return Message( @@ -2623,8 +2620,10 @@ class Message extends DataClass implements Insertable { isDeletedFromSender: data.isDeletedFromSender.present ? data.isDeletedFromSender.value : this.isDeletedFromSender, - isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: + data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, ); } @@ -2641,8 +2640,9 @@ class Message extends DataClass implements Insertable { ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') - ..write('isEdited: $isEdited, ') - ..write('createdAt: $createdAt') + ..write('openedAt: $openedAt, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt') ..write(')')) .toString(); } @@ -2659,8 +2659,9 @@ class Message extends DataClass implements Insertable { $driftBlobEquality.hash(downloadToken), quotesMessageId, isDeletedFromSender, - isEdited, - createdAt); + openedAt, + createdAt, + modifiedAt); @override bool operator ==(Object other) => identical(this, other) || @@ -2675,8 +2676,9 @@ class Message extends DataClass implements Insertable { $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && - other.isEdited == this.isEdited && - other.createdAt == this.createdAt); + other.openedAt == this.openedAt && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt); } class MessagesCompanion extends UpdateCompanion { @@ -2690,8 +2692,9 @@ class MessagesCompanion extends UpdateCompanion { final Value downloadToken; final Value quotesMessageId; final Value isDeletedFromSender; - final Value isEdited; + final Value openedAt; final Value createdAt; + final Value modifiedAt; final Value rowid; const MessagesCompanion({ this.groupId = const Value.absent(), @@ -2704,8 +2707,9 @@ class MessagesCompanion extends UpdateCompanion { this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), - this.isEdited = const Value.absent(), + this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), }); MessagesCompanion.insert({ @@ -2719,8 +2723,9 @@ class MessagesCompanion extends UpdateCompanion { this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), - this.isEdited = const Value.absent(), + this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), type = Value(type); @@ -2735,8 +2740,9 @@ class MessagesCompanion extends UpdateCompanion { Expression? downloadToken, Expression? quotesMessageId, Expression? isDeletedFromSender, - Expression? isEdited, + Expression? openedAt, Expression? createdAt, + Expression? modifiedAt, Expression? rowid, }) { return RawValuesInsertable({ @@ -2751,8 +2757,9 @@ class MessagesCompanion extends UpdateCompanion { if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, - if (isEdited != null) 'is_edited': isEdited, + if (openedAt != null) 'opened_at': openedAt, if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, if (rowid != null) 'rowid': rowid, }); } @@ -2768,8 +2775,9 @@ class MessagesCompanion extends UpdateCompanion { Value? downloadToken, Value? quotesMessageId, Value? isDeletedFromSender, - Value? isEdited, + Value? openedAt, Value? createdAt, + Value? modifiedAt, Value? rowid}) { return MessagesCompanion( groupId: groupId ?? this.groupId, @@ -2782,8 +2790,9 @@ class MessagesCompanion extends UpdateCompanion { downloadToken: downloadToken ?? this.downloadToken, quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, - isEdited: isEdited ?? this.isEdited, + openedAt: openedAt ?? this.openedAt, createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, rowid: rowid ?? this.rowid, ); } @@ -2822,12 +2831,15 @@ class MessagesCompanion extends UpdateCompanion { if (isDeletedFromSender.present) { map['is_deleted_from_sender'] = Variable(isDeletedFromSender.value); } - if (isEdited.present) { - map['is_edited'] = Variable(isEdited.value); + if (openedAt.present) { + map['opened_at'] = Variable(openedAt.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (modifiedAt.present) { + map['modified_at'] = Variable(modifiedAt.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2847,8 +2859,9 @@ class MessagesCompanion extends UpdateCompanion { ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') - ..write('isEdited: $isEdited, ') + ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2883,8 +2896,8 @@ class $MessageHistoriesTable extends MessageHistories const VerificationMeta('contactId'); @override late final GeneratedColumn contactId = GeneratedColumn( - 'contact_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + 'contact_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -2924,8 +2937,6 @@ class $MessageHistoriesTable extends MessageHistories if (data.containsKey('contact_id')) { context.handle(_contactIdMeta, contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); - } else if (isInserting) { - context.missing(_contactIdMeta); } if (data.containsKey('content')) { context.handle(_contentMeta, @@ -2949,7 +2960,7 @@ class $MessageHistoriesTable extends MessageHistories messageId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, contactId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), content: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}content']), createdAt: attachedDatabase.typeMapping @@ -2966,13 +2977,13 @@ class $MessageHistoriesTable extends MessageHistories class MessageHistory extends DataClass implements Insertable { final int id; final String messageId; - final int contactId; + final int? contactId; final String? content; final DateTime createdAt; const MessageHistory( {required this.id, required this.messageId, - required this.contactId, + this.contactId, this.content, required this.createdAt}); @override @@ -2980,7 +2991,9 @@ class MessageHistory extends DataClass implements Insertable { final map = {}; map['id'] = Variable(id); map['message_id'] = Variable(messageId); - map['contact_id'] = Variable(contactId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -2992,7 +3005,9 @@ class MessageHistory extends DataClass implements Insertable { return MessageHistoriesCompanion( id: Value(id), messageId: Value(messageId), - contactId: Value(contactId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), content: content == null && nullToAbsent ? const Value.absent() : Value(content), @@ -3006,7 +3021,7 @@ class MessageHistory extends DataClass implements Insertable { return MessageHistory( id: serializer.fromJson(json['id']), messageId: serializer.fromJson(json['messageId']), - contactId: serializer.fromJson(json['contactId']), + contactId: serializer.fromJson(json['contactId']), content: serializer.fromJson(json['content']), createdAt: serializer.fromJson(json['createdAt']), ); @@ -3017,7 +3032,7 @@ class MessageHistory extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'messageId': serializer.toJson(messageId), - 'contactId': serializer.toJson(contactId), + 'contactId': serializer.toJson(contactId), 'content': serializer.toJson(content), 'createdAt': serializer.toJson(createdAt), }; @@ -3026,13 +3041,13 @@ class MessageHistory extends DataClass implements Insertable { MessageHistory copyWith( {int? id, String? messageId, - int? contactId, + Value contactId = const Value.absent(), Value content = const Value.absent(), DateTime? createdAt}) => MessageHistory( id: id ?? this.id, messageId: messageId ?? this.messageId, - contactId: contactId ?? this.contactId, + contactId: contactId.present ? contactId.value : this.contactId, content: content.present ? content.value : this.content, createdAt: createdAt ?? this.createdAt, ); @@ -3074,7 +3089,7 @@ class MessageHistory extends DataClass implements Insertable { class MessageHistoriesCompanion extends UpdateCompanion { final Value id; final Value messageId; - final Value contactId; + final Value contactId; final Value content; final Value createdAt; const MessageHistoriesCompanion({ @@ -3087,11 +3102,10 @@ class MessageHistoriesCompanion extends UpdateCompanion { MessageHistoriesCompanion.insert({ this.id = const Value.absent(), required String messageId, - required int contactId, + this.contactId = const Value.absent(), this.content = const Value.absent(), this.createdAt = const Value.absent(), - }) : messageId = Value(messageId), - contactId = Value(contactId); + }) : messageId = Value(messageId); static Insertable custom({ Expression? id, Expression? messageId, @@ -3111,7 +3125,7 @@ class MessageHistoriesCompanion extends UpdateCompanion { MessageHistoriesCompanion copyWith( {Value? id, Value? messageId, - Value? contactId, + Value? contactId, Value? content, Value? createdAt}) { return MessageHistoriesCompanion( @@ -5827,15 +5841,6 @@ class $MessageActionsTable extends MessageActions final GeneratedDatabase attachedDatabase; final String? _alias; $MessageActionsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @override @@ -5866,8 +5871,7 @@ class $MessageActionsTable extends MessageActions requiredDuringInsert: false, defaultValue: currentDateAndTime); @override - List get $columns => - [id, messageId, contactId, type, actionAt]; + List get $columns => [messageId, contactId, type, actionAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -5878,9 +5882,6 @@ class $MessageActionsTable extends MessageActions {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } if (data.containsKey('message_id')) { context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); @@ -5901,13 +5902,11 @@ class $MessageActionsTable extends MessageActions } @override - Set get $primaryKey => {id}; + Set get $primaryKey => {messageId, contactId, type}; @override MessageAction map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MessageAction( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, messageId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, contactId: attachedDatabase.typeMapping @@ -5930,21 +5929,18 @@ class $MessageActionsTable extends MessageActions } class MessageAction extends DataClass implements Insertable { - final int id; final String messageId; final int contactId; final MessageActionType type; final DateTime actionAt; const MessageAction( - {required this.id, - required this.messageId, + {required this.messageId, required this.contactId, required this.type, required this.actionAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); map['message_id'] = Variable(messageId); map['contact_id'] = Variable(contactId); { @@ -5957,7 +5953,6 @@ class MessageAction extends DataClass implements Insertable { MessageActionsCompanion toCompanion(bool nullToAbsent) { return MessageActionsCompanion( - id: Value(id), messageId: Value(messageId), contactId: Value(contactId), type: Value(type), @@ -5969,7 +5964,6 @@ class MessageAction extends DataClass implements Insertable { {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return MessageAction( - id: serializer.fromJson(json['id']), messageId: serializer.fromJson(json['messageId']), contactId: serializer.fromJson(json['contactId']), type: $MessageActionsTable.$convertertype @@ -5981,7 +5975,6 @@ class MessageAction extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), 'messageId': serializer.toJson(messageId), 'contactId': serializer.toJson(contactId), 'type': serializer @@ -5991,13 +5984,11 @@ class MessageAction extends DataClass implements Insertable { } MessageAction copyWith( - {int? id, - String? messageId, + {String? messageId, int? contactId, MessageActionType? type, DateTime? actionAt}) => MessageAction( - id: id ?? this.id, messageId: messageId ?? this.messageId, contactId: contactId ?? this.contactId, type: type ?? this.type, @@ -6005,7 +5996,6 @@ class MessageAction extends DataClass implements Insertable { ); MessageAction copyWithCompanion(MessageActionsCompanion data) { return MessageAction( - id: data.id.present ? data.id.value : this.id, messageId: data.messageId.present ? data.messageId.value : this.messageId, contactId: data.contactId.present ? data.contactId.value : this.contactId, type: data.type.present ? data.type.value : this.type, @@ -6016,7 +6006,6 @@ class MessageAction extends DataClass implements Insertable { @override String toString() { return (StringBuffer('MessageAction(') - ..write('id: $id, ') ..write('messageId: $messageId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') @@ -6026,12 +6015,11 @@ class MessageAction extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(id, messageId, contactId, type, actionAt); + int get hashCode => Object.hash(messageId, contactId, type, actionAt); @override bool operator ==(Object other) => identical(this, other) || (other is MessageAction && - other.id == this.id && other.messageId == this.messageId && other.contactId == this.contactId && other.type == this.type && @@ -6039,64 +6027,61 @@ class MessageAction extends DataClass implements Insertable { } class MessageActionsCompanion extends UpdateCompanion { - final Value id; final Value messageId; final Value contactId; final Value type; final Value actionAt; + final Value rowid; const MessageActionsCompanion({ - this.id = const Value.absent(), this.messageId = const Value.absent(), this.contactId = const Value.absent(), this.type = const Value.absent(), this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), }); MessageActionsCompanion.insert({ - this.id = const Value.absent(), required String messageId, required int contactId, required MessageActionType type, this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), }) : messageId = Value(messageId), contactId = Value(contactId), type = Value(type); static Insertable custom({ - Expression? id, Expression? messageId, Expression? contactId, Expression? type, Expression? actionAt, + Expression? rowid, }) { return RawValuesInsertable({ - if (id != null) 'id': id, if (messageId != null) 'message_id': messageId, if (contactId != null) 'contact_id': contactId, if (type != null) 'type': type, if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, }); } MessageActionsCompanion copyWith( - {Value? id, - Value? messageId, + {Value? messageId, Value? contactId, Value? type, - Value? actionAt}) { + Value? actionAt, + Value? rowid}) { return MessageActionsCompanion( - id: id ?? this.id, messageId: messageId ?? this.messageId, contactId: contactId ?? this.contactId, type: type ?? this.type, actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, ); } @override Map toColumns(bool nullToAbsent) { final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } if (messageId.present) { map['message_id'] = Variable(messageId.value); } @@ -6110,17 +6095,20 @@ class MessageActionsCompanion extends UpdateCompanion { if (actionAt.present) { map['action_at'] = Variable(actionAt.value); } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } return map; } @override String toString() { return (StringBuffer('MessageActionsCompanion(') - ..write('id: $id, ') ..write('messageId: $messageId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') - ..write('actionAt: $actionAt') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') ..write(')')) .toString(); } @@ -6266,19 +6254,10 @@ typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ Value senderProfileCounter, Value accepted, Value requested, - Value hidden, Value blocked, Value verified, Value deleted, - Value alsoBestFriend, - Value deleteMessagesAfterXMinutes, Value createdAt, - Value totalMediaCounter, - Value lastMessageSend, - Value lastMessageReceived, - Value lastFlameCounterChange, - Value lastFlameSync, - Value flameCounter, }); typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ Value userId, @@ -6289,19 +6268,10 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ Value senderProfileCounter, Value accepted, Value requested, - Value hidden, Value blocked, Value verified, Value deleted, - Value alsoBestFriend, - Value deleteMessagesAfterXMinutes, Value createdAt, - Value totalMediaCounter, - Value lastMessageSend, - Value lastMessageReceived, - Value lastFlameCounterChange, - Value lastFlameSync, - Value flameCounter, }); final class $$ContactsTableReferences @@ -6444,9 +6414,6 @@ class $$ContactsTableFilterComposer ColumnFilters get requested => $composableBuilder( column: $table.requested, builder: (column) => ColumnFilters(column)); - ColumnFilters get hidden => $composableBuilder( - column: $table.hidden, builder: (column) => ColumnFilters(column)); - ColumnFilters get blocked => $composableBuilder( column: $table.blocked, builder: (column) => ColumnFilters(column)); @@ -6456,39 +6423,9 @@ class $$ContactsTableFilterComposer ColumnFilters get deleted => $composableBuilder( column: $table.deleted, builder: (column) => ColumnFilters(column)); - ColumnFilters get alsoBestFriend => $composableBuilder( - column: $table.alsoBestFriend, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get deleteMessagesAfterXMinutes => $composableBuilder( - column: $table.deleteMessagesAfterXMinutes, - builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get totalMediaCounter => $composableBuilder( - column: $table.totalMediaCounter, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastMessageSend => $composableBuilder( - column: $table.lastMessageSend, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastMessageReceived => $composableBuilder( - column: $table.lastMessageReceived, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastFlameCounterChange => $composableBuilder( - column: $table.lastFlameCounterChange, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastFlameSync => $composableBuilder( - column: $table.lastFlameSync, builder: (column) => ColumnFilters(column)); - - ColumnFilters get flameCounter => $composableBuilder( - column: $table.flameCounter, builder: (column) => ColumnFilters(column)); - Expression messagesRefs( Expression Function($$MessagesTableFilterComposer f) f) { final $$MessagesTableFilterComposer composer = $composerBuilder( @@ -6654,9 +6591,6 @@ class $$ContactsTableOrderingComposer ColumnOrderings get requested => $composableBuilder( column: $table.requested, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get hidden => $composableBuilder( - column: $table.hidden, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get blocked => $composableBuilder( column: $table.blocked, builder: (column) => ColumnOrderings(column)); @@ -6666,40 +6600,8 @@ class $$ContactsTableOrderingComposer ColumnOrderings get deleted => $composableBuilder( column: $table.deleted, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get alsoBestFriend => $composableBuilder( - column: $table.alsoBestFriend, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get deleteMessagesAfterXMinutes => $composableBuilder( - column: $table.deleteMessagesAfterXMinutes, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get totalMediaCounter => $composableBuilder( - column: $table.totalMediaCounter, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastMessageSend => $composableBuilder( - column: $table.lastMessageSend, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastMessageReceived => $composableBuilder( - column: $table.lastMessageReceived, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastFlameCounterChange => $composableBuilder( - column: $table.lastFlameCounterChange, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastFlameSync => $composableBuilder( - column: $table.lastFlameSync, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get flameCounter => $composableBuilder( - column: $table.flameCounter, - builder: (column) => ColumnOrderings(column)); } class $$ContactsTableAnnotationComposer @@ -6735,9 +6637,6 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get requested => $composableBuilder(column: $table.requested, builder: (column) => column); - GeneratedColumn get hidden => - $composableBuilder(column: $table.hidden, builder: (column) => column); - GeneratedColumn get blocked => $composableBuilder(column: $table.blocked, builder: (column) => column); @@ -6747,33 +6646,9 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get deleted => $composableBuilder(column: $table.deleted, builder: (column) => column); - GeneratedColumn get alsoBestFriend => $composableBuilder( - column: $table.alsoBestFriend, builder: (column) => column); - - GeneratedColumn get deleteMessagesAfterXMinutes => $composableBuilder( - column: $table.deleteMessagesAfterXMinutes, builder: (column) => column); - GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get totalMediaCounter => $composableBuilder( - column: $table.totalMediaCounter, builder: (column) => column); - - GeneratedColumn get lastMessageSend => $composableBuilder( - column: $table.lastMessageSend, builder: (column) => column); - - GeneratedColumn get lastMessageReceived => $composableBuilder( - column: $table.lastMessageReceived, builder: (column) => column); - - GeneratedColumn get lastFlameCounterChange => $composableBuilder( - column: $table.lastFlameCounterChange, builder: (column) => column); - - GeneratedColumn get lastFlameSync => $composableBuilder( - column: $table.lastFlameSync, builder: (column) => column); - - GeneratedColumn get flameCounter => $composableBuilder( - column: $table.flameCounter, builder: (column) => column); - Expression messagesRefs( Expression Function($$MessagesTableAnnotationComposer a) f) { final $$MessagesTableAnnotationComposer composer = $composerBuilder( @@ -6943,19 +6818,10 @@ class $$ContactsTableTableManager extends RootTableManager< Value senderProfileCounter = const Value.absent(), Value accepted = const Value.absent(), Value requested = const Value.absent(), - Value hidden = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), Value deleted = const Value.absent(), - Value alsoBestFriend = const Value.absent(), - Value deleteMessagesAfterXMinutes = const Value.absent(), Value createdAt = const Value.absent(), - Value totalMediaCounter = const Value.absent(), - Value lastMessageSend = const Value.absent(), - Value lastMessageReceived = const Value.absent(), - Value lastFlameCounterChange = const Value.absent(), - Value lastFlameSync = const Value.absent(), - Value flameCounter = const Value.absent(), }) => ContactsCompanion( userId: userId, @@ -6966,19 +6832,10 @@ class $$ContactsTableTableManager extends RootTableManager< senderProfileCounter: senderProfileCounter, accepted: accepted, requested: requested, - hidden: hidden, blocked: blocked, verified: verified, deleted: deleted, - alsoBestFriend: alsoBestFriend, - deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, createdAt: createdAt, - totalMediaCounter: totalMediaCounter, - lastMessageSend: lastMessageSend, - lastMessageReceived: lastMessageReceived, - lastFlameCounterChange: lastFlameCounterChange, - lastFlameSync: lastFlameSync, - flameCounter: flameCounter, ), createCompanionCallback: ({ Value userId = const Value.absent(), @@ -6989,19 +6846,10 @@ class $$ContactsTableTableManager extends RootTableManager< Value senderProfileCounter = const Value.absent(), Value accepted = const Value.absent(), Value requested = const Value.absent(), - Value hidden = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), Value deleted = const Value.absent(), - Value alsoBestFriend = const Value.absent(), - Value deleteMessagesAfterXMinutes = const Value.absent(), Value createdAt = const Value.absent(), - Value totalMediaCounter = const Value.absent(), - Value lastMessageSend = const Value.absent(), - Value lastMessageReceived = const Value.absent(), - Value lastFlameCounterChange = const Value.absent(), - Value lastFlameSync = const Value.absent(), - Value flameCounter = const Value.absent(), }) => ContactsCompanion.insert( userId: userId, @@ -7012,19 +6860,10 @@ class $$ContactsTableTableManager extends RootTableManager< senderProfileCounter: senderProfileCounter, accepted: accepted, requested: requested, - hidden: hidden, blocked: blocked, verified: verified, deleted: deleted, - alsoBestFriend: alsoBestFriend, - deleteMessagesAfterXMinutes: deleteMessagesAfterXMinutes, createdAt: createdAt, - totalMediaCounter: totalMediaCounter, - lastMessageSend: lastMessageSend, - lastMessageReceived: lastMessageReceived, - lastFlameCounterChange: lastFlameCounterChange, - lastFlameSync: lastFlameSync, - flameCounter: flameCounter, ), withReferenceMapper: (p0) => p0 .map((e) => @@ -7155,23 +6994,39 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ Value groupId, required bool isGroupAdmin, - required bool isGroupOfTwo, + required bool isDirectChat, Value pinned, Value archived, required String groupName, - Value lastMessageExchange, + Value totalMediaCounter, + Value alsoBestFriend, + Value deleteMessagesAfterMilliseconds, Value createdAt, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, + Value lastMessageExchange, Value rowid, }); typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value groupId, Value isGroupAdmin, - Value isGroupOfTwo, + Value isDirectChat, Value pinned, Value archived, Value groupName, - Value lastMessageExchange, + Value totalMediaCounter, + Value alsoBestFriend, + Value deleteMessagesAfterMilliseconds, Value createdAt, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, + Value lastMessageExchange, Value rowid, }); @@ -7209,8 +7064,8 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get isGroupAdmin => $composableBuilder( column: $table.isGroupAdmin, builder: (column) => ColumnFilters(column)); - ColumnFilters get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, builder: (column) => ColumnFilters(column)); + ColumnFilters get isDirectChat => $composableBuilder( + column: $table.isDirectChat, builder: (column) => ColumnFilters(column)); ColumnFilters get pinned => $composableBuilder( column: $table.pinned, builder: (column) => ColumnFilters(column)); @@ -7221,13 +7076,43 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get groupName => $composableBuilder( column: $table.groupName, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, + ColumnFilters get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get deleteMessagesAfterMilliseconds => $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => ColumnFilters(column)); + + ColumnFilters get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnFilters(column)); + Expression messagesRefs( Expression Function($$MessagesTableFilterComposer f) f) { final $$MessagesTableFilterComposer composer = $composerBuilder( @@ -7265,8 +7150,8 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { column: $table.isGroupAdmin, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, + ColumnOrderings get isDirectChat => $composableBuilder( + column: $table.isDirectChat, builder: (column) => ColumnOrderings(column)); ColumnOrderings get pinned => $composableBuilder( @@ -7278,12 +7163,45 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get groupName => $composableBuilder( column: $table.groupName, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, + ColumnOrderings get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get flameCounter => $composableBuilder( + column: $table.flameCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnOrderings(column)); } class $$GroupsTableAnnotationComposer @@ -7301,8 +7219,8 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get isGroupAdmin => $composableBuilder( column: $table.isGroupAdmin, builder: (column) => column); - GeneratedColumn get isGroupOfTwo => $composableBuilder( - column: $table.isGroupOfTwo, builder: (column) => column); + GeneratedColumn get isDirectChat => $composableBuilder( + column: $table.isDirectChat, builder: (column) => column); GeneratedColumn get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); @@ -7313,12 +7231,38 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get groupName => $composableBuilder(column: $table.groupName, builder: (column) => column); - GeneratedColumn get lastMessageExchange => $composableBuilder( - column: $table.lastMessageExchange, builder: (column) => column); + GeneratedColumn get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, builder: (column) => column); + + GeneratedColumn get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, builder: (column) => column); + + GeneratedColumn get deleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, + builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, builder: (column) => column); + + GeneratedColumn get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, builder: (column) => column); + + GeneratedColumn get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, builder: (column) => column); + + GeneratedColumn get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => column); + + GeneratedColumn get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => column); + + GeneratedColumn get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, builder: (column) => column); + Expression messagesRefs( Expression Function($$MessagesTableAnnotationComposer a) f) { final $$MessagesTableAnnotationComposer composer = $composerBuilder( @@ -7366,45 +7310,77 @@ class $$GroupsTableTableManager extends RootTableManager< updateCompanionCallback: ({ Value groupId = const Value.absent(), Value isGroupAdmin = const Value.absent(), - Value isGroupOfTwo = const Value.absent(), + Value isDirectChat = const Value.absent(), Value pinned = const Value.absent(), Value archived = const Value.absent(), Value groupName = const Value.absent(), - Value lastMessageExchange = const Value.absent(), + Value totalMediaCounter = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterMilliseconds = const Value.absent(), Value createdAt = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + Value lastMessageExchange = const Value.absent(), Value rowid = const Value.absent(), }) => GroupsCompanion( groupId: groupId, isGroupAdmin: isGroupAdmin, - isGroupOfTwo: isGroupOfTwo, + isDirectChat: isDirectChat, pinned: pinned, archived: archived, groupName: groupName, - lastMessageExchange: lastMessageExchange, + totalMediaCounter: totalMediaCounter, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds, createdAt: createdAt, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + lastMessageExchange: lastMessageExchange, rowid: rowid, ), createCompanionCallback: ({ Value groupId = const Value.absent(), required bool isGroupAdmin, - required bool isGroupOfTwo, + required bool isDirectChat, Value pinned = const Value.absent(), Value archived = const Value.absent(), required String groupName, - Value lastMessageExchange = const Value.absent(), + Value totalMediaCounter = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterMilliseconds = const Value.absent(), Value createdAt = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + Value lastMessageExchange = const Value.absent(), Value rowid = const Value.absent(), }) => GroupsCompanion.insert( groupId: groupId, isGroupAdmin: isGroupAdmin, - isGroupOfTwo: isGroupOfTwo, + isDirectChat: isDirectChat, pinned: pinned, archived: archived, groupName: groupName, - lastMessageExchange: lastMessageExchange, + totalMediaCounter: totalMediaCounter, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds, createdAt: createdAt, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + lastMessageExchange: lastMessageExchange, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -7871,8 +7847,9 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, - Value isEdited, + Value openedAt, Value createdAt, + Value modifiedAt, Value rowid, }); typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ @@ -7886,8 +7863,9 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, - Value isEdited, + Value openedAt, Value createdAt, + Value modifiedAt, Value rowid, }); @@ -8051,12 +8029,15 @@ class $$MessagesTableFilterComposer column: $table.isDeletedFromSender, builder: (column) => ColumnFilters(column)); - ColumnFilters get isEdited => $composableBuilder( - column: $table.isEdited, builder: (column) => ColumnFilters(column)); + ColumnFilters get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); + $$GroupsTableFilterComposer get groupId { final $$GroupsTableFilterComposer composer = $composerBuilder( composer: this, @@ -8251,12 +8232,15 @@ class $$MessagesTableOrderingComposer column: $table.isDeletedFromSender, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get isEdited => $composableBuilder( - column: $table.isEdited, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnOrderings(column)); ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); + $$GroupsTableOrderingComposer get groupId { final $$GroupsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -8365,12 +8349,15 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => column); - GeneratedColumn get isEdited => - $composableBuilder(column: $table.isEdited, builder: (column) => column); + GeneratedColumn get openedAt => + $composableBuilder(column: $table.openedAt, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => column); + $$GroupsTableAnnotationComposer get groupId { final $$GroupsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -8577,8 +8564,9 @@ class $$MessagesTableTableManager extends RootTableManager< Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), - Value isEdited = const Value.absent(), + Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion( @@ -8592,8 +8580,9 @@ class $$MessagesTableTableManager extends RootTableManager< downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, - isEdited: isEdited, + openedAt: openedAt, createdAt: createdAt, + modifiedAt: modifiedAt, rowid: rowid, ), createCompanionCallback: ({ @@ -8607,8 +8596,9 @@ class $$MessagesTableTableManager extends RootTableManager< Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), - Value isEdited = const Value.absent(), + Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion.insert( @@ -8622,8 +8612,9 @@ class $$MessagesTableTableManager extends RootTableManager< downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, - isEdited: isEdited, + openedAt: openedAt, createdAt: createdAt, + modifiedAt: modifiedAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -8788,7 +8779,7 @@ typedef $$MessageHistoriesTableCreateCompanionBuilder = MessageHistoriesCompanion Function({ Value id, required String messageId, - required int contactId, + Value contactId, Value content, Value createdAt, }); @@ -8796,7 +8787,7 @@ typedef $$MessageHistoriesTableUpdateCompanionBuilder = MessageHistoriesCompanion Function({ Value id, Value messageId, - Value contactId, + Value contactId, Value content, Value createdAt, }); @@ -8974,7 +8965,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< updateCompanionCallback: ({ Value id = const Value.absent(), Value messageId = const Value.absent(), - Value contactId = const Value.absent(), + Value contactId = const Value.absent(), Value content = const Value.absent(), Value createdAt = const Value.absent(), }) => @@ -8988,7 +8979,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager< createCompanionCallback: ({ Value id = const Value.absent(), required String messageId, - required int contactId, + Value contactId = const Value.absent(), Value content = const Value.absent(), Value createdAt = const Value.absent(), }) => @@ -11220,19 +11211,19 @@ typedef $$SignalContactSignedPreKeysTableProcessedTableManager PrefetchHooks Function({bool contactId})>; typedef $$MessageActionsTableCreateCompanionBuilder = MessageActionsCompanion Function({ - Value id, required String messageId, required int contactId, required MessageActionType type, Value actionAt, + Value rowid, }); typedef $$MessageActionsTableUpdateCompanionBuilder = MessageActionsCompanion Function({ - Value id, Value messageId, Value contactId, Value type, Value actionAt, + Value rowid, }); final class $$MessageActionsTableReferences @@ -11265,9 +11256,6 @@ class $$MessageActionsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get contactId => $composableBuilder( column: $table.contactId, builder: (column) => ColumnFilters(column)); @@ -11309,9 +11297,6 @@ class $$MessageActionsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get contactId => $composableBuilder( column: $table.contactId, builder: (column) => ColumnOrderings(column)); @@ -11351,9 +11336,6 @@ class $$MessageActionsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get contactId => $composableBuilder(column: $table.contactId, builder: (column) => column); @@ -11407,32 +11389,32 @@ class $$MessageActionsTableTableManager extends RootTableManager< createComputedFieldComposer: () => $$MessageActionsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value id = const Value.absent(), Value messageId = const Value.absent(), Value contactId = const Value.absent(), Value type = const Value.absent(), Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), }) => MessageActionsCompanion( - id: id, messageId: messageId, contactId: contactId, type: type, actionAt: actionAt, + rowid: rowid, ), createCompanionCallback: ({ - Value id = const Value.absent(), required String messageId, required int contactId, required MessageActionType type, Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), }) => MessageActionsCompanion.insert( - id: id, messageId: messageId, contactId: contactId, type: type, actionAt: actionAt, + rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => ( diff --git a/lib/src/model/json/message_old.dart b/lib/src/model/json/message_old.dart deleted file mode 100644 index d783fc6..0000000 --- a/lib/src/model/json/message_old.dart +++ /dev/null @@ -1,331 +0,0 @@ -// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods - -import 'package:flutter/material.dart'; -import 'package:twonly/src/database/tables_old/messages_table.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Color getMessageColorFromType(MessageContent content, BuildContext context) { - Color color; - - if (content is TextMessageContent) { - color = Colors.blueAccent; - } else { - if (content is MediaMessageContent) { - if (content.isRealTwonly) { - color = context.color.primary; - } else { - if (content.isVideo) { - color = const Color.fromARGB(255, 243, 33, 208); - } else { - color = Colors.redAccent; - } - } - } else { - return (isDarkMode(context)) ? Colors.white : Colors.black; - } - } - return color; -} - -extension MessageKindExtension on MessageKind { - String get name => toString().split('.').last; - - static MessageKind fromString(String name) { - return MessageKind.values.firstWhere((e) => e.name == name); - } -} - -class MessageJson { - MessageJson({ - required this.kind, - required this.content, - required this.timestamp, - this.messageReceiverId, - this.messageSenderId, - this.retransId, - }); - final MessageKind kind; - final MessageContent? content; - final int? messageReceiverId; - final int? messageSenderId; - int? retransId; - DateTime timestamp; - - @override - String toString() { - return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; - } - - static MessageJson fromJson(Map json) { - final kind = MessageKindExtension.fromString(json['kind'] as String); - - return MessageJson( - kind: kind, - messageReceiverId: (json['messageReceiverId'] as num?)?.toInt(), - messageSenderId: (json['messageSenderId'] as num?)?.toInt(), - retransId: (json['retransId'] as num?)?.toInt(), - content: MessageContent.fromJson( - kind, - json['content'] as Map, - ), - timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int), - ); - } - - Map toJson() => { - 'kind': kind.name, - 'content': content?.toJson(), - 'messageReceiverId': messageReceiverId, - 'messageSenderId': messageSenderId, - 'retransId': retransId, - 'timestamp': timestamp.toUtc().millisecondsSinceEpoch, - }; -} - -class MessageContent { - MessageContent(); - - static MessageContent? fromJson(MessageKind kind, Map json) { - switch (kind) { - case MessageKind.media: - return MediaMessageContent.fromJson(json); - case MessageKind.textMessage: - return TextMessageContent.fromJson(json); - case MessageKind.profileChange: - return ProfileContent.fromJson(json); - case MessageKind.pushKey: - return PushKeyContent.fromJson(json); - case MessageKind.reopenedMedia: - return ReopenedMediaFileContent.fromJson(json); - case MessageKind.flameSync: - return FlameSyncContent.fromJson(json); - case MessageKind.ack: - return AckContent.fromJson(json); - case MessageKind.signalDecryptError: - return SignalDecryptErrorContent.fromJson(json); - case MessageKind.storedMediaFile: - case MessageKind.contactRequest: - case MessageKind.rejectRequest: - case MessageKind.acceptRequest: - case MessageKind.opened: - case MessageKind.requestPushKey: - case MessageKind.receiveMediaError: - } - return null; - } - - Map toJson() { - return {}; - } -} - -class MediaMessageContent extends MessageContent { - MediaMessageContent({ - required this.maxShowTime, - required this.isRealTwonly, - required this.isVideo, - required this.mirrorVideo, - this.downloadToken, - this.encryptionKey, - this.encryptionMac, - this.encryptionNonce, - }); - final int maxShowTime; - final bool isRealTwonly; - final bool isVideo; - final bool mirrorVideo; - final List? downloadToken; - final List? encryptionKey; - final List? encryptionMac; - final List? encryptionNonce; - - static MediaMessageContent fromJson(Map json) { - return MediaMessageContent( - downloadToken: json['downloadToken'] == null - ? null - : List.from(json['downloadToken'] as List), - encryptionKey: json['encryptionKey'] == null - ? null - : List.from(json['encryptionKey'] as List), - encryptionMac: json['encryptionMac'] == null - ? null - : List.from(json['encryptionMac'] as List), - encryptionNonce: json['encryptionNonce'] == null - ? null - : List.from(json['encryptionNonce'] as List), - maxShowTime: json['maxShowTime'] as int, - isRealTwonly: json['isRealTwonly'] as bool, - isVideo: json['isVideo'] as bool? ?? false, - mirrorVideo: json['mirrorVideo'] as bool? ?? false, - ); - } - - @override - Map toJson() { - return { - 'downloadToken': downloadToken, - 'encryptionKey': encryptionKey, - 'encryptionMac': encryptionMac, - 'encryptionNonce': encryptionNonce, - 'isRealTwonly': isRealTwonly, - 'maxShowTime': maxShowTime, - 'isVideo': isVideo, - 'mirrorVideo': mirrorVideo, - }; - } -} - -class TextMessageContent extends MessageContent { - TextMessageContent({ - required this.text, - this.responseToMessageId, - this.responseToOtherMessageId, - }); - String text; - int? responseToMessageId; - int? responseToOtherMessageId; - - static TextMessageContent fromJson(Map json) { - return TextMessageContent( - text: json['text'] as String, - responseToOtherMessageId: json.containsKey('responseToOtherMessageId') - ? json['responseToOtherMessageId'] as int? - : null, - responseToMessageId: json.containsKey('responseToMessageId') - ? json['responseToMessageId'] as int? - : null, - ); - } - - @override - Map toJson() { - return { - 'text': text, - 'responseToMessageId': responseToMessageId, - 'responseToOtherMessageId': responseToOtherMessageId, - }; - } -} - -class ReopenedMediaFileContent extends MessageContent { - ReopenedMediaFileContent({required this.messageId}); - int messageId; - - static ReopenedMediaFileContent fromJson(Map json) { - return ReopenedMediaFileContent(messageId: json['messageId'] as int); - } - - @override - Map toJson() { - return {'messageId': messageId}; - } -} - -class SignalDecryptErrorContent extends MessageContent { - SignalDecryptErrorContent({required this.encryptedHash}); - List encryptedHash; - - static SignalDecryptErrorContent fromJson(Map json) { - return SignalDecryptErrorContent( - encryptedHash: List.from(json['encryptedHash'] as List), - ); - } - - @override - Map toJson() { - return { - 'encryptedHash': encryptedHash, - }; - } -} - -class AckContent extends MessageContent { - AckContent({required this.messageIdToAck, required this.retransIdToAck}); - int? messageIdToAck; - int retransIdToAck; - - static AckContent fromJson(Map json) { - return AckContent( - messageIdToAck: json['messageIdToAck'] as int?, - retransIdToAck: json['retransIdToAck'] as int, - ); - } - - @override - Map toJson() { - return { - 'messageIdToAck': messageIdToAck, - 'retransIdToAck': retransIdToAck, - }; - } -} - -class ProfileContent extends MessageContent { - ProfileContent({required this.avatarSvg, required this.displayName}); - String avatarSvg; - String displayName; - - static ProfileContent fromJson(Map json) { - return ProfileContent( - avatarSvg: json['avatarSvg'] as String, - displayName: json['displayName'] as String, - ); - } - - @override - Map toJson() { - return {'avatarSvg': avatarSvg, 'displayName': displayName}; - } -} - -class PushKeyContent extends MessageContent { - PushKeyContent({required this.keyId, required this.key}); - int keyId; - List key; - - static PushKeyContent fromJson(Map json) { - return PushKeyContent( - keyId: json['keyId'] as int, - key: List.from(json['key'] as List), - ); - } - - @override - Map toJson() { - return { - 'keyId': keyId, - 'key': key, - }; - } -} - -class FlameSyncContent extends MessageContent { - FlameSyncContent({ - required this.flameCounter, - required this.bestFriend, - required this.lastFlameCounterChange, - }); - int flameCounter; - DateTime lastFlameCounterChange; - bool bestFriend; - - static FlameSyncContent fromJson(Map json) { - return FlameSyncContent( - flameCounter: json['flameCounter'] as int, - bestFriend: json['bestFriend'] as bool, - lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch( - json['lastFlameCounterChange'] as int, - ), - ); - } - - @override - Map toJson() { - return { - 'flameCounter': flameCounter, - 'bestFriend': bestFriend, - 'lastFlameCounterChange': - lastFlameCounterChange.toUtc().millisecondsSinceEpoch, - }; - } -} diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 987ff9f..00614b0 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -72,7 +72,7 @@ class UserData { List? tutorialDisplayed; - int? myBestFriendContactId; + String? myBestFriendGroupId; DateTime? signalLastSignedPreKeyUpdated; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index a73b3ec..dd475e8 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -48,7 +48,7 @@ UserData _$UserDataFromJson(Map json) => UserData( ..tutorialDisplayed = (json['tutorialDisplayed'] as List?) ?.map((e) => e as String) .toList() - ..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt() + ..myBestFriendGroupId = json['myBestFriendGroupId'] as String? ..signalLastSignedPreKeyUpdated = json['signalLastSignedPreKeyUpdated'] == null ? null @@ -97,7 +97,7 @@ Map _$UserDataToJson(UserData instance) => { 'lastPlanBallance': instance.lastPlanBallance, 'additionalUserInvites': instance.additionalUserInvites, 'tutorialDisplayed': instance.tutorialDisplayed, - 'myBestFriendContactId': instance.myBestFriendContactId, + 'myBestFriendGroupId': instance.myBestFriendGroupId, 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated?.toIso8601String(), 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 2facb77..a209170 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { factory EncryptedContent_MediaUpdate({ EncryptedContent_MediaUpdate_Type? type, - $core.String? targetMessageId, + $core.String? targetMediaId, }) { final $result = create(); if (type != null) { $result.type = type; } - if (targetMessageId != null) { - $result.targetMessageId = targetMessageId; + if (targetMediaId != null) { + $result.targetMediaId = targetMediaId; } return $result; } @@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values) - ..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') + ..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId') ..hasRequiredFields = false ; @@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { void clearType() => clearField(1); @$pb.TagNumber(2) - $core.String get targetMessageId => $_getSZ(1); + $core.String get targetMediaId => $_getSZ(1); @$pb.TagNumber(2) - set targetMessageId($core.String v) { $_setString(1, v); } + set targetMediaId($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) - $core.bool hasTargetMessageId() => $_has(1); + $core.bool hasTargetMediaId() => $_has(1); @$pb.TagNumber(2) - void clearTargetMessageId() => clearField(2); + void clearTargetMediaId() => clearField(2); } class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { @@ -1025,8 +1025,8 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { class EncryptedContent extends $pb.GeneratedMessage { factory EncryptedContent({ $core.String? groupId, + $core.bool? isDirectChat, $fixnum.Int64? senderProfileCounter, - EncryptedContent_TextMessage? textMessage, EncryptedContent_MessageUpdate? messageUpdate, EncryptedContent_Media? media, EncryptedContent_MediaUpdate? mediaUpdate, @@ -1035,17 +1035,18 @@ class EncryptedContent extends $pb.GeneratedMessage { EncryptedContent_FlameSync? flameSync, EncryptedContent_PushKeys? pushKeys, EncryptedContent_Reaction? reaction, + EncryptedContent_TextMessage? textMessage, }) { final $result = create(); if (groupId != null) { $result.groupId = groupId; } + if (isDirectChat != null) { + $result.isDirectChat = isDirectChat; + } if (senderProfileCounter != null) { $result.senderProfileCounter = senderProfileCounter; } - if (textMessage != null) { - $result.textMessage = textMessage; - } if (messageUpdate != null) { $result.messageUpdate = messageUpdate; } @@ -1070,6 +1071,9 @@ class EncryptedContent extends $pb.GeneratedMessage { if (reaction != null) { $result.reaction = reaction; } + if (textMessage != null) { + $result.textMessage = textMessage; + } return $result; } EncryptedContent._() : super(); @@ -1078,8 +1082,8 @@ class EncryptedContent extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create) ..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') - ..aInt64(3, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter') - ..aOM(4, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) + ..aOB(3, _omitFieldNames ? '' : 'isDirectChat', protoName: 'isDirectChat') + ..aInt64(4, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter') ..aOM(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create) ..aOM(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create) ..aOM(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.create) @@ -1088,6 +1092,7 @@ class EncryptedContent extends $pb.GeneratedMessage { ..aOM(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create) ..aOM(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create) ..aOM(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create) + ..aOM(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) ..hasRequiredFields = false ; @@ -1121,26 +1126,24 @@ class EncryptedContent extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearGroupId() => clearField(2); - /// / This can be added, so the receiver can check weather he is up to date with the current profile @$pb.TagNumber(3) - $fixnum.Int64 get senderProfileCounter => $_getI64(1); + $core.bool get isDirectChat => $_getBF(1); @$pb.TagNumber(3) - set senderProfileCounter($fixnum.Int64 v) { $_setInt64(1, v); } + set isDirectChat($core.bool v) { $_setBool(1, v); } @$pb.TagNumber(3) - $core.bool hasSenderProfileCounter() => $_has(1); + $core.bool hasIsDirectChat() => $_has(1); @$pb.TagNumber(3) - void clearSenderProfileCounter() => clearField(3); + void clearIsDirectChat() => clearField(3); + /// / This can be added, so the receiver can check weather he is up to date with the current profile @$pb.TagNumber(4) - EncryptedContent_TextMessage get textMessage => $_getN(2); + $fixnum.Int64 get senderProfileCounter => $_getI64(2); @$pb.TagNumber(4) - set textMessage(EncryptedContent_TextMessage v) { setField(4, v); } + set senderProfileCounter($fixnum.Int64 v) { $_setInt64(2, v); } @$pb.TagNumber(4) - $core.bool hasTextMessage() => $_has(2); + $core.bool hasSenderProfileCounter() => $_has(2); @$pb.TagNumber(4) - void clearTextMessage() => clearField(4); - @$pb.TagNumber(4) - EncryptedContent_TextMessage ensureTextMessage() => $_ensure(2); + void clearSenderProfileCounter() => clearField(4); @$pb.TagNumber(5) EncryptedContent_MessageUpdate get messageUpdate => $_getN(3); @@ -1229,6 +1232,17 @@ class EncryptedContent extends $pb.GeneratedMessage { void clearReaction() => clearField(12); @$pb.TagNumber(12) EncryptedContent_Reaction ensureReaction() => $_ensure(10); + + @$pb.TagNumber(13) + EncryptedContent_TextMessage get textMessage => $_getN(11); + @$pb.TagNumber(13) + set textMessage(EncryptedContent_TextMessage v) { setField(13, v); } + @$pb.TagNumber(13) + $core.bool hasTextMessage() => $_has(11); + @$pb.TagNumber(13) + void clearTextMessage() => clearField(13); + @$pb.TagNumber(13) + EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11); } diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index ef30eb5..f4c557c 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -95,8 +95,8 @@ const EncryptedContent$json = { '1': 'EncryptedContent', '2': [ {'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true}, - {'1': 'senderProfileCounter', '3': 3, '4': 1, '5': 3, '9': 1, '10': 'senderProfileCounter', '17': true}, - {'1': 'textMessage', '3': 4, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 2, '10': 'textMessage', '17': true}, + {'1': 'isDirectChat', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'isDirectChat', '17': true}, + {'1': 'senderProfileCounter', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'senderProfileCounter', '17': true}, {'1': 'messageUpdate', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', '9': 3, '10': 'messageUpdate', '17': true}, {'1': 'media', '3': 6, '4': 1, '5': 11, '6': '.EncryptedContent.Media', '9': 4, '10': 'media', '17': true}, {'1': 'mediaUpdate', '3': 7, '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', '9': 5, '10': 'mediaUpdate', '17': true}, @@ -105,12 +105,13 @@ const EncryptedContent$json = { {'1': 'flameSync', '3': 10, '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', '9': 8, '10': 'flameSync', '17': true}, {'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true}, {'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true}, + {'1': 'textMessage', '3': 13, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 11, '10': 'textMessage', '17': true}, ], '3': [EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], '8': [ {'1': '_groupId'}, + {'1': '_isDirectChat'}, {'1': '_senderProfileCounter'}, - {'1': '_textMessage'}, {'1': '_messageUpdate'}, {'1': '_media'}, {'1': '_mediaUpdate'}, @@ -119,6 +120,7 @@ const EncryptedContent$json = { {'1': '_flameSync'}, {'1': '_pushKeys'}, {'1': '_reaction'}, + {'1': '_textMessage'}, ], }; @@ -219,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = { '1': 'MediaUpdate', '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, - {'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'}, + {'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'}, ], '4': [EncryptedContent_MediaUpdate_Type$json], }; @@ -315,59 +317,60 @@ const EncryptedContent_FlameSync$json = { /// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( - 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARI3ChRzZW' - '5kZXJQcm9maWxlQ291bnRlchgDIAEoA0gBUhRzZW5kZXJQcm9maWxlQ291bnRlcogBARJECgt0' - 'ZXh0TWVzc2FnZRgEIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIAlILdGV4dE' - '1lc3NhZ2WIAQESSgoNbWVzc2FnZVVwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVz' - 'c2FnZVVwZGF0ZUgDUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdG' - 'VkQ29udGVudC5NZWRpYUgEUgVtZWRpYYgBARJECgttZWRpYVVwZGF0ZRgHIAEoCzIdLkVuY3J5' - 'cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIBVILbWVkaWFVcGRhdGWIAQESSgoNY29udGFjdFVwZG' - 'F0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgGUg1jb250YWN0VXBk' - 'YXRliAEBEk0KDmNvbnRhY3RSZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YW' - 'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy' - 'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA' - 'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u' - 'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG' - 'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' - 'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU' - '1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa' - 'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh' - 'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC' - 'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' - 'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh' - 'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW' - 'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0' - 'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA' - 'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4' - 'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA' - 'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw' - 'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2' - '5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp' - 'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA' - 'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93' - 'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ' - 'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp' - 'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB' - 'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs' - 'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX' - 'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh' - 'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG' - 'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw' - 'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2' - '50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx' - 'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0' - 'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50' - 'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3' - 'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH' - 'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg' - 'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl' - 'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ' - 'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ' - 'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV' - 'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291' - 'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA' - 'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO' - 'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG' - 'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL' - 'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u'); + 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0' + 'RpcmVjdENoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjcKFHNlbmRlclByb2ZpbGVDb3Vu' + 'dGVyGAQgASgDSAJSFHNlbmRlclByb2ZpbGVDb3VudGVyiAEBEkoKDW1lc3NhZ2VVcGRhdGUYBS' + 'ABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGVIA1INbWVzc2FnZVVwZGF0ZYgB' + 'ARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFIBFIFbWVkaWGIAQESRA' + 'oLbWVkaWFVcGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAVSC21l' + 'ZGlhVXBkYXRliAEBEkoKDWNvbnRhY3RVcGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk' + 'NvbnRhY3RVcGRhdGVIBlINY29udGFjdFVwZGF0ZYgBARJNCg5jb250YWN0UmVxdWVzdBgJIAEo' + 'CzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3RIB1IOY29udGFjdFJlcXVlc3SIAQ' + 'ESPgoJZmxhbWVTeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNICFIJZmxh' + 'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0' + 'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh' + 'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2' + '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z' + 'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX' + 'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ' + 'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA' + 'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI' + 'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3' + 'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu' + 'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3' + 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11' + 'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX' + 'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ' + 'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg' + '9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu' + 'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG' + 'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp' + 'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX' + 'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN' + 'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi' + 'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u' + 'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA' + 'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ' + 'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX' + 'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu' + 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR' + 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn' + 'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg' + 'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw' + 'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg' + 'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw' + 'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG' + 'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt' + 'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE' + 'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB' + 'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA' + 'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo' + 'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2' + 'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl' + 'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm' + 'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK' + 'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg' + '5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk' + 'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm' + 'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index d8dc45d..cd37d58 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -30,11 +30,11 @@ message PlaintextContent { message EncryptedContent { optional string groupId = 2; + optional bool isDirectChat = 3; /// This can be added, so the receiver can check weather he is up to date with the current profile - optional int64 senderProfileCounter = 3; + optional int64 senderProfileCounter = 4; - optional TextMessage textMessage = 4; optional MessageUpdate messageUpdate = 5; optional Media media = 6; optional MediaUpdate mediaUpdate = 7; @@ -43,6 +43,7 @@ message EncryptedContent { optional FlameSync flameSync = 10; optional PushKeys pushKeys = 11; optional Reaction reaction = 12; + optional TextMessage textMessage = 13; message TextMessage { string senderMessageId = 1; @@ -98,7 +99,7 @@ message EncryptedContent { DECRYPTION_ERROR = 2; } Type type = 1; - string targetMessageId = 2; + string targetMediaId = 2; } message ContactRequest { diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 4b1b738..8da0d48 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -207,7 +207,8 @@ Future requestMediaReupload(String mediaId) async { final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); if (messages.length != 1 || messages.first.senderId == null) { Log.error( - 'Media file has none or more than one sender. That is not possible'); + 'Media file has none or more than one sender. That is not possible', + ); return; } @@ -216,7 +217,7 @@ Future requestMediaReupload(String mediaId) async { EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMessageId: mediaId, + targetMediaId: mediaId, ), ), ); diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index de6c9f3..6c8c783 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -119,6 +119,15 @@ Future _createUploadRequest(MediaFileService media) async { for (final message in messages) { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(message.groupId); + + if (media.mediaFile.reuploadRequestedBy == null) { + await twonlyDB.groupsDao.incFlameCounter( + message.groupId, + false, + message.createdAt, + ); + } + for (final groupMember in groupMembers) { /// only send the upload to the users if (media.mediaFile.reuploadRequestedBy != null) { @@ -128,12 +137,6 @@ Future _createUploadRequest(MediaFileService media) async { } } - await twonlyDB.contactsDao.incFlameCounter( - groupMember.contactId, - false, - message.createdAt, - ); - final downloadToken = getRandomUint8List(32); var type = EncryptedContent_Media_Type.IMAGE; @@ -169,7 +172,8 @@ Future _createUploadRequest(MediaFileService media) async { if (cipherText == null) { Log.error( - 'Could not generate ciphertext message for ${groupMember.contactId}'); + 'Could not generate ciphertext message for ${groupMember.contactId}', + ); } final messageOnSuccess = TextMessage() diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index eaab030..1298115 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -161,11 +161,13 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Future insertAndSendTextMessage( String groupId, String textMessage, + String? quotesMessageId, ) async { final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( groupId: Value(groupId), content: Value(textMessage), + quotesMessageId: Value(quotesMessageId), ), ); if (message == null) { @@ -173,19 +175,40 @@ Future insertAndSendTextMessage( return; } + final encryptedContent = pb.EncryptedContent( + textMessage: pb.EncryptedContent_TextMessage( + senderMessageId: message.messageId, + text: textMessage, + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + ), + ); + + if (quotesMessageId != null) { + encryptedContent.textMessage.quoteMessageId = quotesMessageId; + } + + await sendCipherTextToGroup(groupId, encryptedContent); +} + +Future sendCipherTextToGroup( + String groupId, + pb.EncryptedContent encryptedContent, +) async { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) return; + + encryptedContent + ..groupId = groupId + ..isDirectChat = group.isDirectChat; for (final groupMember in groupMembers) { - unawaited(sendCipherText( - groupMember.contactId, - pb.EncryptedContent( - textMessage: pb.EncryptedContent_TextMessage( - senderMessageId: message.messageId, - text: textMessage, - timestamp: Int64(message.createdAt.millisecondsSinceEpoch), - ), + unawaited( + sendCipherText( + groupMember.contactId, + encryptedContent, ), - )); + ); } } diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index d305800..2b3519e 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -82,24 +82,22 @@ Future handleFlameSync( EncryptedContent_FlameSync flameSync, ) async { Log.info('Got a flameSync from $contactId'); - final contact = await twonlyDB.contactsDao - .getContactByUserId(contactId) - .getSingleOrNull(); - if (contact == null || contact.lastFlameCounterChange != null) return; + final group = await twonlyDB.groupsDao.getDirectChat(contactId); + if (group == null || group.lastFlameCounterChange != null) return; - var updates = ContactsCompanion( + var updates = GroupsCompanion( alsoBestFriend: Value(flameSync.bestFriend), ); - if (isToday(contact.lastFlameCounterChange!) && + if (isToday(group.lastFlameCounterChange!) && isToday(fromTimestamp(flameSync.lastFlameCounterChange))) { - if (flameSync.flameCounter > contact.flameCounter) { - updates = ContactsCompanion( + if (flameSync.flameCounter > group.flameCounter) { + updates = GroupsCompanion( flameCounter: Value(flameSync.flameCounter.toInt()), ); } } - await twonlyDB.contactsDao.updateContact(contactId, updates); + await twonlyDB.groupsDao.updateGroup(group.groupId, updates); } Future checkForProfileUpdate( diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 0b03728..b822fb4 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -100,8 +100,8 @@ Future handleMedia( ); if (message != null) { Log.info('Inserted a new media message with ID: ${message.messageId}'); - await twonlyDB.contactsDao.incFlameCounter( - fromUserId, + await twonlyDB.groupsDao.incFlameCounter( + message.groupId, true, fromTimestamp(media.timestamp), ); @@ -115,15 +115,17 @@ Future handleMediaUpdate( String groupId, EncryptedContent_MediaUpdate mediaUpdate, ) async { - final message = await twonlyDB.messagesDao - .getMessageById(mediaUpdate.targetMessageId) - .getSingleOrNull(); - if (message == null || message.mediaId == null) return; + final messages = await twonlyDB.messagesDao + .getMessagesByMediaId(mediaUpdate.targetMediaId); + if (messages.length != 1) return; + final message = messages.first; + if (message.senderId != fromUserId) return; final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); if (mediaFile == null) { Log.info( - 'Got media file update, but media file was not found ${message.mediaId}'); + 'Got media file update, but media file was not found ${message.mediaId}', + ); return; } diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/server_messages/messages.server_messages.dart index 6c7347a..b0b2457 100644 --- a/lib/src/services/api/server_messages/messages.server_messages.dart +++ b/lib/src/services/api/server_messages/messages.server_messages.dart @@ -10,7 +10,8 @@ Future handleMessageUpdate( switch (messageUpdate.type) { case EncryptedContent_MessageUpdate_Type.OPENED: Log.info( - 'Opened message ${messageUpdate.multipleSenderMessageIds.length}'); + 'Opened message ${messageUpdate.multipleSenderMessageIds.length}', + ); for (final senderMessageId in messageUpdate.multipleSenderMessageIds) { await twonlyDB.messagesDao.handleMessageOpened( contactId, diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 1bc0674..e0a101e 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -88,7 +88,7 @@ Future handleMediaError(MediaFile media) async { EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMessageId: message.messageId, + targetMediaId: message.mediaId, ), ), ); diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 1f98386..c28d59e 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; @@ -10,49 +10,52 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future syncFlameCounters() async { - final user = await getUser(); - if (user == null) return; - - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); - if (contacts.isEmpty) return; - final maxMessageCounter = contacts.map((x) => x.totalMediaCounter).max; + final groups = await twonlyDB.groupsDao.getAllDirectChats(); + if (groups.isEmpty) return; + final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; final bestFriend = - contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); + groups.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); - if (user.myBestFriendContactId != bestFriend.userId) { + if (gUser.myBestFriendGroupId != bestFriend.groupId) { await updateUserdata((user) { - user.myBestFriendContactId = bestFriend.userId; + user.myBestFriendGroupId = bestFriend.groupId; return user; }); } - for (final contact in contacts) { - if (contact.lastFlameCounterChange == null || contact.deleted) continue; - if (!isToday(contact.lastFlameCounterChange!)) continue; - if (contact.lastFlameSync != null) { - if (isToday(contact.lastFlameSync!)) continue; + for (final group in groups) { + if (group.lastFlameCounterChange == null) continue; + if (!isToday(group.lastFlameCounterChange!)) continue; + if (group.lastFlameSync != null) { + if (isToday(group.lastFlameSync!)) continue; } - final flameCounter = getFlameCounterFromContact(contact) - 1; + final flameCounter = getFlameCounterFromGroup(group) - 1; - // only sync when flame counter is higher than three days - if (flameCounter < 1 && bestFriend.userId != contact.userId) continue; + // only sync when flame counter is higher than three days or when they are bestFriends + if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue; + + final groupMembers = + await twonlyDB.groupsDao.getGroupMembers(group.groupId); + if (groupMembers.length != 1) { + continue; // flame sync is only done for groups of two + } await sendCipherText( - contact.userId, + groupMembers.first.contactId, EncryptedContent( flameSync: EncryptedContent_FlameSync( flameCounter: Int64(flameCounter), lastFlameCounterChange: - Int64(contact.lastFlameCounterChange!.millisecondsSinceEpoch), - bestFriend: contact.userId == bestFriend.userId, + Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), + bestFriend: group.groupId == bestFriend.groupId, ), ), ); - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( lastFlameSync: Value(DateTime.now()), ), ); diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 2a062d2..73d5a78 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -119,7 +119,7 @@ class MediaFileService { originalPath, storedPath, thumbnailPath, - uploadRequestPath + uploadRequestPath, ]; for (final path in pathsToRemove) { @@ -146,11 +146,13 @@ class MediaFileService { String namePrefix = '', String extensionParam = '', }) { - final mediaBaseDir = Directory(join( - applicationSupportDirectory.path, - 'mediafiles', - directory, - )); + final mediaBaseDir = Directory( + join( + applicationSupportDirectory.path, + 'mediafiles', + directory, + ), + ); if (!mediaBaseDir.existsSync()) { mediaBaseDir.createSync(recursive: true); } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index e217869..a1fa18a 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; @@ -8,11 +7,14 @@ import 'package:intl/intl.dart'; import 'package:local_auth/local_auth.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:provider/provider.dart'; +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/localization/generated/app_localizations.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; @@ -284,3 +286,28 @@ PieTheme getPieCanvasTheme(BuildContext context) { ), ); } + +Color getMessageColorFromType( + Message message, + MediaFile? mediaFile, + BuildContext context, +) { + Color color; + + if (message.type == MessageType.text) { + color = Colors.blueAccent; + } else if (mediaFile != null) { + if (mediaFile.requiresAuthentication) { + color = context.color.primary; + } else { + if (mediaFile.type == MediaType.video) { + color = const Color.fromARGB(255, 243, 33, 208); + } else { + color = Colors.redAccent; + } + } + } else { + return (isDarkMode(context)) ? Colors.white : Colors.black; + } + return color; +} diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart index f63cba5..491abf9 100644 --- a/lib/src/views/camera/share_image_components/best_friends_selector.dart +++ b/lib/src/views/camera/share_image_components/best_friends_selector.dart @@ -1,37 +1,31 @@ -// ignore_for_file: strict_raw_type - import 'dart:collection'; - import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; class BestFriendsSelector extends StatelessWidget { const BestFriendsSelector({ - required this.users, - required this.isRealTwonly, - required this.updateStatus, - required this.selectedUserIds, + required this.groups, + required this.selectedGroupIds, + required this.updateSelectedGroupIds, required this.title, + required this.showSelectAll, super.key, }); - final List users; - final void Function(int, bool) updateStatus; - final HashSet selectedUserIds; - final bool isRealTwonly; + final List groups; + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; final String title; + final bool showSelectAll; @override Widget build(BuildContext context) { - if (users.isEmpty) { + if (groups.isEmpty) { return Container(); } - return Column( children: [ Row( @@ -39,11 +33,11 @@ class BestFriendsSelector extends StatelessWidget { Expanded( child: HeadLineComponent(title), ), - if (!isRealTwonly) + if (showSelectAll) GestureDetector( onTap: () { - for (final user in users) { - updateStatus(user.userId, true); + for (final group in groups) { + updateSelectedGroupIds(group.groupId, true); } }, child: Container( @@ -70,7 +64,7 @@ class BestFriendsSelector extends StatelessWidget { Column( spacing: 8, children: List.generate( - (users.length + 1) ~/ 2, + (groups.length + 1) ~/ 2, (rowIndex) { final firstUserIndex = rowIndex * 2; final secondUserIndex = firstUserIndex + 1; @@ -79,21 +73,19 @@ class BestFriendsSelector extends StatelessWidget { children: [ Expanded( child: UserCheckbox( - isChecked: selectedUserIds - .contains(users[firstUserIndex].userId), - user: users[firstUserIndex], - onChanged: updateStatus, - isRealTwonly: isRealTwonly, + isChecked: selectedGroupIds + .contains(groups[firstUserIndex].groupId), + group: groups[firstUserIndex], + onChanged: updateSelectedGroupIds, ), ), - if (secondUserIndex < users.length) + if (secondUserIndex < groups.length) Expanded( child: UserCheckbox( - isChecked: selectedUserIds - .contains(users[secondUserIndex].userId), - user: users[secondUserIndex], - onChanged: updateStatus, - isRealTwonly: isRealTwonly, + isChecked: selectedGroupIds + .contains(groups[secondUserIndex].groupId), + group: groups[secondUserIndex], + onChanged: updateSelectedGroupIds, ), ) else @@ -112,28 +104,24 @@ class BestFriendsSelector extends StatelessWidget { class UserCheckbox extends StatelessWidget { const UserCheckbox({ - required this.user, + required this.group, required this.onChanged, - required this.isRealTwonly, required this.isChecked, super.key, }); - final Contact user; - final void Function(int, bool) onChanged; + final Group group; + final void Function(String, bool) onChanged; final bool isChecked; - final bool isRealTwonly; @override Widget build(BuildContext context) { - final displayName = getContactDisplayName(user); - return Container( padding: const EdgeInsets.symmetric( horizontal: 3, ), // Padding inside the container child: GestureDetector( onTap: () { - onChanged(user.userId, !isChecked); + onChanged(group.groupId, !isChecked); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), @@ -149,8 +137,8 @@ class UserCheckbox extends StatelessWidget { ), child: Row( children: [ - ContactAvatar( - contact: user, + AvatarIcon( + group: group, fontSize: 12, ), const SizedBox(width: 8), @@ -160,28 +148,21 @@ class UserCheckbox extends StatelessWidget { Row( children: [ Text( - displayName.length > 8 - ? '${displayName.substring(0, 8)}...' - : displayName, + group.groupName.length > 12 + ? '${group.groupName.substring(0, 9)}...' + : group.groupName, overflow: TextOverflow.ellipsis, ), ], ), - StreamBuilder( - stream: twonlyDB.contactsDao.watchFlameCounter(user.userId), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data! == 0) { - return Container(); - } - return FlameCounterWidget(user, snapshot.data!); - }, - ), + FlameCounterWidget(groupId: group.groupId), ], ), Expanded(child: Container()), Checkbox( value: isChecked, side: WidgetStateBorderSide.resolveWith( + // ignore: strict_raw_type (Set states) { if (states.contains(WidgetState.selected)) { return const BorderSide(width: 0); @@ -192,7 +173,7 @@ class UserCheckbox extends StatelessWidget { }, ), onChanged: (bool? value) { - onChanged(user.userId, value ?? false); + onChanged(group.groupId, value ?? false); }, ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index ba13f90..e5d787a 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -186,7 +186,8 @@ class _ShareImageEditorView extends State { onPressed: () async { if (media.type != MediaType.video) { await mediaService.setDisplayLimit( - (media.displayLimitInMilliseconds == null) ? 0 : null); + (media.displayLimitInMilliseconds == null) ? 0 : null, + ); if (!mounted) return; setState(() {}); return; @@ -465,8 +466,9 @@ class _ShareImageEditorView extends State { : const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { if (sendingOrLoadingImage) return; - if (widget.sendToGroup == null) + if (widget.sendToGroup == null) { return pushShareImageView(); + } await sendImageToSinglePerson(); }, style: ButtonStyle( diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 13629ef..92cb735 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -2,23 +2,19 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/verified_shield.dart'; -import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ @@ -38,28 +34,27 @@ class ShareImageView extends StatefulWidget { } class _ShareImageView extends State { - List contacts = []; - List _otherUsers = []; - List _bestFriends = []; - List _pinnedContacts = []; - Uint8List? imageBytes; + List contacts = []; + List _otherUsers = []; + List _bestFriends = []; + List _pinnedContacts = []; + bool sendingImage = false; + bool mediaStoreFutureReady = false; bool hideArchivedUsers = true; final TextEditingController searchUserName = TextEditingController(); - late StreamSubscription> contactSub; + late StreamSubscription> allGroupSub; String lastQuery = ''; @override void initState() { super.initState(); - final allContacts = twonlyDB.contactsDao.watchContactsForShareView(); - - contactSub = allContacts.listen((allContacts) async { + allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async { setState(() { - contacts = allContacts; + contacts = allGroups; }); - await updateUsers(allContacts.where((x) => !x.archived).toList()); + await updateGroups(allGroups.where((x) => !x.archived).toList()); }); unawaited(initAsync()); @@ -69,6 +64,7 @@ class _ShareImageView extends State { if (widget.mediaStoreFuture != null) { await widget.mediaStoreFuture; } + mediaStoreFutureReady = true; await widget.mediaFileService.setUploadState(UploadState.preprocessing); unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; @@ -77,16 +73,17 @@ class _ShareImageView extends State { @override void dispose() { - unawaited(contactSub.cancel()); + unawaited(allGroupSub.cancel()); super.dispose(); } - Future updateUsers(List users) async { + Future updateGroups(List groups) async { // Sort contacts by flameCounter and then by totalMediaCounter - users.sort((a, b) { + groups.sort((a, b) { // First, compare by flameCounter - final flameComparison = getFlameCounterFromContact(b) - .compareTo(getFlameCounterFromContact(a)); + + final flameComparison = + getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a)); if (flameComparison != 0) { return flameComparison; // Sort by flameCounter in descending order } @@ -97,18 +94,18 @@ class _ShareImageView extends State { }); // Separate best friends and other users - final bestFriends = []; - final otherUsers = []; - final pinnedContacts = users.where((c) => c.pinned).toList(); + final bestFriends = []; + final otherUsers = []; + final pinnedContacts = groups.where((c) => c.pinned).toList(); - for (final contact in users) { - if (contact.pinned) continue; - if (!contact.archived && - (getFlameCounterFromContact(contact)) > 0 && + for (final group in groups) { + if (group.pinned) continue; + if (!group.archived && + (getFlameCounterFromGroup(group)) > 0 && bestFriends.length < 6) { - bestFriends.add(contact); + bestFriends.add(group); } else { - otherUsers.add(contact); + otherUsers.add(group); } } @@ -122,13 +119,13 @@ class _ShareImageView extends State { Future _filterUsers(String query) async { lastQuery = query; if (query.isEmpty) { - await updateUsers( + await updateGroups( contacts .where( (x) => !x.archived || !hideArchivedUsers || - widget.selectedUserIds.contains(x.userId), + widget.selectedGroupIds.contains(x.groupId), ) .toList(), ); @@ -136,16 +133,14 @@ class _ShareImageView extends State { } final usersFiltered = contacts .where( - (user) => getContactDisplayName(user) - .toLowerCase() - .contains(query.toLowerCase()), + (user) => user.groupName.toLowerCase().contains(query.toLowerCase()), ) .toList(); - await updateUsers(usersFiltered); + await updateGroups(usersFiltered); } - void updateStatus(int userId, bool checked) { - widget.updateStatus(userId, checked); + void updateSelectedGroupIds(String groupId, bool checked) { + widget.updateSelectedGroupIds(groupId, checked); setState(() {}); } @@ -173,19 +168,21 @@ class _ShareImageView extends State { ), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), BestFriendsSelector( - users: _pinnedContacts, - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + groups: _pinnedContacts, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, title: context.lang.shareImagePinnedContacts, + showSelectAll: + !widget.mediaFileService.mediaFile.requiresAuthentication, ), const SizedBox(height: 10), BestFriendsSelector( - users: _bestFriends, - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + groups: _bestFriends, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, title: context.lang.shareImageBestFriends, + showSelectAll: + !widget.mediaFileService.mediaFile.requiresAuthentication, ), const SizedBox(height: 10), if (_otherUsers.isNotEmpty) @@ -229,9 +226,8 @@ class _ShareImageView extends State { Expanded( child: UserList( List.from(_otherUsers), - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, ), ), ], @@ -246,7 +242,7 @@ class _ShareImageView extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton.icon( - icon: imageBytes == null || sendingImage + icon: !mediaStoreFutureReady || sendingImage ? SizedBox( height: 12, width: 12, @@ -257,50 +253,28 @@ class _ShareImageView extends State { ) : const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { - if (imageBytes == null || widget.selectedUserIds.isEmpty) { + if (!mediaStoreFutureReady || + widget.selectedGroupIds.isEmpty) { return; } - final err = await isAllowedToSend(); - if (!context.mounted) return; + setState(() { + sendingImage = true; + }); - if (err != null) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SubscriptionView( - redirectError: err, - ); - }, - ), - ); - } else { - setState(() { - sendingImage = true; - }); + await insertMediaFileInMessagesTable( + widget.mediaFileService, + widget.selectedGroupIds.toList(), + ); - await finalizeUpload( - widget.mediaUploadId, - widget.selectedUserIds.toList(), - widget.isRealTwonly, - widget.videoUploadHandler != null, - widget.mirrorVideo, - widget.maxShowTime, - ); - - /// trigger the upload of the media file. - unawaited(handleNextMediaUploadSteps(widget.mediaUploadId)); - - if (context.mounted) { - Navigator.pop(context, true); - // if (widget.preselectedUser != null) { - // Navigator.pop(context, true); - // } else { - // Navigator.popUntil(context, (route) => route.isFirst, true); - // globalUpdateOfHomeViewPageIndex(1); - // } - } + if (context.mounted) { + Navigator.pop(context, true); + // if (widget.preselectedUser != null) { + // Navigator.pop(context, true); + // } else { + // Navigator.popUntil(context, (route) => route.isFirst, true); + // globalUpdateOfHomeViewPageIndex(1); + // } } }, style: ButtonStyle( @@ -308,7 +282,7 @@ class _ShareImageView extends State { const EdgeInsets.symmetric(vertical: 10, horizontal: 30), ), backgroundColor: WidgetStateProperty.all( - imageBytes == null || widget.selectedUserIds.isEmpty + mediaStoreFutureReady || widget.selectedGroupIds.isEmpty ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.primary, ), @@ -328,52 +302,42 @@ class _ShareImageView extends State { class UserList extends StatelessWidget { const UserList( - this.users, { - required this.selectedUserIds, - required this.updateStatus, - required this.isRealTwonly, + this.groups, { + required this.selectedGroupIds, + required this.updateSelectedGroupIds, super.key, }); - final void Function(int, bool) updateStatus; - final List users; - final bool isRealTwonly; - final HashSet selectedUserIds; + final void Function(String, bool) updateSelectedGroupIds; + final List groups; + final HashSet selectedGroupIds; @override Widget build(BuildContext context) { // Step 1: Sort the users alphabetically - users + groups .sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); return ListView.builder( restorationId: 'new_message_users_list', - itemCount: users.length, + itemCount: groups.length, itemBuilder: (BuildContext context, int i) { - final user = users[i]; - final flameCounter = getFlameCounterFromContact(user); + final group = groups[i]; return ListTile( title: Row( children: [ - if (isRealTwonly) - Padding( - padding: const EdgeInsets.only(right: 1), - child: VerifiedShield(user), - ), - Text(getContactDisplayName(user)), - if (flameCounter >= 1) - FlameCounterWidget( - user, - flameCounter, - prefix: true, - ), + Text(group.groupName), + FlameCounterWidget( + groupId: group.groupId, + prefix: true, + ), ], ), - leading: ContactAvatar( - contact: user, + leading: AvatarIcon( + group: group, fontSize: 15, ), trailing: Checkbox( - value: selectedUserIds.contains(user.userId), + value: selectedGroupIds.contains(group.groupId), side: WidgetStateBorderSide.resolveWith( (Set states) { if (states.contains(WidgetState.selected)) { @@ -384,11 +348,14 @@ class UserList extends StatelessWidget { ), onChanged: (bool? value) { if (value == null) return; - updateStatus(user.userId, value); + updateSelectedGroupIds(group.groupId, value); }, ), onTap: () { - updateStatus(user.userId, !selectedUserIds.contains(user.userId)); + updateSelectedGroupIds( + group.groupId, + !selectedGroupIds.contains(group.groupId), + ); }, ); }, diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index 64e5089..dedf439 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -1,15 +1,12 @@ import 'dart:async'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; @@ -17,8 +14,8 @@ import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; class AddNewUserView extends StatefulWidget { const AddNewUserView({super.key}); @@ -97,19 +94,18 @@ class _SearchUsernameView extends State { if (added > 0) { if (await createNewSignalSession(userdata)) { - // before notifying the other party, add + // 1. Setup notifications keys with the other user await setupNotificationWithUsers( forceContact: userdata.userId.toInt(), ); - await encryptAndSendMessageAsync( - null, + // 2. Then send user request + await sendCipherText( userdata.userId.toInt(), - MessageJson( - kind: MessageKind.contactRequest, - timestamp: DateTime.now(), - content: MessageContent(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), ), - pushNotification: PushNotification(kind: PushKind.contactRequest), ); } } @@ -198,7 +194,7 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15), onPressed: () async { - const update = ContactsCompanion(archived: Value(true)); + const update = ContactsCompanion(requested: Value(false)); await twonlyDB.contactsDao.updateContact(contact.userId, update); }, ), @@ -234,17 +230,18 @@ class ContactsListView extends StatelessWidget { IconButton( icon: const Icon(Icons.check, color: Colors.green), onPressed: () async { - const update = ContactsCompanion(accepted: Value(true)); + const update = ContactsCompanion( + accepted: Value(true), + requested: Value(false), + ); await twonlyDB.contactsDao.updateContact(contact.userId, update); - await encryptAndSendMessageAsync( - null, + await sendCipherText( contact.userId, - MessageJson( - kind: MessageKind.acceptRequest, - timestamp: DateTime.now(), - content: MessageContent(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.ACCEPT, + ), ), - pushNotification: PushNotification(kind: PushKind.acceptRequest), ); await notifyContactsAboutProfileChange(); }, @@ -261,7 +258,7 @@ class ContactsListView extends StatelessWidget { final displayName = getContactDisplayName(contact); return ListTile( title: Text(displayName), - leading: ContactAvatar(contact: contact), + leading: AvatarIcon(contact: contact), trailing: Row( mainAxisSize: MainAxisSize.min, children: contact.requested diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 302ad66..adde0ea 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -7,27 +6,18 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; -import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; -import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; -import 'package:twonly/src/views/chats/media_viewer.view.dart'; +import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/chats/start_new_chat.view.dart'; -import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; @@ -41,10 +31,9 @@ class ChatListView extends StatefulWidget { } class _ChatListViewState extends State { - late StreamSubscription> _contactsSub; - List _contacts = []; - List _pinnedContacts = []; - UserData? _user; + late StreamSubscription> _contactsSub; + List _groupsNotPinned = []; + List _groupsPinned = []; GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey(); @@ -58,11 +47,11 @@ class _ChatListViewState extends State { } Future initAsync() async { - final stream = twonlyDB.contactsDao.watchContactsForChatList(); - _contactsSub = stream.listen((contacts) { + final stream = twonlyDB.groupsDao.watchGroups(); + _contactsSub = stream.listen((groups) { setState(() { - _contacts = contacts.where((x) => !x.pinned).toList(); - _pinnedContacts = contacts.where((x) => x.pinned).toList(); + _groupsNotPinned = groups.where((x) => !x.pinned).toList(); + _groupsPinned = groups.where((x) => x.pinned).toList(); }); }); @@ -71,21 +60,16 @@ class _ChatListViewState extends State { if (!mounted) return; await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); if (!mounted) return; - if (_contacts.isNotEmpty) { + if (_groupsNotPinned.isNotEmpty) { await showChatListTutorialContextMenu(context, firstUserListItemKey); } }); - final user = await getUser(); - if (user == null) return; - setState(() { - _user = user; - }); final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLogHash = (await compute(Sha256().hash, changeLog.codeUnits)).bytes; - if (!user.hideChangeLog && - user.lastChangeLogHash.toString() != changeLogHash.toString()) { + if (!gUser.hideChangeLog && + gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { await updateUserdata((u) { u.lastChangeLogHash = changeLogHash; return u; @@ -93,7 +77,7 @@ class _ChatListViewState extends State { if (!mounted) return; // only show changelog to people who already have contacts // this prevents that this is shown directly after the user registered - if (_contacts.isNotEmpty) { + if (_groupsNotPinned.isNotEmpty) { await Navigator.push( context, MaterialPageRoute( @@ -133,12 +117,11 @@ class _ChatListViewState extends State { }, ), ); - _user = await getUser(); if (!mounted) return; - setState(() {}); + setState(() {}); // gUser has updated }, - child: ContactAvatar( - userData: _user, + child: AvatarIcon( + userData: gUser, fontSize: 14, color: context.color.onSurface.withAlpha(20), ), @@ -210,9 +193,8 @@ class _ChatListViewState extends State { builder: (context) => const SettingsMainView(), ), ); - _user = await getUser(); if (!mounted) return; - setState(() {}); + setState(() {}); // gUser may has changed... }, icon: const FaIcon(FontAwesomeIcons.gear, size: 19), ), @@ -227,7 +209,7 @@ class _ChatListViewState extends State { child: isConnected ? Container() : const ConnectionInfo(), ), Positioned.fill( - child: (_contacts.isEmpty && _pinnedContacts.isEmpty) + child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) ? Center( child: Padding( padding: const EdgeInsets.all(10), @@ -252,9 +234,9 @@ class _ChatListViewState extends State { await Future.delayed(const Duration(seconds: 1)); }, child: ListView.builder( - itemCount: _pinnedContacts.length + - (_pinnedContacts.isNotEmpty ? 1 : 0) + - _contacts.length + + itemCount: _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + + _groupsNotPinned.length + 1, itemBuilder: (context, index) { if (index == 0) { @@ -262,11 +244,11 @@ class _ChatListViewState extends State { } index -= 1; // Check if the index is for the pinned users - if (index < _pinnedContacts.length) { - final contact = _pinnedContacts[index]; - return UserListItem( - key: ValueKey(contact.userId), - user: contact, + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItem( + key: ValueKey(group.groupId), + group: group, firstUserListItemKey: (index == 0 || index == 1) ? firstUserListItemKey : null, @@ -274,21 +256,21 @@ class _ChatListViewState extends State { } // If there are pinned users, account for the Divider - var adjustedIndex = index - _pinnedContacts.length; - if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) { + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { return const Divider(); } // Adjust the index for the contacts list - adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0); + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); // Get the contacts that are not pinned - final contact = _contacts.elementAt( + final group = _groupsNotPinned.elementAt( adjustedIndex, ); - return UserListItem( - key: ValueKey(contact.userId), - user: contact, + return GroupListItem( + key: ValueKey(group.groupId), + group: group, firstUserListItemKey: (index == 0) ? firstUserListItemKey : null, ); @@ -317,219 +299,3 @@ class _ChatListViewState extends State { ); } } - -class UserListItem extends StatefulWidget { - const UserListItem({ - required this.user, - required this.firstUserListItemKey, - super.key, - }); - final Contact user; - final GlobalKey? firstUserListItemKey; - - @override - State createState() => _UserListItem(); -} - -class _UserListItem extends State { - MessageSendState state = MessageSendState.send; - Message? currentMessage; - - List messagesNotOpened = []; - late StreamSubscription> messagesNotOpenedStream; - - List lastMessages = []; - late StreamSubscription> lastMessageStream; - - List previewMessages = []; - bool hasNonOpenedMediaFile = false; - - @override - void initState() { - super.initState(); - initStreams(); - } - - @override - void dispose() { - messagesNotOpenedStream.cancel(); - lastMessageStream.cancel(); - super.dispose(); - } - - void initStreams() { - lastMessageStream = twonlyDB.messagesDao - .watchLastMessage(widget.user.userId) - .listen((update) { - updateState(update, messagesNotOpened); - }); - - messagesNotOpenedStream = twonlyDB.messagesDao - .watchMessageNotOpened(widget.user.userId) - .listen((update) { - updateState(lastMessages, update); - }); - } - - void updateState( - List newLastMessages, - List newMessagesNotOpened, - ) { - if (newLastMessages.isEmpty) { - // there are no messages at all - currentMessage = null; - previewMessages = []; - } else if (newMessagesNotOpened.isEmpty) { - // there are no not opened messages show just the last message in the table - currentMessage = newLastMessages.last; - previewMessages = newLastMessages; - } else { - // filter first for received messages - final receivedMessages = - newMessagesNotOpened.where((x) => x.messageOtherId != null).toList(); - - if (receivedMessages.isNotEmpty) { - previewMessages = receivedMessages; - currentMessage = receivedMessages.first; - } else { - previewMessages = newMessagesNotOpened; - currentMessage = newMessagesNotOpened.first; - } - } - - final msgs = - previewMessages.where((x) => x.kind == MessageKind.media).toList(); - if (msgs.isNotEmpty && - msgs.first.kind == MessageKind.media && - msgs.first.messageOtherId != null && - msgs.first.openedAt == null) { - hasNonOpenedMediaFile = true; - } else { - hasNonOpenedMediaFile = false; - } - - lastMessages = newLastMessages; - messagesNotOpened = newMessagesNotOpened; - setState(() { - // sets lastMessages, messagesNotOpened and currentMessage - }); - } - - Future onTap() async { - if (currentMessage == null) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.user); - }, - ), - ); - return; - } - - if (hasNonOpenedMediaFile) { - final msgs = - previewMessages.where((x) => x.kind == MessageKind.media).toList(); - switch (msgs.first.downloadState) { - case DownloadState.pending: - await startDownloadMedia(msgs.first, true); - return; - case DownloadState.downloaded: - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return MediaViewerView(widget.user); - }, - ), - ); - return; - case DownloadState.downloading: - return; - } - } - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(widget.user); - }, - ), - ); - } - - @override - Widget build(BuildContext context) { - final flameCounter = getFlameCounterFromContact(widget.user); - - return Stack( - children: [ - Positioned( - top: 0, - bottom: 0, - left: 50, - child: SizedBox( - key: widget.firstUserListItemKey, - height: 20, - width: 20, - ), - ), - UserContextMenu( - contact: widget.user, - child: ListTile( - title: Text( - getContactDisplayName(widget.user), - ), - subtitle: (widget.user.deleted) - ? Text(context.lang.userDeletedAccount) - : (currentMessage == null) - ? Text(context.lang.chatsTapToSend) - : Row( - children: [ - MessageSendStateIcon(previewMessages), - const Text('•'), - const SizedBox(width: 5), - if (currentMessage != null) - LastMessageTime(message: currentMessage!), - if (flameCounter > 0) - FlameCounterWidget( - widget.user, - flameCounter, - prefix: true, - ), - ], - ), - leading: ContactAvatar(contact: widget.user), - trailing: (widget.user.deleted) - ? null - : IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - if (hasNonOpenedMediaFile) { - return ChatMessagesView(widget.user); - } else { - return CameraSendToView(widget.user); - } - }, - ), - ); - }, - icon: FaIcon( - hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), - ), - ), - onTap: onTap, - ), - ), - ], - ); - } -} diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart new file mode 100644 index 0000000..dbb2fa4 --- /dev/null +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +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/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_send_to_view.dart'; +import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; +import 'package:twonly/src/views/chats/media_viewer.view.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/group_context_menu.component.dart'; + +class GroupListItem extends StatefulWidget { + const GroupListItem({ + required this.group, + required this.firstUserListItemKey, + super.key, + }); + final Group group; + final GlobalKey? firstUserListItemKey; + + @override + State createState() => _UserListItem(); +} + +class _UserListItem extends State { + MessageSendState state = MessageSendState.send; + Message? currentMessage; + + List messagesNotOpened = []; + late StreamSubscription> messagesNotOpenedStream; + + List lastMessages = []; + late StreamSubscription> lastMessageStream; + + List previewMessages = []; + bool hasNonOpenedMediaFile = false; + + @override + void initState() { + super.initState(); + initStreams(); + } + + @override + void dispose() { + messagesNotOpenedStream.cancel(); + lastMessageStream.cancel(); + super.dispose(); + } + + void initStreams() { + lastMessageStream = twonlyDB.messagesDao + .watchLastMessage(widget.group.groupId) + .listen((update) { + updateState(update, messagesNotOpened); + }); + + messagesNotOpenedStream = twonlyDB.messagesDao + .watchMessageNotOpened(widget.group.groupId) + .listen((update) { + updateState(lastMessages, update); + }); + } + + void updateState( + List newLastMessages, + List newMessagesNotOpened, + ) { + if (newLastMessages.isEmpty) { + // there are no messages at all + currentMessage = null; + previewMessages = []; + } else if (newMessagesNotOpened.isEmpty) { + // there are no not opened messages show just the last message in the table + currentMessage = newLastMessages.last; + previewMessages = newLastMessages; + } else { + // filter first for received messages + final receivedMessages = + newMessagesNotOpened.where((x) => x.senderId != null).toList(); + + if (receivedMessages.isNotEmpty) { + previewMessages = receivedMessages; + currentMessage = receivedMessages.first; + } else { + previewMessages = newMessagesNotOpened; + currentMessage = newMessagesNotOpened.first; + } + } + + final msgs = + previewMessages.where((x) => x.type == MessageType.media).toList(); + if (msgs.isNotEmpty && + msgs.first.type == MessageType.media && + msgs.first.senderId != null && + msgs.first.openedAt == null) { + hasNonOpenedMediaFile = true; + } else { + hasNonOpenedMediaFile = false; + } + + lastMessages = newLastMessages; + messagesNotOpened = newMessagesNotOpened; + setState(() { + // sets lastMessages, messagesNotOpened and currentMessage + }); + } + + Future onTap() async { + if (currentMessage == null) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + return; + } + + if (hasNonOpenedMediaFile) { + final msgs = + previewMessages.where((x) => x.type == MessageType.media).toList(); + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); + if (mediaFile?.downloadState == null) return; + if (mediaFile!.downloadState! == DownloadState.pending) { + await startDownloadMedia(mediaFile, true); + return; + } + if (mediaFile.downloadState! == DownloadState.downloaded) { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MediaViewerView(widget.group); + }, + ), + ); + return; + } + } + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(widget.group); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 0, + bottom: 0, + left: 50, + child: SizedBox( + key: widget.firstUserListItemKey, + height: 20, + width: 20, + ), + ), + GroupContextMenu( + group: widget.group, + child: ListTile( + title: Text( + widget.group.groupName, + ), + subtitle: (currentMessage == null) + ? Text(context.lang.chatsTapToSend) + : Row( + children: [ + MessageSendStateIcon(previewMessages), + const Text('•'), + const SizedBox(width: 5), + if (currentMessage != null) + LastMessageTime(message: currentMessage!), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ), + leading: AvatarIcon(group: widget.group), + trailing: IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + if (hasNonOpenedMediaFile) { + return ChatMessagesView(widget.group); + } else { + return CameraSendToView(widget.group); + } + }, + ), + ); + }, + icon: FaIcon( + hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), + ), + onTap: onTap, + ), + ), + ], + ); + } +} diff --git a/lib/src/views/chats/chat_list_components/last_message_time.dart b/lib/src/views/chats/chat_list_components/last_message_time.dart index 273869c..b81981a 100644 --- a/lib/src/views/chats/chat_list_components/last_message_time.dart +++ b/lib/src/views/chats/chat_list_components/last_message_time.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -21,10 +22,13 @@ class _LastMessageTimeState extends State { void initState() { super.initState(); // Change the color every 200 milliseconds - updateTime = Timer.periodic(const Duration(milliseconds: 500), (timer) { + updateTime = + Timer.periodic(const Duration(milliseconds: 500), (timer) async { + final lastAction = await twonlyDB.messagesDao + .getLastMessageAction(widget.message.messageId); setState(() { lastMessageInSeconds = DateTime.now() - .difference(widget.message.openedAt ?? widget.message.sendAt) + .difference(lastAction?.actionAt ?? widget.message.createdAt) .inSeconds; if (lastMessageInSeconds < 0) { lastMessageInSeconds = 0; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 603c4cb..a318f43 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -1,19 +1,13 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -21,25 +15,17 @@ import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; -import 'package:twonly/src/views/components/animate_icon.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.component.dart'; -import 'package:twonly/src/views/components/verified_shield.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; Color getMessageColor(Message message) { - return (message.messageOtherId == null) + return (message.senderId == null) ? const Color.fromARGB(255, 58, 136, 102) : const Color.fromARGB(233, 68, 137, 255); } -class ChatMessage { - ChatMessage({required this.message, required this.responseTo}); - final Message message; - final Message? responseTo; -} - class ChatItem { const ChatItem._({this.message, this.date, this.time}); factory ChatItem.date(DateTime date) { @@ -48,10 +34,10 @@ class ChatItem { factory ChatItem.time(DateTime time) { return ChatItem._(time: time); } - factory ChatItem.message(ChatMessage message) { + factory ChatItem.message(Message message) { return ChatItem._(message: message); } - final ChatMessage? message; + final Message? message; final DateTime? date; final DateTime? time; bool get isMessage => message != null; @@ -72,14 +58,13 @@ class ChatMessagesView extends StatefulWidget { class _ChatMessagesViewState extends State { TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); - late Contact user; + late Group group; String currentInputText = ''; - late StreamSubscription userSub; + late StreamSubscription userSub; late StreamSubscription> messageSub; List messages = []; List galleryItems = []; - Map> emojiReactionsToMessageId = {}; - Message? responseToMessage; + Message? quotesMessage; GlobalKey verifyShieldKey = GlobalKey(); late FocusNode textFieldFocus; Timer? tutorial; @@ -89,7 +74,7 @@ class _ChatMessagesViewState extends State { @override void initState() { super.initState(); - user = widget.contact; + group = widget.group; textFieldFocus = FocusNode(); initStreams(); @@ -110,118 +95,59 @@ class _ChatMessagesViewState extends State { } Future initStreams() async { - await twonlyDB.messagesDao.removeOldMessages(); - final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId); - userSub = contact.listen((contact) { - if (contact == null) return; + final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId); + userSub = groupStream.listen((newGroup) { + if (newGroup == null) return; setState(() { - user = contact; + group = newGroup; }); }); - final msgStream = - twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId); + final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); messageSub = msgStream.listen((newMessages) async { - // if (!context.mounted) return; - if (Platform.isAndroid) { - await flutterLocalNotificationsPlugin.cancel(widget.contact.userId); - } else { - await flutterLocalNotificationsPlugin.cancelAll(); - } + await flutterLocalNotificationsPlugin.cancelAll(); + final chatItems = []; final storedMediaFiles = []; + DateTime? lastDate; - final tmpEmojiReactionsToMessageId = >{}; - // only send openedMessage to one text message, as receiver will then set all as read... - List openedTextMessageOtherIds; - - final messageOtherMessageIdToMyMessageId = {}; - final messageIdToMessage = {}; - - /// there is probably a better way... - for (final msg in newMessages) { - if (msg.messageOtherId != null) { - messageOtherMessageIdToMyMessageId[msg.messageOtherId!] = - msg.messageId; - } - messageIdToMessage[msg.messageId] = msg; - } + final openedMessages = >{}; for (final msg in newMessages) { - if (msg.kind == MessageKind.textMessage && - msg.messageOtherId != null && - msg.openedAt == null && - (openedTextMessageOtherIds == null || - openedTextMessageOtherIds < msg.messageOtherId!)) { - openedTextMessageOtherIds.add(msg.messageOtherId); + if (msg.type == MessageType.text && + msg.senderId != null && + msg.openedAt == null) { + openedMessages[msg.senderId!]!.add(msg.messageId); } - Message? responseTo; - - if (msg.kind == MessageKind.media && msg.mediaStored) { + if (msg.type == MessageType.media && msg.mediaStored) { storedMediaFiles.add(msg); } - final responseId = msg.responseToMessageId ?? - messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId]; - - var isReaction = false; - if (responseId != null) { - responseTo = messageIdToMessage[responseId]; - final content = MessageContent.fromJson( - msg.kind, - jsonDecode(msg.contentJson!) as Map, - ); - if (content is TextMessageContent) { - if (isEmoji(content.text)) { - isReaction = true; - tmpEmojiReactionsToMessageId - .putIfAbsent(responseId, () => []) - .add(msg); - } - } - if (msg.kind == MessageKind.reopenedMedia) { - isReaction = true; - tmpEmojiReactionsToMessageId - .putIfAbsent(responseId, () => []) - .add(msg); - } - } - if (!isReaction) { - if (lastDate == null || - msg.sendAt.day != lastDate.day || - msg.sendAt.month != lastDate.month || - msg.sendAt.year != lastDate.year) { - chatItems.add(ChatItem.date(msg.sendAt)); - lastDate = msg.sendAt; - } else if (msg.sendAt.difference(lastDate).inMinutes >= 20) { - chatItems.add(ChatItem.time(msg.sendAt)); - lastDate = msg.sendAt; - } - chatItems.add( - ChatItem.message( - ChatMessage( - message: msg, - responseTo: responseTo, - ), - ), - ); + if (lastDate == null || + msg.createdAt.day != lastDate.day || + msg.createdAt.month != lastDate.month || + msg.createdAt.year != lastDate.year) { + chatItems.add(ChatItem.date(msg.createdAt)); + lastDate = msg.createdAt; + } else if (msg.createdAt.difference(lastDate).inMinutes >= 20) { + chatItems.add(ChatItem.time(msg.createdAt)); + lastDate = msg.createdAt; } + chatItems.add(ChatItem.message(msg)); } - if (openedTextMessageOtherIds.isNotEmpty) { + for (final contactId in openedMessages.keys) { await notifyContactAboutOpeningMessage( - widget.contact.userId, - openedTextMessageOtherIds, + contactId, + openedMessages[contactId]!, ); } - await twonlyDB.messagesDao - .openedAllNonMediaMessages(widget.contact.userId); + await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId); setState(() { - emojiReactionsToMessageId = tmpEmojiReactionsToMessageId; messages = chatItems.reversed.toList(); }); @@ -234,33 +160,21 @@ class _ChatMessagesViewState extends State { Future _sendMessage() async { if (newMessageController.text == '') return; - await sendTextMessage( - user.userId, - TextMessageContent( - text: newMessageController.text, - responseToMessageId: responseToMessage?.messageOtherId, - responseToOtherMessageId: responseToMessage?.messageId, - ), - PushNotification( - kind: (responseToMessage == null) - ? PushKind.text - : (isEmoji(newMessageController.text)) - ? PushKind.reaction - : PushKind.response, - reactionContent: (isEmoji(newMessageController.text)) - ? newMessageController.text - : null, - ), + await insertAndSendTextMessage( + group.groupId, + newMessageController.text, + quotesMessage?.messageId, ); + newMessageController.clear(); currentInputText = ''; - responseToMessage = null; + quotesMessage = null; setState(() {}); } - Future scrollToMessage(int messageId) async { + Future scrollToMessage(String messageId) async { final index = messages.indexWhere( - (x) => x.isMessage && x.message!.message.messageId == messageId, + (x) => x.isMessage && x.message!.messageId == messageId, ); if (index == -1) return; setState(() { @@ -286,20 +200,34 @@ class _ChatMessagesViewState extends State { child: Scaffold( appBar: AppBar( title: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(widget.contact.userId); - }, - ), - ); + onTap: () async { + if (widget.group.isDirectChat) { + final member = await twonlyDB.groupsDao + .getGroupMembers(widget.group.groupId); + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(member.first.contactId); + }, + ), + ); + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return GroupView(widget.group); + }, + ), + ); + } }, child: Row( children: [ - ContactAvatar( - contact: user, + AvatarIcon( + group: group, fontSize: 19, ), const SizedBox(width: 10), @@ -308,10 +236,10 @@ class _ChatMessagesViewState extends State { color: Colors.transparent, child: Row( children: [ - Text(getContactDisplayName(user)), + Text(group.groupName), const SizedBox(width: 10), - if (user.verified) - VerifiedShield(key: verifyShieldKey, user), + // if (group.verified) + // VerifiedShield(key: verifyShieldKey, group), ], ), ), @@ -345,7 +273,7 @@ class _ChatMessagesViewState extends State { return Transform.translate( offset: Offset( (focusedScrollItem == i) - ? (chatMessage.message.messageOtherId == null) + ? (chatMessage.quotesMessageId == null) ? -8 : 8 : 0, @@ -354,19 +282,15 @@ class _ChatMessagesViewState extends State { child: Transform.scale( scale: (focusedScrollItem == i) ? 1.05 : 1, child: ChatListEntry( - key: - Key(chatMessage.message.messageId.toString()), + key: Key(chatMessage.messageId), chatMessage, - user, + group, galleryItems, isLastMessageFromSameUser(messages, i), - emojiReactionsToMessageId[ - chatMessage.message.messageId] ?? - [], scrollToMessage: scrollToMessage, onResponseTriggered: () { setState(() { - responseToMessage = chatMessage.message; + quotesMessage = chatMessage; }); textFieldFocus.requestFocus(); }, @@ -377,7 +301,7 @@ class _ChatMessagesViewState extends State { }, ), ), - if (responseToMessage != null && !user.deleted) + if (quotesMessage != null) Container( padding: const EdgeInsets.only( left: 20, @@ -388,15 +312,15 @@ class _ChatMessagesViewState extends State { children: [ Expanded( child: ResponsePreview( - message: responseToMessage!, + message: quotesMessage, showBorder: true, - contact: user, + group: group, ), ), IconButton( onPressed: () { setState(() { - responseToMessage = null; + quotesMessage = null; }); }, icon: const FaIcon( @@ -415,50 +339,48 @@ class _ChatMessagesViewState extends State { top: 10, ), child: Row( - children: (user.deleted) - ? [] - : [ - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.contact); - }, - ), - ); + children: [ + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: inputTextMessageDeco(context), + ), + ), + if (currentInputText != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); }, ), - ], + ); + }, + ), + ], ), ), ], @@ -479,11 +401,11 @@ bool isLastMessageFromSameUser(List messages, int index) { final currentMessage = messages[index]; if (lastMessage.isMessage && currentMessage.isMessage) { - // Check if both messages have the same messageOtherId (or both are null) - return (lastMessage.message!.message.messageOtherId == null && - currentMessage.message!.message.messageOtherId == null) || - (lastMessage.message!.message.messageOtherId != null && - currentMessage.message!.message.messageOtherId != null); + // Check if both messages have the same quotesMessageId (or both are null) + return (lastMessage.message!.quotesMessageId == null && + currentMessage.message!.quotesMessageId == null) || + (lastMessage.message!.quotesMessageId != null && + currentMessage.message!.quotesMessageId != null); } return false; } diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index d4770b5..990f127 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -1,10 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; +import 'package:twonly/src/database/tables/messages.table.dart' + hide MessageActions; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart'; @@ -14,21 +13,19 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain class ChatListEntry extends StatefulWidget { const ChatListEntry( - this.msg, - this.contact, + this.message, + this.group, this.galleryItems, - this.lastMessageFromSameUser, - this.otherReactions, { + this.lastMessageFromSameUser, { required this.onResponseTriggered, required this.scrollToMessage, super.key, }); - final ChatMessage msg; - final Contact contact; + final Message message; + final Group group; final bool lastMessageFromSameUser; - final List otherReactions; final List galleryItems; - final void Function(int) scrollToMessage; + final void Function(String) scrollToMessage; final void Function() onResponseTriggered; @override @@ -36,26 +33,22 @@ class ChatListEntry extends StatefulWidget { } class _ChatListEntryState extends State { - MessageContent? content; - String? textMessage; + MediaFileService? mediaService; @override void initState() { + initAsync(); super.initState(); - final msgContent = MessageContent.fromJson( - widget.msg.message.kind, - jsonDecode(widget.msg.message.contentJson!) as Map, - ); - if (msgContent is TextMessageContent) { - textMessage = msgContent.text; - } - content = msgContent; + } + + Future initAsync() async { + mediaService = await MediaFileService.fromMediaId(widget.message.messageId); + setState(() {}); } @override Widget build(BuildContext context) { - if (content == null) return Container(); - final right = widget.msg.message.messageOtherId == null; + final right = widget.message.senderId == null; return Align( alignment: right ? Alignment.centerRight : Alignment.centerLeft, @@ -64,7 +57,7 @@ class _ChatListEntryState extends State { ? const EdgeInsets.only(top: 5, right: 10, left: 10) : const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), child: MessageContextMenu( - message: widget.msg.message, + message: widget.message, onResponseTriggered: widget.onResponseTriggered, child: Column( mainAxisAlignment: @@ -73,36 +66,36 @@ class _ChatListEntryState extends State { right ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ MessageActions( - message: widget.msg.message, + message: widget.message, onResponseTriggered: widget.onResponseTriggered, child: Stack( alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ ResponseContainer( - msg: widget.msg, - contact: widget.contact, + msg: widget.message, + group: widget.group, + mediaService: mediaService, scrollToMessage: widget.scrollToMessage, - child: (textMessage != null) + child: (widget.message.type == MessageType.text) ? ChatTextEntry( - message: widget.msg, - text: textMessage!, - hasReaction: widget.otherReactions.isNotEmpty, + message: widget.message, ) - : ChatMediaEntry( - message: widget.msg.message, - contact: widget.contact, - galleryItems: widget.galleryItems, - content: content!, - ), + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), ), Positioned( bottom: 5, left: 5, right: 5, child: ReactionRow( - otherReactions: widget.otherReactions, - message: widget.msg.message, + message: widget.message, ), ), ], diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 4be5049..32fa297 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -1,35 +1,33 @@ import 'dart:async'; - -import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +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/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + hide Message; import 'package:twonly/src/services/api/mediafiles/download.service.dart' as received; 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/camera/share_image_editor_view.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/tutorial/tutorials.dart'; class ChatMediaEntry extends StatefulWidget { const ChatMediaEntry({ required this.message, - required this.contact, - required this.content, + required this.group, required this.galleryItems, + required this.mediaService, super.key, }); final Message message; - final Contact contact; - final MessageContent content; + final Group group; final List galleryItems; + final MediaFileService mediaService; @override State createState() => _ChatMediaEntryState(); @@ -39,97 +37,58 @@ class _ChatMediaEntryState extends State { GlobalKey reopenMediaFile = GlobalKey(); bool canBeReopened = false; - @override - void initState() { - super.initState(); - unawaited(checkIfTutorialCanBeShown()); - } - - Future checkIfTutorialCanBeShown() async { - if (widget.message.openedAt == null && - widget.message.messageOtherId != null || - widget.message.mediaStored) { - return; - } - final content = getMediaContent(widget.message); - if (content == null || - content.isRealTwonly || - content.maxShowTime != gMediaShowInfinite) { - return; - } - if (await received.existsMediaFile(widget.message.messageId, 'png')) { - if (mounted) { - setState(() { - canBeReopened = true; - }); - } - Future.delayed(const Duration(seconds: 1), () async { - if (!mounted) return; - await showReopenMediaFilesTutorial(context, reopenMediaFile); - }); - } - } - Future onDoubleTap() async { - if (widget.message.openedAt == null && - widget.message.messageOtherId != null || - widget.message.mediaStored) { + if (widget.message.openedAt == null || widget.message.mediaStored) { return; } - if (await received.existsMediaFile(widget.message.messageId, 'png')) { - await encryptAndSendMessageAsync( - null, - widget.contact.userId, - MessageJson( - kind: MessageKind.reopenedMedia, - messageSenderId: widget.message.messageId, - content: ReopenedMediaFileContent( - messageId: widget.message.messageOtherId!, + if (widget.mediaService.tempPath.existsSync()) { + await sendCipherTextToGroup( + widget.message.groupId, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.REOPENED, + targetMediaId: widget.message.mediaId, ), - timestamp: DateTime.now(), - ), - pushNotification: PushNotification( - kind: PushKind.reopenedMedia, ), ); - await twonlyDB.messagesDao.updateMessageByMessageId( - widget.message.messageId, - const MessagesCompanion(openedAt: Value(null)), - ); + await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId); } } Future onTap() async { - if (widget.message.downloadState == DownloadState.downloaded && + if (widget.mediaService.mediaFile.downloadState == + DownloadState.downloaded && widget.message.openedAt == null) { + if (!mounted) return; await Navigator.push( context, MaterialPageRoute( builder: (context) { return MediaViewerView( - widget.contact, + widget.group, initialMessage: widget.message, ); }, ), ); - await checkIfTutorialCanBeShown(); - } else if (widget.message.downloadState == DownloadState.pending) { - await received.startDownloadMedia(widget.message, true); + } else if (widget.mediaService.mediaFile.downloadState == + DownloadState.pending) { + await received.startDownloadMedia(widget.mediaService.mediaFile, true); } } @override Widget build(BuildContext context) { final color = getMessageColorFromType( - widget.content, + widget.message, + widget.mediaService.mediaFile, context, ); return GestureDetector( key: reopenMediaFile, onDoubleTap: onDoubleTap, - onTap: widget.message.kind == MessageKind.media ? onTap : null, + onTap: (widget.message.type == MessageType.media) ? onTap : null, child: SizedBox( width: 150, height: widget.message.mediaStored ? 271 : null, @@ -139,7 +98,8 @@ class _ChatMediaEntryState extends State { borderRadius: BorderRadius.circular(12), child: InChatMediaViewer( message: widget.message, - contact: widget.contact, + group: widget.group, + mediaService: widget.mediaService, color: color, galleryItems: widget.galleryItems, canBeReopened: canBeReopened, diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index d859e8f..8f95acb 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -1,20 +1,15 @@ -import 'dart:convert'; - +import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; class ReactionRow extends StatefulWidget { const ReactionRow({ - required this.otherReactions, required this.message, super.key, }); - final List otherReactions; final Message message; @override @@ -22,65 +17,79 @@ class ReactionRow extends StatefulWidget { } class _ReactionRowState extends State { + List reactions = []; + StreamSubscription>? reactionsSub; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + reactionsSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + final stream = + twonlyDB.reactionsDao.watchReactions(widget.message.messageId); + + reactionsSub = stream.listen((update) { + setState(() { + reactions = update; + }); + }); + } + @override Widget build(BuildContext context) { final children = []; - var hasOneTextReaction = false; - var hasOneReopened = false; - for (final reaction in widget.otherReactions.reversed) { - final content = MessageContent.fromJson( - reaction.kind, - jsonDecode(reaction.contentJson!) as Map, - ); - - if (content is ReopenedMediaFileContent) { - if (hasOneReopened) continue; - hasOneReopened = true; - children.add( - Expanded( - child: Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.only(right: 3), - child: FaIcon( - FontAwesomeIcons.repeat, - size: 12, - color: isDarkMode(context) ? Colors.white : Colors.black, - ), - ), - ), - ), - ); - } + for (final reaction in reactions) { + // if (content is ReopenedMediaFileContent) { + // if (hasOneReopened) continue; + // hasOneReopened = true; + // children.add( + // Expanded( + // child: Align( + // alignment: Alignment.bottomRight, + // child: Padding( + // padding: const EdgeInsets.only(right: 3), + // child: FaIcon( + // FontAwesomeIcons.repeat, + // size: 12, + // color: isDarkMode(context) ? Colors.white : Colors.black, + // ), + // ), + // ), + // ), + // ); + // } // only show one reaction - if (hasOneTextReaction) continue; - if (content is TextMessageContent) { - hasOneTextReaction = true; - if (!isEmoji(content.text)) continue; - late Widget child; - if (EmojiAnimation.animatedIcons.containsKey(content.text)) { - child = SizedBox( - height: 18, - child: EmojiAnimation(emoji: content.text), - ); - } else { - child = Text(content.text, style: const TextStyle(fontSize: 14)); - } - children.insert( - 0, - Padding( - padding: const EdgeInsets.only(left: 3), - child: child, - ), + late Widget child; + if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) { + child = SizedBox( + height: 18, + child: EmojiAnimation(emoji: reaction.emoji), ); + } else { + child = Text(reaction.emoji, style: const TextStyle(fontSize: 14)); } + children.insert( + 0, + Padding( + padding: const EdgeInsets.only(left: 3), + child: child, + ), + ); } if (children.isEmpty) return Container(); return Row( - mainAxisAlignment: widget.message.messageOtherId == null + mainAxisAlignment: widget.message.senderId == null ? MainAxisAlignment.end : MainAxisAlignment.end, children: children, diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index f161c52..c730270 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/better_text.dart'; @@ -6,17 +7,14 @@ import 'package:twonly/src/views/components/better_text.dart'; class ChatTextEntry extends StatelessWidget { const ChatTextEntry({ required this.message, - required this.text, - required this.hasReaction, super.key, }); - final String text; - final ChatMessage message; - final bool hasReaction; + final Message message; @override Widget build(BuildContext context) { + final text = message.content ?? ''; if (EmojiAnimation.supported(text)) { return Container( constraints: const BoxConstraints( @@ -33,16 +31,10 @@ class ChatTextEntry extends StatelessWidget { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), - padding: EdgeInsets.only( - left: 10, - top: 4, - bottom: 4, - right: hasReaction ? 30 : 10, - ), + padding: const EdgeInsets.only(left: 10, top: 4, bottom: 4), decoration: BoxDecoration( - color: message.responseTo == null - ? getMessageColor(message.message) - : null, + color: + message.quotesMessageId == null ? getMessageColor(message) : null, borderRadius: BorderRadius.circular(12), ), child: BetterText(text: text), diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index 28a4623..255cac9 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -1,9 +1,9 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -11,7 +11,8 @@ import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; class InChatMediaViewer extends StatefulWidget { const InChatMediaViewer({ required this.message, - required this.contact, + required this.group, + required this.mediaService, required this.color, required this.galleryItems, required this.canBeReopened, @@ -19,7 +20,8 @@ class InChatMediaViewer extends StatefulWidget { }); final Message message; - final Contact contact; + final Group group; + final MediaFileService mediaService; final List galleryItems; final Color color; final bool canBeReopened; @@ -56,8 +58,7 @@ class _InChatMediaViewerState extends State { bool loadIndex() { if (widget.message.mediaStored) { final index = widget.galleryItems.indexWhere( - (x) => - x.id == (widget.message.mediaUploadId ?? widget.message.messageId), + (x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId), ); if (index != -1) { galleryItemIndex = index; @@ -83,7 +84,7 @@ class _InChatMediaViewerState extends State { if (widget.message.mediaStored) return; final stream = twonlyDB.messagesDao - .getMessageByMessageId(widget.message.messageId) + .getMessageById(widget.message.messageId) .watchSingleOrNull(); messageStream = stream.listen((updated) async { if (updated != null) { diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 4118d69..542fcdd 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,14 +1,14 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' + as pb; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; @@ -48,24 +48,22 @@ class MessageContextMenu extends StatelessWidget { ) as EmojiLayerData?; if (layer == null) return; - await sendTextMessage( - message.contactId, - TextMessageContent( - text: layer.text, - responseToMessageId: message.messageOtherId, - responseToOtherMessageId: - (message.messageOtherId == null) ? message.messageId : null, + await twonlyDB.reactionsDao.insertReaction( + ReactionsCompanion( + messageId: Value(message.messageId), + emoji: Value(layer.text), + ), + ); + + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: message.messageId, + emoji: layer.text, + remove: false, + ), ), - (message.messageOtherId != null) - ? PushNotification( - kind: (message.kind == MessageKind.textMessage) - ? PushKind.reactionToText - : (getMediaContent(message)!.isVideo) - ? PushKind.reactionToVideo - : PushKind.reactionToImage, - reactionContent: layer.text, - ) - : null, ); }, child: const FaIcon(FontAwesomeIcons.faceLaugh), @@ -75,15 +73,15 @@ class MessageContextMenu extends StatelessWidget { onSelect: onResponseTriggered, child: const FaIcon(FontAwesomeIcons.reply), ), - PieAction( - tooltip: Text(context.lang.copy), - onSelect: () async { - final text = getMessageText(message); - await Clipboard.setData(ClipboardData(text: text)); - await HapticFeedback.heavyImpact(); - }, - child: const FaIcon(FontAwesomeIcons.solidCopy), - ), + if (message.content != null) + PieAction( + tooltip: Text(context.lang.copy), + onSelect: () async { + await Clipboard.setData(ClipboardData(text: message.content!)); + await HapticFeedback.heavyImpact(); + }, + child: const FaIcon(FontAwesomeIcons.solidCopy), + ), PieAction( tooltip: Text(context.lang.delete), onSelect: () async { @@ -94,8 +92,7 @@ class MessageContextMenu extends StatelessWidget { customOk: context.lang.deleteOkBtn, ); if (delete) { - await twonlyDB.messagesDao - .deleteMessagesByMessageId(message.messageId); + await twonlyDB.messagesDao.deleteMessagesById(message.messageId); } }, child: const FaIcon(FontAwesomeIcons.trash), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 52293e4..d423c10 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -1,11 +1,11 @@ import 'dart:collection'; -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/globals.dart'; +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/json/message_old.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -18,17 +18,23 @@ enum MessageSendState { sending, } -MessageSendState messageSendStateFromMessage(Message msg) { +Future messageSendStateFromMessage(Message msg) async { MessageSendState state; - if (!msg.acknowledgeByServer) { - if (msg.messageOtherId == null) { + final ackByServer = await twonlyDB.messagesDao.haveAllMembers( + msg.groupId, + msg.messageId, + MessageActionType.ackByServerAt, + ); + + if (!ackByServer) { + if (msg.senderId == null) { state = MessageSendState.sending; } else { state = MessageSendState.receiving; } } else { - if (msg.messageOtherId == null) { + if (msg.senderId == null) { // message send if (msg.openedAt == null) { state = MessageSendState.send; @@ -63,9 +69,113 @@ class MessageSendStateIcon extends StatefulWidget { } class _MessageSendStateIconState extends State { + List icons = []; + String text = ''; + Widget? textWidget; + @override void initState() { super.initState(); + initAsync(); + } + + Future initAsync() async { + final kindsAlreadyShown = HashSet(); + + for (final message in widget.messages) { + if (icons.length == 2) break; + if (kindsAlreadyShown.contains(message.type)) continue; + kindsAlreadyShown.add(message.type); + + final state = await messageSendStateFromMessage(message); + + final mediaFile = message.mediaId == null + ? null + : await MediaFileService.fromMediaId(message.mediaId!); + + if (!mounted) return; + + final color = + getMessageColorFromType(message, mediaFile?.mediaFile, context); + + Widget icon = const Placeholder(); + textWidget = null; + + switch (state) { + case MessageSendState.receivedOpened: + icon = Icon(Icons.crop_square, size: 14, color: color); + if (message.content != null) { + if (isEmoji(message.content!)) { + icon = Text( + message.content!, + style: const TextStyle(fontSize: 12), + ); + } + } + text = context.lang.messageSendState_Received; + if (widget.canBeReopened) { + textWidget = Text( + context.lang.doubleClickToReopen, + style: const TextStyle(fontSize: 9), + ); + } + case MessageSendState.sendOpened: + icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color); + text = context.lang.messageSendState_Opened; + case MessageSendState.received: + icon = Icon(Icons.square_rounded, size: 14, color: color); + text = context.lang.messageSendState_Received; + if (message.type == MessageType.media) { + if (mediaFile!.mediaFile.downloadState == DownloadState.pending) { + text = context.lang.messageSendState_TapToLoad; + } + if (mediaFile.mediaFile.downloadState == + DownloadState.downloading) { + text = context.lang.messageSendState_Loading; + icon = getLoaderIcon(color); + } + } + case MessageSendState.send: + icon = + FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color); + text = context.lang.messageSendState_Send; + case MessageSendState.sending: + icon = getLoaderIcon(color); + text = context.lang.messageSendState_Sending; + case MessageSendState.receiving: + icon = getLoaderIcon(color); + text = context.lang.messageSendState_Received; + } + + if (message.mediaStored) { + icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); + text = context.lang.messageStoredInGallery; + } + + if (mediaFile != null) { + if (mediaFile.mediaFile.stored) { + icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); + text = context.lang.messageReopened; + } + + if (mediaFile.mediaFile.reuploadRequestedBy != null) { + icon = + FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); + textWidget = Text( + context.lang.retransmissionRequested, + style: const TextStyle(fontSize: 9), + ); + } + } + + if (message.type == MessageType.media) { + icons.insert(0, icon); + } else { + icons.add(icon); + } + } + + setState(() {}); } Widget getLoaderIcon(Color color) { @@ -83,108 +193,6 @@ class _MessageSendStateIconState extends State { @override Widget build(BuildContext context) { - final icons = []; - var text = ''; - - final kindsAlreadyShown = HashSet(); - Widget? textWidget; - - for (final message in widget.messages) { - if (icons.length == 2) break; - if (kindsAlreadyShown.contains(message.kind)) continue; - kindsAlreadyShown.add(message.kind); - - final state = messageSendStateFromMessage(message); - late Color color; - MessageContent? content; - - if (message.contentJson == null) { - color = getMessageColorFromType(TextMessageContent(text: ''), context); - } else { - content = MessageContent.fromJson( - message.kind, - jsonDecode(message.contentJson!) as Map, - ); - if (content == null) continue; - color = getMessageColorFromType(content, context); - } - - Widget icon = const Placeholder(); - textWidget = null; - - switch (state) { - case MessageSendState.receivedOpened: - icon = Icon(Icons.crop_square, size: 14, color: color); - if (content is TextMessageContent) { - if (isEmoji(content.text)) { - icon = Text(content.text, style: const TextStyle(fontSize: 12)); - } - } - text = context.lang.messageSendState_Received; - if (widget.canBeReopened) { - textWidget = Text( - context.lang.doubleClickToReopen, - style: const TextStyle(fontSize: 9), - ); - } - case MessageSendState.sendOpened: - icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color); - text = context.lang.messageSendState_Opened; - case MessageSendState.received: - icon = Icon(Icons.square_rounded, size: 14, color: color); - text = context.lang.messageSendState_Received; - if (message.kind == MessageKind.media) { - if (message.downloadState == DownloadState.pending) { - text = context.lang.messageSendState_TapToLoad; - } - if (message.downloadState == DownloadState.downloading) { - text = context.lang.messageSendState_Loading; - icon = getLoaderIcon(color); - } - } - case MessageSendState.send: - icon = - FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color); - text = context.lang.messageSendState_Send; - case MessageSendState.sending: - icon = getLoaderIcon(color); - text = context.lang.messageSendState_Sending; - case MessageSendState.receiving: - icon = getLoaderIcon(color); - text = context.lang.messageSendState_Received; - } - - if (message.kind == MessageKind.storedMediaFile) { - icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); - text = context.lang.messageStoredInGallery; - } - - if (message.kind == MessageKind.reopenedMedia) { - icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); - text = context.lang.messageReopened; - } - - if (message.errorWhileSending) { - icon = - FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color); - text = 'Error'; - } - - if (message.mediaRetransmissionState == MediaRetransmitting.requested) { - icon = FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); - textWidget = Text( - context.lang.retransmissionRequested, - style: const TextStyle(fontSize: 9), - ); - } - - if (message.kind == MessageKind.media) { - icons.insert(0, icon); - } else { - icons.add(icon); - } - } - if (icons.isEmpty) return Container(); var icon = icons[0]; @@ -215,7 +223,7 @@ class _MessageSendStateIconState extends State { icon, const SizedBox(width: 3), if (textWidget != null) - textWidget + textWidget! else Text( text, diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index c02d4b2..b46d6fd 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -1,29 +1,27 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:flutter/material.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/globals.dart'; +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/json/message_old.dart'; -import 'package:twonly/src/model/memory_item.model.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.view.dart'; class ResponseContainer extends StatefulWidget { const ResponseContainer({ required this.msg, - required this.contact, + required this.group, required this.child, required this.scrollToMessage, + required this.mediaService, super.key, }); - final ChatMessage msg; - final Widget child; - final Contact contact; - final void Function(int) scrollToMessage; + final Message msg; + final Widget? child; + final Group group; + final MediaFileService? mediaService; + final void Function(String) scrollToMessage; @override State createState() => _ResponseContainerState(); @@ -57,17 +55,20 @@ class _ResponseContainerState extends State { @override Widget build(BuildContext context) { - if (widget.msg.responseTo == null) { - return widget.child; + if (widget.msg.quotesMessageId == null) { + if (widget.child == null) { + return Container(); + } + return widget.child!; } return GestureDetector( - onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId), + onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), decoration: BoxDecoration( - color: getMessageColor(widget.msg.message), + color: getMessageColor(widget.msg), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -88,8 +89,8 @@ class _ResponseContainerState extends State { ), ), child: ResponsePreview( - contact: widget.contact, - message: widget.msg.responseTo!, + group: widget.group, + messageId: widget.msg.quotesMessageId, showBorder: false, ), ), @@ -108,14 +109,16 @@ class _ResponseContainerState extends State { class ResponsePreview extends StatefulWidget { const ResponsePreview({ - required this.message, - required this.contact, + required this.group, required this.showBorder, + this.message, + this.messageId, super.key, }); - final Message message; - final Contact contact; + final Message? message; + final String? messageId; + final Group group; final bool showBorder; @override @@ -123,56 +126,49 @@ class ResponsePreview extends StatefulWidget { } class _ResponsePreviewState extends State { - File? thumbnailPath; + Message? message; + MediaFileService? mediaService; @override void initState() { + message = widget.message; + initAsync(); super.initState(); - unawaited(initAsync()); } Future initAsync() async { - final items = await MemoryItem.convertFromMessages([widget.message]); - if (items.length == 1 && mounted) { - setState(() { - thumbnailPath = items.values.first.thumbnailPath; - }); + message ??= await twonlyDB.messagesDao + .getMessageById(widget.messageId!) + .getSingleOrNull(); + if (message?.mediaId != null) { + mediaService = await MediaFileService.fromMediaId(message!.mediaId!); } + if (mounted) setState(() {}); } @override Widget build(BuildContext context) { + if (message == null) return Container(); String? subtitle; - if (widget.message.kind == MessageKind.textMessage) { - if (widget.message.contentJson != null) { - final content = MessageContent.fromJson( - MessageKind.textMessage, - jsonDecode(widget.message.contentJson!) as Map, - ); - if (content is TextMessageContent) { - subtitle = truncateString(content.text); - } + if (message!.type == MessageType.text) { + if (message!.content != null) { + subtitle = truncateString(message!.content!); } } - if (widget.message.kind == MessageKind.media) { - final content = MessageContent.fromJson( - MessageKind.media, - jsonDecode(widget.message.contentJson!) as Map, - ); - if (content is MediaMessageContent) { - subtitle = content.isVideo ? 'Video' : 'Image'; - } + if (message!.type == MessageType.media && mediaService != null) { + subtitle = + mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image'; } var username = 'You'; - if (widget.message.messageOtherId != null) { - username = getContactDisplayName(widget.contact); + if (message!.senderId != null) { + username = message!.senderId.toString(); } - final color = getMessageColor(widget.message); + final color = getMessageColor(message!); - if (!widget.message.mediaStored) { + if (!message!.mediaStored) { return Container( padding: widget.showBorder ? const EdgeInsets.only(left: 10, right: 10) @@ -225,10 +221,10 @@ class _ResponsePreviewState extends State { ], ), ), - if (thumbnailPath != null) + if (mediaService != null) SizedBox( height: widget.showBorder ? 100 : 210, - child: Image.file(thumbnailPath!), + child: Image.file(mediaService!.thumbnailPath), ), ], ), diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 887c1b6..5a17af7 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -1,29 +1,24 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart' + show DownloadState, MediaType; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:video_player/video_player.dart'; @@ -31,8 +26,8 @@ import 'package:video_player/video_player.dart'; final NoScreenshot _noScreenshot = NoScreenshot.instance; class MediaViewerView extends StatefulWidget { - const MediaViewerView(this.contact, {super.key, this.initialMessage}); - final Contact contact; + const MediaViewerView(this.group, {super.key, this.initialMessage}); + final Group group; final Message? initialMessage; @@ -48,23 +43,21 @@ class _MediaViewerViewState extends State { double mediaViewerDistanceFromBottom = 0; // current image related - Uint8List? imageBytes; - String? videoPath; VideoPlayerController? videoController; + MediaFileService? currentMedia; + Message? currentMessage; + DateTime? canBeSeenUntil; - int maxShowTime = gMediaShowInfinite; double progress = 0; - bool isRealTwonly = false; - bool mirrorVideo = false; - bool isDownloading = false; bool showSendTextMessageInput = false; final GlobalKey mediaWidgetKey = GlobalKey(); bool imageSaved = false; bool imageSaving = false; + bool displayTwonlyPresent = true; - StreamSubscription? downloadStateListener; + StreamSubscription? downloadStateListener; List allMediaFiles = []; late StreamSubscription> _subscription; @@ -94,20 +87,21 @@ class _MediaViewerViewState extends State { Future asyncLoadNextMedia(bool firstRun) async { final messages = - twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId); + twonlyDB.messagesDao.watchMediaNotOpened(widget.group.groupId); - _subscription = messages.listen((messages) { + _subscription = messages.listen((messages) async { for (final msg in messages) { - // if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { - // allMediaFiles.add(msg); - // } - // Find the index of the existing message with the same messageId + if (msg.mediaId == currentMedia?.mediaFile.mediaId) { + // The update of the current Media in case of a download is done in loadCurrentMediaFile + continue; + } + + /// If the messages was already there just replace it and go to the next... + final index = allMediaFiles.indexWhere((m) => m.messageId == msg.messageId); if (index >= 1) { - // to not modify the first message - // If the message exists, replace it allMediaFiles[index] = msg; } else if (index == -1) { // If the message does not exist, add it @@ -116,101 +110,90 @@ class _MediaViewerViewState extends State { } setState(() {}); if (firstRun) { - loadCurrentMediaFile(); // ignore: parameter_assignments firstRun = false; + await loadCurrentMediaFile(); } }); } Future nextMediaOrExit() async { - if (!mounted) return; - await videoController?.dispose(); - nextMediaTimer?.cancel(); - progressTimer?.cancel(); - if (allMediaFiles.isNotEmpty) { - try { - if (!imageSaved && maxShowTime != gMediaShowInfinite) { - await deleteMediaFile(allMediaFiles.first.messageId, 'mp4'); - await deleteMediaFile(allMediaFiles.first.messageId, 'png'); - } - } catch (e) { - Log.error('$e'); + /// Remove the current media file in case it is not set to unlimited + if (currentMedia != null) { + if (!imageSaved && + currentMedia!.mediaFile.displayLimitInMilliseconds != null) { + currentMedia!.fullMediaRemoval(); } } - if (allMediaFiles.isEmpty || allMediaFiles.length == 1) { - if (mounted) { - Navigator.pop(context); - } + + await videoController?.dispose(); + if (!mounted) return; + + nextMediaTimer?.cancel(); + progressTimer?.cancel(); + + if (allMediaFiles.isEmpty) { + Navigator.pop(context); } else { - allMediaFiles.removeAt(0); await loadCurrentMediaFile(); } } Future loadCurrentMediaFile({bool showTwonly = false}) async { - if (!mounted) return; - if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit(); + if (!mounted || !context.mounted) return; + if (allMediaFiles.isEmpty) return nextMediaOrExit(); await _noScreenshot.screenshotOff(); setState(() { videoController = null; - imageBytes = null; + currentMedia = null; + currentMessage = null; canBeSeenUntil = null; - maxShowTime = gMediaShowInfinite; imageSaving = false; imageSaved = false; - mirrorVideo = false; progress = 0; - videoPath = null; - isDownloading = false; - isRealTwonly = false; showSendTextMessageInput = false; }); - if (Platform.isAndroid) { - await flutterLocalNotificationsPlugin - .cancel(allMediaFiles.first.contactId); - } else { - await flutterLocalNotificationsPlugin.cancelAll(); - } + // if (Platform.isAndroid) { + // await flutterLocalNotificationsPlugin + // .cancel(allMediaFiles.first.contactId); + // } else { + await flutterLocalNotificationsPlugin.cancelAll(); + // } - if (allMediaFiles.first.downloadState != DownloadState.downloaded) { - setState(() { - isDownloading = true; - }); - await startDownloadMedia(allMediaFiles.first, true); + final stream = + twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId); - final stream = twonlyDB.messagesDao - .getMessageByMessageId(allMediaFiles.first.messageId) - .watchSingleOrNull(); - await downloadStateListener?.cancel(); - downloadStateListener = stream.listen((updated) async { - if (updated != null) { - if (updated.downloadState == DownloadState.downloaded) { - await downloadStateListener?.cancel(); - await handleNextDownloadedMedia(updated, showTwonly); - // start downloading all the other possible missing media files. - await tryDownloadAllMediaFiles(force: true); - } + var downloadTriggered = false; + + await downloadStateListener?.cancel(); + downloadStateListener = stream.listen((updated) async { + if (updated == null) return; + if (updated.downloadState != DownloadState.downloaded) { + if (!downloadTriggered) { + downloadTriggered = true; + await startDownloadMedia(currentMedia!.mediaFile, true); + unawaited(tryDownloadAllMediaFiles(force: true)); } - }); - } else { - await handleNextDownloadedMedia(allMediaFiles.first, showTwonly); - } + return; + } + + await downloadStateListener?.cancel(); + await handleNextDownloadedMedia(showTwonly); + // start downloading all the other possible missing media files. + }); } Future handleNextDownloadedMedia( - Message current, bool showTwonly, ) async { - final content = - MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map); + currentMessage = allMediaFiles.removeAt(0); + final currentMediaLocal = + await MediaFileService.fromMediaId(currentMessage!.mediaId!); + if (currentMediaLocal == null || !mounted) return; - if (content.isRealTwonly) { - setState(() { - isRealTwonly = true; - }); + if (currentMediaLocal.mediaFile.requiresAuthentication) { if (!showTwonly) return; final isAuth = await authenticateUser( @@ -224,66 +207,56 @@ class _MediaViewerViewState extends State { } await notifyContactAboutOpeningMessage( - current.contactId, - [current.messageOtherId!], + currentMessage!.senderId!, + [currentMessage!.messageId], ); - await twonlyDB.messagesDao.updateMessageByMessageId( - current.messageId, + await twonlyDB.messagesDao.updateMessageId( + currentMessage!.messageId, MessagesCompanion(openedAt: Value(DateTime.now())), ); - if (content.isVideo) { - final videoPathTmp = await getVideoPath(current.messageId); - if (videoPathTmp != null) { - videoController = VideoPlayerController.file(File(videoPathTmp.path)); - await videoController - ?.setLooping(content.maxShowTime == gMediaShowInfinite); - await videoController?.initialize().then((_) { - videoController!.play(); - videoController?.addListener(() { - setState(() { - progress = 1 - - videoController!.value.position.inSeconds / - videoController!.value.duration.inSeconds; - }); - if (content.maxShowTime != gMediaShowInfinite) { - if (videoController?.value.position == - videoController?.value.duration) { - nextMediaOrExit(); - } - } - }); - setState(() { - videoPath = videoPathTmp.path; - }); - // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler - }).catchError(Log.error); - } - } - - imageBytes = await getImageBytes(current.messageId); - - if ((imageBytes == null && !content.isVideo) || - (content.isVideo && videoController == null)) { - Log.error('media files are not found...'); - // When the message should be downloaded but imageBytes are null then a error happened - await handleMediaError(current); + if (!currentMediaLocal.tempPath.existsSync()) { + Log.error('Temp media file not found...'); + await handleMediaError(currentMediaLocal.mediaFile); return nextMediaOrExit(); } - if (!content.isVideo) { - if (content.maxShowTime != gMediaShowInfinite) { + if (currentMediaLocal.mediaFile.type == MediaType.video) { + videoController = VideoPlayerController.file(currentMediaLocal.tempPath); + await videoController?.setLooping( + currentMediaLocal.mediaFile.displayLimitInMilliseconds == null, + ); + await videoController?.initialize().then((_) { + videoController!.play(); + videoController?.addListener(() { + setState(() { + progress = 1 - + videoController!.value.position.inSeconds / + videoController!.value.duration.inSeconds; + }); + if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { + if (videoController?.value.position == + videoController?.value.duration) { + nextMediaOrExit(); + } + } + }); + // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler + }).catchError(Log.error); + } else { + if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { canBeSeenUntil = DateTime.now().add( - Duration(seconds: content.maxShowTime), + Duration( + milliseconds: + currentMediaLocal.mediaFile.displayLimitInMilliseconds!, + ), ); startTimer(); } } setState(() { - maxShowTime = content.maxShowTime; - isDownloading = false; - mirrorVideo = content.mirrorVideo; + currentMedia = currentMediaLocal; }); } @@ -299,44 +272,37 @@ class _MediaViewerViewState extends State { if (canBeSeenUntil != null) { final difference = canBeSeenUntil!.difference(DateTime.now()); // Calculate the progress as a value between 0.0 and 1.0 - progress = difference.inMilliseconds / (maxShowTime * 1000); + progress = difference.inMilliseconds / + (currentMedia!.mediaFile.displayLimitInMilliseconds!); setState(() {}); } }); } Future onPressedSaveToGallery() async { - if (allMediaFiles.first.messageOtherId == null) { - return; // should not be possible - } setState(() { imageSaving = true; }); - await twonlyDB.messagesDao.updateMessageByMessageId( - allMediaFiles.first.messageId, - const MessagesCompanion(mediaStored: Value(true)), - ); - await encryptAndSendMessageAsync( - null, - widget.contact.userId, - MessageJson( - kind: MessageKind.storedMediaFile, - messageSenderId: allMediaFiles.first.messageId, - messageReceiverId: allMediaFiles.first.messageOtherId, - content: MessageContent(), - timestamp: DateTime.now(), + await currentMedia!.storeMediaFile(); + await sendCipherTextToGroup( + widget.group.groupId, + pb.EncryptedContent( + mediaUpdate: pb.EncryptedContent_MediaUpdate( + type: pb.EncryptedContent_MediaUpdate_Type.STORED, + targetMediaId: currentMedia!.mediaFile.mediaId, + ), ), - pushNotification: PushNotification(kind: PushKind.storedMediaFile), ); setState(() { imageSaved = true; }); - final user = await getUser(); - if (user != null && (user.storeMediaFilesInGallery)) { - if (videoPath != null) { - await saveVideoToGallery(videoPath!); - } else { - await saveImageToGallery(imageBytes!); + + if (gUser.storeMediaFilesInGallery) { + if (currentMedia!.mediaFile.type == MediaType.video) { + await saveVideoToGallery(currentMedia!.storedPath.path); + } else if (currentMedia!.mediaFile.type == MediaType.image) { + final imageBytes = await currentMedia!.storedPath.readAsBytes(); + await saveImageToGallery(imageBytes); } } setState(() { @@ -358,7 +324,8 @@ class _MediaViewerViewState extends State { key: mediaWidgetKey, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (maxShowTime == gMediaShowInfinite) + if (currentMedia != null && + currentMedia!.mediaFile.displayLimitInMilliseconds == null) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: imageSaved @@ -368,7 +335,7 @@ class _MediaViewerViewState extends State { ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.primary, ), - onPressed: onPressedSaveToGallery, + onPressed: (currentMedia == null) ? null : onPressedSaveToGallery, child: Row( children: [ if (imageSaving) @@ -450,11 +417,12 @@ class _MediaViewerViewState extends State { context, MaterialPageRoute( builder: (context) { - return CameraSendToView(widget.contact); + return CameraSendToView(widget.group); }, ), ); - if (mounted && maxShowTime != gMediaShowInfinite) { + if (mounted && + currentMedia!.mediaFile.displayLimitInMilliseconds != null) { await nextMediaOrExit(); } else { await videoController?.play(); @@ -477,7 +445,7 @@ class _MediaViewerViewState extends State { child: Stack( fit: StackFit.expand, children: [ - if ((imageBytes != null || videoController != null) && + if ((currentMedia != null || videoController != null) && (canBeSeenUntil == null || progress >= 0)) GestureDetector( onTap: () { @@ -497,15 +465,12 @@ class _MediaViewerViewState extends State { children: [ if (videoController != null) Positioned.fill( - child: Transform.flip( - flipX: mirrorVideo, - child: VideoPlayer(videoController!), - ), + child: VideoPlayer(videoController!), ), - if (imageBytes != null) + if (currentMedia!.mediaFile.type == MediaType.image) Positioned.fill( - child: Image.memory( - imageBytes!, + child: Image.file( + currentMedia!.tempPath, fit: BoxFit.contain, frameBuilder: ( context, @@ -534,7 +499,9 @@ class _MediaViewerViewState extends State { ), ), ), - if (isRealTwonly && imageBytes == null) + if (currentMedia != null && + currentMedia!.mediaFile.requiresAuthentication && + displayTwonlyPresent) Positioned.fill( child: GestureDetector( onTap: () { @@ -570,7 +537,8 @@ class _MediaViewerViewState extends State { ], ), ), - if (isDownloading) + if (currentMedia?.mediaFile.downloadState != + DownloadState.downloaded) const Positioned.fill( child: Center( child: SizedBox( @@ -602,7 +570,7 @@ class _MediaViewerViewState extends State { left: showSendTextMessageInput ? 0 : null, right: showSendTextMessageInput ? 0 : 15, child: Text( - getContactDisplayName(widget.contact), + widget.group.groupName, textAlign: TextAlign.center, style: TextStyle( fontSize: showSendTextMessageInput ? 24 : 14, @@ -658,18 +626,12 @@ class _MediaViewerViewState extends State { ), IconButton( icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () { + onPressed: () async { if (textMessageController.text.isNotEmpty) { - sendTextMessage( - widget.contact.userId, - TextMessageContent( - text: textMessageController.text, - responseToMessageId: - allMediaFiles.first.messageOtherId, - ), - PushNotification( - kind: PushKind.response, - ), + await insertAndSendTextMessage( + widget.group.groupId, + textMessageController.text, + currentMessage!.messageId, ); textMessageController.clear(); } @@ -683,14 +645,13 @@ class _MediaViewerViewState extends State { ), ), ), - if (allMediaFiles.isNotEmpty) + if (currentMedia != null) ReactionButtons( show: showShortReactions, textInputFocused: showSendTextMessageInput, mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, - userId: widget.contact.userId, - responseToMessageId: allMediaFiles.first.messageOtherId!, - isVideo: videoController != null, + groupId: widget.group.groupId, + messageId: currentMessage!.messageId, hide: () { setState(() { showShortReactions = false; @@ -704,195 +665,3 @@ class _MediaViewerViewState extends State { ); } } - -class ReactionButtons extends StatefulWidget { - const ReactionButtons({ - required this.show, - required this.textInputFocused, - required this.userId, - required this.mediaViewerDistanceFromBottom, - required this.responseToMessageId, - required this.isVideo, - required this.hide, - super.key, - }); - - final double mediaViewerDistanceFromBottom; - final bool show; - final bool isVideo; - final bool textInputFocused; - final int userId; - final int responseToMessageId; - final void Function() hide; - - @override - State createState() => _ReactionButtonsState(); -} - -class _ReactionButtonsState extends State { - int selectedShortReaction = -1; - - List selectedEmojis = - EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); - - @override - void initState() { - super.initState(); - initAsync(); - } - - Future initAsync() async { - final user = await getUser(); - if (user != null && user.preSelectedEmojies != null) { - selectedEmojis = user.preSelectedEmojies!; - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final firstRowEmojis = selectedEmojis.take(6).toList(); - final secondRowEmojis = - selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; - - return AnimatedPositioned( - duration: const Duration(milliseconds: 200), // Animation duration - bottom: widget.show - ? (widget.textInputFocused - ? 50 - : widget.mediaViewerDistanceFromBottom) - : widget.mediaViewerDistanceFromBottom - 20, - left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, - right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, - curve: Curves.linearToEaseOut, - child: AnimatedOpacity( - opacity: widget.show ? 1.0 : 0.0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: Container( - color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, - padding: - widget.show ? const EdgeInsets.symmetric(vertical: 32) : null, - child: Column( - children: [ - if (secondRowEmojis.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: secondRowEmojis - .map( - (emoji) => EmojiReactionWidget( - userId: widget.userId, - responseToMessageId: widget.responseToMessageId, - hide: widget.hide, - show: widget.show, - isVideo: widget.isVideo, - emoji: emoji as String, - ), - ) - .toList(), - ), - if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: firstRowEmojis - .map( - (emoji) => EmojiReactionWidget( - userId: widget.userId, - responseToMessageId: widget.responseToMessageId, - hide: widget.hide, - show: widget.show, - isVideo: widget.isVideo, - emoji: emoji, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ); - } -} - -class EmojiReactionWidget extends StatefulWidget { - const EmojiReactionWidget({ - required this.userId, - required this.responseToMessageId, - required this.hide, - required this.isVideo, - required this.show, - required this.emoji, - super.key, - }); - final int userId; - final int responseToMessageId; - final Function hide; - final bool show; - final bool isVideo; - final String emoji; - - @override - State createState() => _EmojiReactionWidgetState(); -} - -class _EmojiReactionWidgetState extends State { - int selectedShortReaction = -1; - - @override - Widget build(BuildContext context) { - return AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - child: GestureDetector( - onTap: () { - sendTextMessage( - widget.userId, - TextMessageContent( - text: widget.emoji, - responseToMessageId: widget.responseToMessageId, - ), - PushNotification( - kind: widget.isVideo - ? PushKind.reactionToVideo - : PushKind.reactionToImage, - reactionContent: widget.emoji, - ), - ); - setState(() { - selectedShortReaction = 0; // Assuming index is 0 for this example - }); - Future.delayed(const Duration(milliseconds: 300), () { - if (mounted) { - setState(() { - widget.hide(); - selectedShortReaction = -1; - }); - } - }); - }, - child: (selectedShortReaction == - 0) // Assuming index is 0 for this example - ? EmojiAnimationFlying( - emoji: widget.emoji, - duration: const Duration(milliseconds: 300), - startPosition: 0, - size: (widget.show) ? 40 : 10, - ) - : AnimatedOpacity( - opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: SizedBox( - width: widget.show ? 40 : 10, - child: Center( - child: EmojiAnimation( - emoji: widget.emoji, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart new file mode 100644 index 0000000..bea166e --- /dev/null +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -0,0 +1,92 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +class EmojiReactionWidget extends StatefulWidget { + const EmojiReactionWidget({ + required this.messageId, + required this.groupId, + required this.hide, + required this.show, + required this.emoji, + super.key, + }); + final String messageId; + final String groupId; + final Function hide; + final bool show; + final String emoji; + + @override + State createState() => _EmojiReactionWidgetState(); +} + +class _EmojiReactionWidgetState extends State { + int selectedShortReaction = -1; + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.linearToEaseOut, + child: GestureDetector( + onTap: () async { + await twonlyDB.reactionsDao.insertReaction( + ReactionsCompanion( + messageId: Value(widget.messageId), + emoji: Value(widget.emoji), + ), + ); + + await sendCipherTextToGroup( + widget.groupId, + EncryptedContent( + reaction: EncryptedContent_Reaction( + targetMessageId: widget.messageId, + emoji: widget.emoji, + ), + ), + ); + + setState(() { + selectedShortReaction = 0; // Assuming index is 0 for this example + }); + Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) { + setState(() { + widget.hide(); + selectedShortReaction = -1; + }); + } + }); + }, + child: (selectedShortReaction == + 0) // Assuming index is 0 for this example + ? EmojiAnimationFlying( + emoji: widget.emoji, + duration: const Duration(milliseconds: 300), + startPosition: 0, + size: (widget.show) ? 40 : 10, + ) + : AnimatedOpacity( + opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out + duration: const Duration(milliseconds: 150), + child: SizedBox( + width: widget.show ? 40 : 10, + child: Center( + child: EmojiAnimation( + emoji: widget.emoji, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart new file mode 100644 index 0000000..75146c4 --- /dev/null +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +class ReactionButtons extends StatefulWidget { + const ReactionButtons({ + required this.show, + required this.textInputFocused, + required this.mediaViewerDistanceFromBottom, + required this.messageId, + required this.groupId, + required this.hide, + super.key, + }); + + final double mediaViewerDistanceFromBottom; + final bool show; + final bool textInputFocused; + final String messageId; + final String groupId; + final void Function() hide; + + @override + State createState() => _ReactionButtonsState(); +} + +class _ReactionButtonsState extends State { + int selectedShortReaction = -1; + + List selectedEmojis = + EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + final user = await getUser(); + if (user != null && user.preSelectedEmojies != null) { + selectedEmojis = user.preSelectedEmojies!; + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final firstRowEmojis = selectedEmojis.take(6).toList(); + final secondRowEmojis = + selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; + + return AnimatedPositioned( + duration: const Duration(milliseconds: 200), // Animation duration + bottom: widget.show + ? (widget.textInputFocused + ? 50 + : widget.mediaViewerDistanceFromBottom) + : widget.mediaViewerDistanceFromBottom - 20, + left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, + right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, + curve: Curves.linearToEaseOut, + child: AnimatedOpacity( + opacity: widget.show ? 1.0 : 0.0, // Fade in/out + duration: const Duration(milliseconds: 150), + child: Container( + color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, + padding: + widget.show ? const EdgeInsets.symmetric(vertical: 32) : null, + child: Column( + children: [ + if (secondRowEmojis.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: secondRowEmojis + .map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: widget.show, + emoji: emoji as String, + ), + ) + .toList(), + ), + if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: firstRowEmojis + .map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: widget.show, + emoji: emoji, + ), + ) + .toList(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index d4dbcac..2a0829c 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -10,8 +9,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart'; class StartNewChatView extends StatefulWidget { @@ -30,7 +29,7 @@ class _StartNewChatView extends State { void initState() { super.initState(); - final stream = twonlyDB.contactsDao.watchContactsForStartNewChat(); + final stream = twonlyDB.contactsDao.watchAllAcceptedContacts(); contactSub = stream.listen((update) async { update.sort( @@ -147,7 +146,6 @@ class UserList extends StatelessWidget { return const Divider(); } final user = users[i - 2]; - final flameCounter = getFlameCounterFromContact(user); return UserContextMenu( key: Key(user.userId.toString()), contact: user, @@ -155,40 +153,37 @@ class UserList extends StatelessWidget { title: Row( children: [ Text(getContactDisplayName(user)), - if (flameCounter >= 1) - FlameCounterWidget( - user, - flameCounter, - prefix: true, - ), - const Spacer(), - IconButton( - icon: FaIcon( - FontAwesomeIcons.boxOpen, - size: 13, - color: user.archived ? null : Colors.transparent, - ), - onPressed: user.archived - ? () async { - const update = - ContactsCompanion(archived: Value(false)); - await twonlyDB.contactsDao - .updateContact(user.userId, update); - } - : null, + FlameCounterWidget( + contactId: user.userId, + prefix: true, ), ], ), - leading: ContactAvatar( + leading: AvatarIcon( contact: user, fontSize: 13, ), onTap: () async { + var directChat = + await twonlyDB.groupsDao.getDirectChat(user.userId); + if (directChat == null) { + await twonlyDB.groupsDao.insertGroup( + GroupsCompanion( + isDirectChat: const Value(true), + groupName: Value( + getContactDisplayName(user), + ), + ), + ); + directChat = + await twonlyDB.groupsDao.getDirectChat(user.userId); + } + if (!context.mounted) return; await Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) { - return ChatMessagesView(user); + return ChatMessagesView(directChat!); }, ), ); diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart new file mode 100644 index 0000000..8e9706e --- /dev/null +++ b/lib/src/views/components/avatar_icon.component.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/src/utils/log.dart'; + +class AvatarIcon extends StatefulWidget { + const AvatarIcon({ + super.key, + this.group, + this.contact, + this.userData, + this.fontSize = 20, + this.color, + }); + final Group? group; + final Contact? contact; + final UserData? userData; + final double? fontSize; + final Color? color; + + @override + State createState() => _AvatarIconState(); +} + +class _AvatarIconState extends State { + List avatarSVGs = []; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + if (widget.group != null) { + final contacts = + await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); + if (contacts.length == 1) { + if (contacts.first.avatarSvg != null) { + avatarSVGs.add(contacts.first.avatarSvg!); + } + } else { + for (final contact in contacts) { + if (contact.avatarSvg != null) { + avatarSVGs.add(contact.avatarSvg!); + } + } + } + // avatarSvg = group!.avatarSvg; + } else if (widget.userData?.avatarSvg != null) { + avatarSVGs.add(widget.userData!.avatarSvg!); + } else if (widget.contact?.avatarSvg != null) { + avatarSVGs.add(widget.contact!.avatarSvg!); + } + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2); + + return Container( + constraints: BoxConstraints( + minHeight: 2 * (widget.fontSize ?? 20), + minWidth: 2 * (widget.fontSize ?? 20), + maxWidth: 2 * (widget.fontSize ?? 20), + maxHeight: 2 * (widget.fontSize ?? 20), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + height: proSize as double, + width: proSize, + color: widget.color, + child: Center( + child: avatarSVGs.isEmpty + ? SvgPicture.asset('assets/images/default_avatar.svg') + : SvgPicture.string( + avatarSVGs.first, + errorBuilder: (context, error, stackTrace) { + Log.error('$error'); + return Container(); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index b37c01c..7753e55 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -1,26 +1,65 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; -class FlameCounterWidget extends StatelessWidget { - const FlameCounterWidget( - this.user, - this.flameCounter, { +class FlameCounterWidget extends StatefulWidget { + const FlameCounterWidget({ + this.groupId, + this.contactId, this.prefix = false, super.key, }); - final Contact user; - final int flameCounter; + final String? groupId; + final int? contactId; final bool prefix; + @override + State createState() => _FlameCounterWidgetState(); +} + +class _FlameCounterWidgetState extends State { + int flameCounter = 0; + bool isBestFriend = false; + + StreamSubscription? flameCounterSub; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + flameCounterSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + var groupId = widget.groupId; + if (widget.groupId == null && widget.contactId != null) { + final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!); + groupId = group?.groupId; + } + if (groupId != null) { + isBestFriend = gUser.myBestFriendGroupId == groupId; + final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); + flameCounterSub = stream.listen((counter) { + setState(() { + flameCounter = counter; + }); + }); + } + } + @override Widget build(BuildContext context) { return Row( children: [ - if (prefix) const SizedBox(width: 5), - if (prefix) const Text('•'), - if (prefix) const SizedBox(width: 5), + if (widget.prefix) const SizedBox(width: 5), + if (widget.prefix) const Text('•'), + if (widget.prefix) const SizedBox(width: 5), Text( flameCounter.toString(), style: const TextStyle(fontSize: 13), @@ -28,7 +67,7 @@ class FlameCounterWidget extends StatelessWidget { SizedBox( height: 15, child: EmojiAnimation( - emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥', + emoji: isBestFriend ? '❤️‍🔥' : '🔥', ), ), ], diff --git a/lib/src/views/components/initialsavatar.dart b/lib/src/views/components/initialsavatar.dart deleted file mode 100644 index ec7ae11..0000000 --- a/lib/src/views/components/initialsavatar.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/utils/log.dart'; - -class ContactAvatar extends StatelessWidget { - const ContactAvatar({ - super.key, - this.contact, - this.userData, - this.fontSize = 20, - this.color, - }); - final Contact? contact; - final UserData? userData; - final double? fontSize; - final Color? color; - - @override - Widget build(BuildContext context) { - String? avatarSvg; - - if (contact != null) { - avatarSvg = contact!.avatarSvg; - } else if (userData != null) { - avatarSvg = userData!.avatarSvg; - } else { - return Container(); - } - - final proSize = (fontSize == null) ? 40 : (fontSize! * 2); - - return Container( - constraints: BoxConstraints( - minHeight: 2 * (fontSize ?? 20), - minWidth: 2 * (fontSize ?? 20), - maxWidth: 2 * (fontSize ?? 20), - maxHeight: 2 * (fontSize ?? 20), - ), - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - height: proSize as double, - width: proSize, - color: color, - child: Center( - child: avatarSvg == null - ? SvgPicture.asset('assets/images/default_avatar.svg') - : SvgPicture.string( - avatarSvg, - errorBuilder: (context, error, stackTrace) { - Log.error('$error'); - return Container(); - }, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/views/components/user_context_menu.component.dart b/lib/src/views/components/user_context_menu.component.dart index 46c8716..2d0f004 100644 --- a/lib/src/views/components/user_context_menu.component.dart +++ b/lib/src/views/components/user_context_menu.component.dart @@ -5,8 +5,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; -class UserContextMenuBlocked extends StatefulWidget { - const UserContextMenuBlocked({ +class UserContextMenu extends StatefulWidget { + const UserContextMenu({ required this.contact, required this.child, super.key, @@ -15,10 +15,10 @@ class UserContextMenuBlocked extends StatefulWidget { final Contact contact; @override - State createState() => _UserContextMenuBlocked(); + State createState() => _UserContextMenuBlocked(); } -class _UserContextMenuBlocked extends State { +class _UserContextMenuBlocked extends State { @override Widget build(BuildContext context) { return PieMenu( diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 20d6a46..7b51f36 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -7,9 +7,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; -import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; @@ -105,12 +104,12 @@ class _ContactViewState extends State { return Container(); } final contact = snapshot.data!; - final flameCounter = getFlameCounterFromContact(contact); + // final flameCounter = getFlameCounterFromContact(contact); return ListView( children: [ Padding( padding: const EdgeInsets.all(10), - child: ContactAvatar(contact: contact, fontSize: 30), + child: AvatarIcon(contact: contact, fontSize: 30), ), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -123,12 +122,12 @@ class _ContactViewState extends State { getContactDisplayName(contact), style: const TextStyle(fontSize: 20), ), - if (flameCounter > 0) - FlameCounterWidget( - contact, - flameCounter, - prefix: true, - ), + // if (flameCounter > 0) + // FlameCounterWidget( + // contact, + // flameCounter, + // prefix: true, + // ), ], ), if (getContactDisplayName(contact) != contact.username) diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart new file mode 100644 index 0000000..85aa283 --- /dev/null +++ b/lib/src/views/groups/group.view.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +class GroupView extends StatefulWidget { + const GroupView(this.group, {super.key}); + + final Group group; + + @override + State createState() => _GroupViewState(); +} + +class _GroupViewState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 5d9f1b7..55e298b 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -208,7 +208,8 @@ class _MemoriesPhotoSliderViewState extends State { minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, heroAttributes: PhotoViewHeroAttributes( - tag: item.mediaService.mediaFile.mediaId), + tag: item.mediaService.mediaFile.mediaId, + ), ) : PhotoViewGalleryPageOptions( imageProvider: FileImage(item.mediaService.storedPath), @@ -216,7 +217,8 @@ class _MemoriesPhotoSliderViewState extends State { minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, heroAttributes: PhotoViewHeroAttributes( - tag: item.mediaService.mediaFile.mediaId), + tag: item.mediaService.mediaFile.mediaId, + ), ); } } diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index d2b55ee..52e17d8 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -43,20 +43,18 @@ class _AutomatedTestingViewState extends State { await twonlyDB.contactsDao.getContactsByUsername(username); for (final contact in contacts) { - final groups = + final group = await twonlyDB.groupsDao.getDirectChat(contact.userId); - - for (final group in groups) { - for (var i = 0; i < 200; i++) { - setState(() { - lotsOfMessagesStatus = - 'At message $i to ${contact.username}.'; - }); - await insertAndSendTextMessage( - group.groupId, - 'Message $i.', - ); - } + 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/lib/src/views/settings/developer/developer.view.dart b/lib/src/views/settings/developer/developer.view.dart index d4df29f..efa8812 100644 --- a/lib/src/views/settings/developer/developer.view.dart +++ b/lib/src/views/settings/developer/developer.view.dart @@ -1,9 +1,6 @@ import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; @@ -69,14 +66,14 @@ class _DeveloperSettingsViewState extends State { ); }, ), - if (kDebugMode) - ListTile( - title: const Text('FlameSync Test'), - onTap: () async { - await twonlyDB.contactsDao.modifyFlameCounterForTesting(); - await syncFlameCounters(); - }, - ), + // if (kDebugMode) + // ListTile( + // title: const Text('FlameSync Test'), + // onTap: () async { + // await twonlyDB.contactsDao.modifyFlameCounterForTesting(); + // await syncFlameCounters(); + // }, + // ), if (kDebugMode) ListTile( title: const Text('Automated Testing'), diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 01c2b17..8b383f2 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -5,7 +5,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart'; class PrivacyViewBlockUsers extends StatefulWidget { @@ -108,7 +108,7 @@ class UserList extends StatelessWidget { itemCount: users.length, itemBuilder: (BuildContext context, int i) { final user = users[i]; - return UserContextMenuBlocked( + return UserContextMenu( contact: user, child: ListTile( title: Row( @@ -116,7 +116,7 @@ class UserList extends StatelessWidget { Text(getContactDisplayName(user)), ], ), - leading: ContactAvatar(contact: user, fontSize: 15), + leading: AvatarIcon(contact: user, fontSize: 15), trailing: Checkbox( value: user.blocked, onChanged: (bool? value) async { diff --git a/lib/src/views/settings/settings_main.view.dart b/lib/src/views/settings/settings_main.view.dart index b1ffe1c..c27b5a4 100644 --- a/lib/src/views/settings/settings_main.view.dart +++ b/lib/src/views/settings/settings_main.view.dart @@ -5,8 +5,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/settings/account.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart'; @@ -72,7 +72,7 @@ class _SettingsMainViewState extends State { color: context.color.surface.withAlpha(0), child: Row( children: [ - ContactAvatar( + AvatarIcon( userData: userData, fontSize: 30, ), From 18dd85d9374be242421347e483fb829719acfdc4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 25 Oct 2025 12:00:40 +0200 Subject: [PATCH 11/76] fixing uuid issue in tables --- ios/Podfile.lock | 10 +-- lib/app.dart | 78 +++++++++++++------ lib/main.dart | 8 +- lib/src/database/daos/groups.dao.dart | 7 +- lib/src/database/daos/mediafiles.dao.dart | 7 +- lib/src/database/daos/messages.dao.dart | 7 +- lib/src/database/daos/receipts.dao.dart | 7 +- lib/src/database/tables/groups.table.dart | 3 +- lib/src/database/tables/mediafiles.table.dart | 4 +- lib/src/database/tables/messages.table.dart | 3 +- lib/src/database/tables/receipts.table.dart | 3 +- lib/src/database/twonly.db.g.dart | 58 +++++++------- lib/src/model/json/userdata.dart | 4 - lib/src/model/json/userdata.g.dart | 2 - lib/src/services/api.service.dart | 2 +- .../create_backup.twonly_safe.dart | 2 +- lib/src/views/onboarding/register.view.dart | 1 - .../updates/62_database_migration.view.dart | 15 ++++ pubspec.lock | 76 +++++++++--------- 19 files changed, 173 insertions(+), 124 deletions(-) create mode 100644 lib/src/views/updates/62_database_migration.view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 42f70db..3d0a176 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -386,14 +386,14 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf @@ -401,14 +401,14 @@ SPEC CHECKSUMS: SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftProtobuf: 81e341191afbddd64aa031bd12862dccfab2f639 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_compress: f2133a07762889d67f0711ac831faa26f956980e - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 PODFILE CHECKSUM: 47470fbd5b59affa461eaf943ac57acce81e0ee8 diff --git a/lib/app.dart b/lib/app.dart index bc9f76b..acd726c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:path/path.dart' show join; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; @@ -11,6 +14,7 @@ import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart'; +import 'package:twonly/src/views/updates/62_database_migration.view.dart'; class App extends StatefulWidget { const App({super.key}); @@ -144,36 +148,60 @@ class AppMainWidget extends StatefulWidget { } class _AppMainWidgetState extends State { - Future userCreated = isUserCreated(); - bool showOnboarding = true; + bool _isUserCreated = false; + bool _showDatabaseMigration = false; + bool _showOnboarding = true; + bool _isLoaded = false; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + _showDatabaseMigration = File( + join( + (await getApplicationSupportDirectory()).path, + 'twonly_database.sqlite', + ), + ).existsSync(); + + _isUserCreated = await isUserCreated(); + setState(() { + _isLoaded = true; + }); + } @override Widget build(BuildContext context) { + if (!_isLoaded) { + return Center(child: Container()); + } + + late Widget child; + + if (_showDatabaseMigration) { + child = const DatabaseMigrationView(); + } else if (_isUserCreated) { + child = HomeView( + initialPage: widget.initialPage, + ); + } else if (_showOnboarding) { + child = OnboardingView( + callbackOnSuccess: () => setState(() { + _showOnboarding = false; + }), + ); + } else { + child = RegisterView( + callbackOnSuccess: initAsync, + ); + } + return Stack( children: [ - FutureBuilder( - future: userCreated, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center(child: Container()); - } else if (snapshot.data!) { - return HomeView( - initialPage: widget.initialPage, - ); - } else if (showOnboarding) { - return OnboardingView( - callbackOnSuccess: () => setState(() { - showOnboarding = false; - }), - ); - } - return RegisterView( - callbackOnSuccess: () => setState(() { - userCreated = isUserCreated(); - }), - ); - }, - ), + child, const AppOutdated(), ], ); diff --git a/lib/main.dart b/lib/main.dart index 19fee5c..c182e2c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,7 +10,6 @@ import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; -import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -27,9 +24,6 @@ void main() async { final user = await getUser(); if (user != null) { gUser = user; - if (user.isDemoUser) { - await deleteLocalUserData(); - } } final settingsController = SettingsChangeProvider(); @@ -37,7 +31,7 @@ void main() async { await settingsController.loadSettings(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - unawaited(setupPushNotification()); + // unawaited(setupPushNotification()); gCameras = await availableCameras(); diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index cfd2157..3de814b 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -33,7 +34,11 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future insertGroup(GroupsCompanion group) async { - await into(groups).insert(group); + await into(groups).insert( + group.copyWith( + groupId: Value(uuid.v4()), + ), + ); } Future> getGroupContact(String groupId) async { diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 872f73c..30e82a7 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; @@ -15,7 +16,11 @@ class MediaFilesDao extends DatabaseAccessor Future insertMedia(MediaFilesCompanion mediaFile) async { try { - final rowId = await into(mediaFiles).insert(mediaFile); + final rowId = await into(mediaFiles).insert( + mediaFile.copyWith( + mediaId: Value(uuid.v7()), + ), + ); return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) .getSingle(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 9a7829a..f892173 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; @@ -290,7 +291,11 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Future insertMessage(MessagesCompanion message) async { try { - final rowId = await into(messages).insert(message); + final rowId = await into(messages).insert( + message.copyWith( + messageId: Value(uuid.v7()), + ), + ); await twonlyDB.groupsDao.updateGroup( message.groupId.value, diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 01df2f9..03453e2 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -51,7 +52,11 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { Future insertReceipt(ReceiptsCompanion entry) async { try { - final id = await into(receipts).insert(entry); + final id = await into(receipts).insert( + entry.copyWith( + receiptId: Value(uuid.v4()), + ), + ); return await (select(receipts)..where((t) => t.rowId.equals(id))) .getSingle(); } catch (e) { diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 674f2ac..bd1ad71 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -1,10 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; @DataClassName('Group') class Groups extends Table { - TextColumn get groupId => text().clientDefault(() => uuid.v4())(); + TextColumn get groupId => text()(); BoolColumn get isGroupAdmin => boolean()(); BoolColumn get isDirectChat => boolean()(); diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 26b9a8d..15ba964 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -1,7 +1,5 @@ import 'dart:convert'; - import 'package:drift/drift.dart'; -import 'package:hashlib/random.dart'; enum MediaType { image, @@ -36,7 +34,7 @@ enum DownloadState { @DataClassName('MediaFile') class MediaFiles extends Table { - TextColumn get mediaId => text().clientDefault(() => uuid.v7())(); + TextColumn get mediaId => text()(); TextColumn get type => textEnum()(); diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 0e1c70d..cff2f6d 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -1,5 +1,4 @@ import 'package:drift/drift.dart'; -import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -10,7 +9,7 @@ enum MessageType { media, text } class Messages extends Table { TextColumn get groupId => text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); - TextColumn get messageId => text().clientDefault(() => uuid.v7())(); + TextColumn get messageId => text()(); // in case senderId is null, it was send by user itself IntColumn get senderId => diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart index c0fa4e6..997b208 100644 --- a/lib/src/database/tables/receipts.table.dart +++ b/lib/src/database/tables/receipts.table.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; -import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @DataClassName('Receipt') class Receipts extends Table { - TextColumn get receiptId => text().clientDefault(() => uuid.v4())(); + TextColumn get receiptId => text()(); IntColumn get contactId => integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index de4a722..0a25803 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -606,9 +606,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { @override late final GeneratedColumn groupId = GeneratedColumn( 'group_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v4()); + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _isGroupAdminMeta = const VerificationMeta('isGroupAdmin'); @override @@ -759,6 +757,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { if (data.containsKey('group_id')) { context.handle(_groupIdMeta, groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); } if (data.containsKey('is_group_admin')) { context.handle( @@ -1239,7 +1239,7 @@ class GroupsCompanion extends UpdateCompanion { this.rowid = const Value.absent(), }); GroupsCompanion.insert({ - this.groupId = const Value.absent(), + required String groupId, required bool isGroupAdmin, required bool isDirectChat, this.pinned = const Value.absent(), @@ -1256,7 +1256,8 @@ class GroupsCompanion extends UpdateCompanion { this.flameCounter = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.rowid = const Value.absent(), - }) : isGroupAdmin = Value(isGroupAdmin), + }) : groupId = Value(groupId), + isGroupAdmin = Value(isGroupAdmin), isDirectChat = Value(isDirectChat), groupName = Value(groupName); static Insertable custom({ @@ -1442,9 +1443,7 @@ class $MediaFilesTable extends MediaFiles @override late final GeneratedColumn mediaId = GeneratedColumn( 'media_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v7()); + type: DriftSqlType.string, requiredDuringInsert: true); @override late final GeneratedColumnWithTypeConverter type = GeneratedColumn('type', aliasedName, false, @@ -1566,6 +1565,8 @@ class $MediaFilesTable extends MediaFiles if (data.containsKey('media_id')) { context.handle(_mediaIdMeta, mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); + } else if (isInserting) { + context.missing(_mediaIdMeta); } if (data.containsKey('requires_authentication')) { context.handle( @@ -2020,7 +2021,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.rowid = const Value.absent(), }); MediaFilesCompanion.insert({ - this.mediaId = const Value.absent(), + required String mediaId, required MediaType type, this.uploadState = const Value.absent(), this.downloadState = const Value.absent(), @@ -2035,7 +2036,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : type = Value(type); + }) : mediaId = Value(mediaId), + type = Value(type); static Insertable custom({ Expression? mediaId, Expression? type, @@ -2212,9 +2214,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { @override late final GeneratedColumn messageId = GeneratedColumn( 'message_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v7()); + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _senderIdMeta = const VerificationMeta('senderId'); @override @@ -2334,6 +2334,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { if (data.containsKey('message_id')) { context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); } if (data.containsKey('sender_id')) { context.handle(_senderIdMeta, @@ -2714,7 +2716,7 @@ class MessagesCompanion extends UpdateCompanion { }); MessagesCompanion.insert({ required String groupId, - this.messageId = const Value.absent(), + required String messageId, this.senderId = const Value.absent(), required MessageType type, this.content = const Value.absent(), @@ -2728,6 +2730,7 @@ class MessagesCompanion extends UpdateCompanion { this.modifiedAt = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), + messageId = Value(messageId), type = Value(type); static Insertable custom({ Expression? groupId, @@ -3744,9 +3747,7 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { @override late final GeneratedColumn receiptId = GeneratedColumn( 'receipt_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - clientDefault: () => uuid.v4()); + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _contactIdMeta = const VerificationMeta('contactId'); @override @@ -3834,6 +3835,8 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { if (data.containsKey('receipt_id')) { context.handle(_receiptIdMeta, receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta)); + } else if (isInserting) { + context.missing(_receiptIdMeta); } if (data.containsKey('contact_id')) { context.handle(_contactIdMeta, @@ -4119,7 +4122,7 @@ class ReceiptsCompanion extends UpdateCompanion { this.rowid = const Value.absent(), }); ReceiptsCompanion.insert({ - this.receiptId = const Value.absent(), + required String receiptId, required int contactId, this.messageId = const Value.absent(), required Uint8List message, @@ -4129,7 +4132,8 @@ class ReceiptsCompanion extends UpdateCompanion { this.lastRetry = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : contactId = Value(contactId), + }) : receiptId = Value(receiptId), + contactId = Value(contactId), message = Value(message); static Insertable custom({ Expression? receiptId, @@ -6992,7 +6996,7 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< bool signalContactPreKeysRefs, bool signalContactSignedPreKeysRefs})>; typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ - Value groupId, + required String groupId, required bool isGroupAdmin, required bool isDirectChat, Value pinned, @@ -7346,7 +7350,7 @@ class $$GroupsTableTableManager extends RootTableManager< rowid: rowid, ), createCompanionCallback: ({ - Value groupId = const Value.absent(), + required String groupId, required bool isGroupAdmin, required bool isDirectChat, Value pinned = const Value.absent(), @@ -7425,7 +7429,7 @@ typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< Group, PrefetchHooks Function({bool messagesRefs})>; typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ - Value mediaId, + required String mediaId, required MediaType type, Value uploadState, Value downloadState, @@ -7758,7 +7762,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< rowid: rowid, ), createCompanionCallback: ({ - Value mediaId = const Value.absent(), + required String mediaId, required MediaType type, Value uploadState = const Value.absent(), Value downloadState = const Value.absent(), @@ -7838,7 +7842,7 @@ typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< PrefetchHooks Function({bool messagesRefs})>; typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String groupId, - Value messageId, + required String messageId, Value senderId, required MessageType type, Value content, @@ -8587,7 +8591,7 @@ class $$MessagesTableTableManager extends RootTableManager< ), createCompanionCallback: ({ required String groupId, - Value messageId = const Value.absent(), + required String messageId, Value senderId = const Value.absent(), required MessageType type, Value content = const Value.absent(), @@ -9647,7 +9651,7 @@ typedef $$GroupMembersTableProcessedTableManager = ProcessedTableManager< GroupMember, PrefetchHooks Function({bool contactId})>; typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({ - Value receiptId, + required String receiptId, required int contactId, Value messageId, required Uint8List message, @@ -9969,7 +9973,7 @@ class $$ReceiptsTableTableManager extends RootTableManager< rowid: rowid, ), createCompanionCallback: ({ - Value receiptId = const Value.absent(), + required String receiptId, required int contactId, Value messageId = const Value.absent(), required Uint8List message, diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 00614b0..98a9cd9 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -9,16 +9,12 @@ class UserData { required this.username, required this.displayName, required this.subscriptionPlan, - required this.isDemoUser, }); factory UserData.fromJson(Map json) => _$UserDataFromJson(json); final int userId; - @JsonKey(defaultValue: false) - bool isDemoUser = false; - // -- USER PROFILE -- String username; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index dd475e8..9336cdf 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -11,7 +11,6 @@ UserData _$UserDataFromJson(Map json) => UserData( username: json['username'] as String, displayName: json['displayName'] as String, subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', - isDemoUser: json['isDemoUser'] as bool? ?? false, ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? @@ -74,7 +73,6 @@ UserData _$UserDataFromJson(Map json) => UserData( Map _$UserDataToJson(UserData instance) => { 'userId': instance.userId, - 'isDemoUser': instance.isDemoUser, 'username': instance.username, 'displayName': instance.displayName, 'avatarSvg': instance.avatarSvg, diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index a30e8cc..79a9dc3 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -157,7 +157,7 @@ class ApiService { reconnectionTimer?.cancel(); reconnectionTimer = null; final user = await getUser(); - if (user != null && user.isDemoUser) { + if (user != null) { globalCallbackConnectionState(isConnected: true); return false; } diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 138c623..42bc911 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -23,7 +23,7 @@ import 'package:twonly/src/views/settings/backup/backup.view.dart'; Future performTwonlySafeBackup({bool force = false}) async { final user = await getUser(); - if (user == null || user.twonlySafeBackup == null || user.isDemoUser) { + if (user == null || user.twonlySafeBackup == null) { return; } diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 8e8d2af..7f6dd27 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -84,7 +84,6 @@ class _RegisterViewState extends State { username: username, displayName: username, subscriptionPlan: 'Preview', - isDemoUser: false, ); await const FlutterSecureStorage() diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart new file mode 100644 index 0000000..6e8e663 --- /dev/null +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class DatabaseMigrationView extends StatefulWidget { + const DatabaseMigrationView({super.key}); + + @override + State createState() => _DatabaseMigrationViewState(); +} + +class _DatabaseMigrationViewState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7c121f6..99827f9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,18 +101,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.0" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "4e54dbeefdc70691ba80b3bce3976af63b5425c8c07dface348dfee664a0edc1" + sha256: a9461b8e586bf018dd4afd2e13b49b08c6a844a4b226c8d1d10f3a723cdd78c3 url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.1" built_collection: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: camera - sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + sha256: "87a27e0553e3432119c1c2f6e4b9a1bbf7d2c660552b910bfa59185a9facd632" url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.2+1" camera_android_camerax: dependency: "direct overridden" description: @@ -174,10 +174,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "397f44f8a63c8c0a474668d500f9739d4f2bc45ac2b21801194b7d29260f03ee" + sha256: "34bcd5db30e52414f1f0783c5e3f566909fab14141a21b3b576c78bd35382bf6" url: "https://pub.dev" source: hosted - version: "0.9.22+1" + version: "0.9.22+4" camera_platform_interface: dependency: transitive description: @@ -334,10 +334,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6" + sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "12.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -358,18 +358,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1" + sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114" url: "https://pub.dev" source: hosted - version: "2.28.2" + version: "2.29.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "4db0eeedc7e8bed117a9f22d867ab7a3a294300fed5c269aac90d0b3545967ca" + sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43" url: "https://pub.dev" source: hosted - version: "2.28.3" + version: "2.29.0" drift_flutter: dependency: "direct main" description: @@ -422,10 +422,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2" url: "https://pub.dev" source: hosted - version: "0.9.4+4" + version: "0.9.4+5" file_selector_platform_interface: dependency: transitive description: @@ -738,10 +738,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: "27af5982e6c510dec1ba038eff634fa284676ee84e3fd807225c80c4ad869177" + sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 url: "https://pub.dev" source: hosted - version: "10.10.0" + version: "10.12.0" gal: dependency: "direct main" description: @@ -866,10 +866,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 url: "https://pub.dev" source: hosted - version: "0.8.13" + version: "0.8.13+1" image_picker_linux: dependency: transitive description: @@ -882,18 +882,18 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.2+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.11.1" image_picker_windows: dependency: transitive description: @@ -1186,10 +1186,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" path_provider_linux: dependency: transitive description: @@ -1403,10 +1403,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6" + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" share_plus_platform_interface: dependency: transitive description: @@ -1435,10 +1435,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.5" shared_preferences_linux: dependency: transitive description: @@ -1584,10 +1584,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67" + sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c" url: "https://pub.dev" source: hosted - version: "0.41.2" + version: "0.42.0" stack_trace: dependency: transitive description: @@ -1688,10 +1688,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" url: "https://pub.dev" source: hosted - version: "6.3.4" + version: "6.3.5" url_launcher_linux: dependency: transitive description: @@ -1704,10 +1704,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.4" url_launcher_platform_interface: dependency: transitive description: @@ -1808,10 +1808,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd + sha256: "19ed1162a7a5520e7d7791e0b7b73ba03161b6a69428b82e4689e435b325432d" url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.8.5" video_player_platform_interface: dependency: transitive description: From 4cb7f0ab019f0341850bd64abe9a5d1a3400bc2b Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 25 Oct 2025 12:12:44 +0200 Subject: [PATCH 12/76] fixing unit tests --- lib/src/utils/misc.dart | 2 ++ test/drift/twonly_database/migration_test.dart | 6 +++--- test/unit_test.dart | 16 ---------------- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index a1fa18a..79848fd 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -248,6 +248,8 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { bool isUUIDNewer(String uuid1, String uuid2) { final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); + print(timestamp1); + print(timestamp2); return timestamp1 > timestamp2; } diff --git a/test/drift/twonly_database/migration_test.dart b/test/drift/twonly_database/migration_test.dart index ee37ec9..50fc215 100644 --- a/test/drift/twonly_database/migration_test.dart +++ b/test/drift/twonly_database/migration_test.dart @@ -2,7 +2,7 @@ // ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations_native.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly_database_old.dart'; import 'package:flutter_test/flutter_test.dart'; import 'generated/schema.dart'; @@ -27,7 +27,7 @@ void main() { for (final toVersion in versions.skip(i + 1)) { test('to $toVersion', () async { final schema = await verifier.schemaAt(fromVersion); - final db = TwonlyDatabase(schema.newConnection()); + final db = TwonlyDatabaseOld(schema.newConnection()); await verifier.migrateAndValidate(db, toVersion); await db.close(); }); @@ -70,7 +70,7 @@ void main() { newVersion: 2, createOld: v1.DatabaseAtV1.new, createNew: v2.DatabaseAtV2.new, - openTestedDatabase: TwonlyDatabase.new, + openTestedDatabase: TwonlyDatabaseOld.new, createItems: (batch, oldDb) { batch.insertAll(oldDb.contacts, oldContactsData); batch.insertAll(oldDb.messages, oldMessagesData); diff --git a/test/unit_test.dart b/test/unit_test.dart index 603e6ed..3466630 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,8 +1,5 @@ -import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hashlib/random.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -24,18 +21,5 @@ void main() { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1))); }); - - test('encoding uuid4', () async { - final uv4 = uuid.v4(); - final uv4Bytes = Uint8List.fromList(uv4.codeUnits); - final uv4String = utf8.decode(uv4Bytes.cast()); - expect(uv4String, uv4); - }); - test('comparing uui7', () async { - final uv7Old = uuid.v7(); - sleep(const Duration(milliseconds: 1000)); - final uv7New = uuid.v7(); - expect(isUUIDNewer(uv7New, uv7Old), true); - }); }); } From 5ae943bcf301a7df28731a3b596657e72d4ef71a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 01:14:46 +0200 Subject: [PATCH 13/76] message and media sending does work --- lib/app.dart | 17 +- lib/main.dart | 12 + lib/src/database/daos/contacts.dao.dart | 17 + lib/src/database/daos/groups.dao.dart | 47 +- lib/src/database/daos/mediafiles.dao.dart | 25 +- lib/src/database/daos/messages.dao.dart | 87 ++-- lib/src/database/daos/receipts.dao.dart | 34 +- lib/src/database/daos/receipts.dao.g.dart | 2 + lib/src/database/daos/signal.dao.dart | 3 +- .../signal/connect_pre_key_store.dart | 7 +- lib/src/database/tables/messages.table.dart | 2 + lib/src/database/tables/receipts.table.dart | 10 + lib/src/database/twonly.db.dart | 1 + lib/src/database/twonly.db.g.dart | 455 +++++++++++++++++- lib/src/model/json/userdata.dart | 3 + lib/src/model/json/userdata.g.dart | 2 + lib/src/model/memory_item.model.dart | 4 + .../client/generated/messages.pb.dart | 26 +- .../client/generated/messages.pbjson.dart | 48 +- lib/src/model/protobuf/client/messages.proto | 4 +- lib/src/services/api.service.dart | 5 - .../api/mediafiles/download.service.dart | 6 +- .../mediafiles/media_background.service.dart | 8 +- .../api/mediafiles/upload.service.dart | 70 ++- lib/src/services/api/messages.dart | 42 +- lib/src/services/api/server_messages.dart | 57 ++- .../media.server_messages.dart | 26 +- .../messages.server_messages.dart | 27 +- .../text_message.server_messages.dart | 2 + lib/src/services/api/utils.dart | 4 +- .../mediafiles/mediafile.service.dart | 24 +- .../notifications/pushkeys.notifications.dart | 8 +- lib/src/services/signal/prekeys.signal.dart | 7 +- lib/src/utils/misc.dart | 45 +- .../save_to_gallery.dart | 10 +- .../views/camera/share_image_editor_view.dart | 10 +- lib/src/views/chats/chat_list.view.dart | 51 +- .../chat_list_components/group_list_item.dart | 67 ++- lib/src/views/chats/chat_messages.view.dart | 114 ++--- .../chat_list_entry.dart | 22 +- .../chat_media_entry.dart | 17 +- .../in_chat_media_viewer.dart | 6 +- .../message_context_menu.dart | 1 + .../message_send_state_icon.dart | 128 ++--- lib/src/views/chats/media_viewer.view.dart | 20 +- .../emoji_reactions_row.component.dart | 1 + lib/src/views/chats/start_new_chat.view.dart | 4 +- lib/src/views/memories/memories.view.dart | 10 +- .../memories/memories_item_thumbnail.dart | 10 +- lib/src/views/onboarding/register.view.dart | 2 +- .../developer/automated_testing.view.dart | 12 +- .../developer/retransmission_data.view.dart | 44 ++ .../updates/62_database_migration.view.dart | 436 ++++++++++++++++- test/unit_test.dart | 39 ++ 54 files changed, 1712 insertions(+), 429 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index acd726c..14e339b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,9 +1,6 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:path/path.dart' show join; -import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; @@ -160,14 +157,14 @@ class _AppMainWidgetState extends State { } Future initAsync() async { - _showDatabaseMigration = File( - join( - (await getApplicationSupportDirectory()).path, - 'twonly_database.sqlite', - ), - ).existsSync(); - _isUserCreated = await isUserCreated(); + + if (_isUserCreated) { + if (gUser.appVersion < 62) { + _showDatabaseMigration = true; + } + } + setState(() { _isLoaded = true; }); diff --git a/lib/main.dart b/lib/main.dart index c182e2c..ce9f360 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,9 @@ +import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -35,6 +38,15 @@ void main() async { gCameras = await availableCameras(); + // try { + // File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite')) + // .deleteSync(); + // } catch (e) {} + // await updateUserdata((u) { + // u.appVersion = 0; + // return u; + // }); + apiService = ApiService(); twonlyDB = TwonlyDB(); diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index de674dc..898297c 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/database/twonly_database_old.dart' as old; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; part 'contacts.dao.g.dart'; @@ -111,6 +112,22 @@ String getContactDisplayName(Contact user) { return name; } +String getContactDisplayNameOld(old.Contact user) { + var name = user.username; + if (user.nickName != null && user.nickName != '') { + name = user.nickName!; + } else if (user.displayName != null) { + name = user.displayName!; + } + if (user.deleted) { + name = applyStrikethrough(name); + } + if (name.length > 12) { + return '${name.substring(0, 12)}...'; + } + return name; +} + String applyStrikethrough(String text) { return text.split('').map((char) => '$char\u0336').join(); } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 3de814b..92c24cd 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -1,7 +1,10 @@ import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; part 'groups.dao.g.dart'; @@ -33,12 +36,46 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .get(); } - Future insertGroup(GroupsCompanion group) async { - await into(groups).insert( - group.copyWith( - groupId: Value(uuid.v4()), - ), + Future createNewGroup(GroupsCompanion group) async { + final insertGroup = group.copyWith( + groupId: Value(uuid.v4()), + isGroupAdmin: const Value(true), ); + return _insertGroup(insertGroup); + } + + Future createNewDirectChat( + int contactId, + GroupsCompanion group, + ) async { + final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId); + final insertGroup = group.copyWith( + groupId: Value(groupIdDirectChat), + isDirectChat: const Value(true), + isGroupAdmin: const Value(true), + ); + + final result = await _insertGroup(insertGroup); + if (result != null) { + await into(groupMembers).insert(GroupMembersCompanion( + groupId: Value(result.groupId), + contactId: Value( + contactId, + ), + )); + } + return result; + } + + Future _insertGroup(GroupsCompanion group) async { + try { + final rowId = await into(groups).insert(group); + return await (select(groups)..where((t) => t.rowId.equals(rowId))) + .getSingle(); + } catch (e) { + Log.error('Could not insert group: $e'); + return null; + } } Future> getGroupContact(String groupId) async { diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 30e82a7..cc01ca9 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -16,11 +16,15 @@ class MediaFilesDao extends DatabaseAccessor Future insertMedia(MediaFilesCompanion mediaFile) async { try { - final rowId = await into(mediaFiles).insert( - mediaFile.copyWith( + var insertMediaFile = mediaFile; + + if (insertMediaFile.mediaId == const Value.absent()) { + insertMediaFile = mediaFile.copyWith( mediaId: Value(uuid.v7()), - ), - ); + ); + } + + final rowId = await into(mediaFiles).insert(insertMediaFile); return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) .getSingle(); @@ -72,11 +76,22 @@ class MediaFilesDao extends DatabaseAccessor Future> getAllMediaFilesPendingDownload() async { return (select(mediaFiles) - ..where((t) => t.downloadState.equals(DownloadState.pending.name))) + ..where( + (t) => + t.downloadState.equals(DownloadState.pending.name) | + t.downloadState.equals(DownloadState.downloading.name), + )) .get(); } Stream> watchAllStoredMediaFiles() { return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); } + + Stream> watchNewestMediaFiles() { + return (select(mediaFiles) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(100)) + .watch(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index f892173..e3e2d43 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -38,24 +38,28 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Stream> watchMediaNotOpened(String groupId) { - return (select(messages) - ..where( - (t) => - t.openedAt.isNull() & - t.groupId.equals(groupId) & - t.senderId.isNotNull() & - t.type.equals(MessageType.media.name), - ) - ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) - .watch(); + final query = select(messages).join([ + leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)), + ]) + ..where( + mediaFiles.downloadState + .equals(DownloadState.reuploadRequested.name) + .not() & + messages.openedAt.isNull() & + messages.groupId.equals(groupId) & + messages.mediaId.isNotNull() & + messages.senderId.isNotNull() & + messages.type.equals(MessageType.media.name), + ); + return query.map((row) => row.readTable(messages)).watch(); } - Stream> watchLastMessage(String groupId) { + Stream watchLastMessage(String groupId) { return (select(messages) ..where((t) => t.groupId.equals(groupId)) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) ..limit(1)) - .watch(); + .watchSingleOrNull(); } Stream> watchByGroupId(String groupId) { @@ -64,6 +68,16 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .watch(); } + Stream> watchMessageActionChanges(String messageId) { + return (select(messageActions)..where((t) => t.messageId.equals(messageId))) + .watch(); + } + + Stream watchMessageById(String messageId) { + return (select(messages)..where((t) => t.messageId.equals(messageId))) + .watchSingleOrNull(); + } + // Future removeOldMessages() { // return (update(messages) // ..where( @@ -206,14 +220,20 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { String messageId, DateTime timestamp, ) async { - await into(messageActions).insert( + await into(messageActions).insertOnConflictUpdate( MessageActionsCompanion( messageId: Value(messageId), contactId: Value(contactId), - type: const Value(MessageActionType.ackByUserAt), + type: const Value(MessageActionType.openedAt), actionAt: Value(timestamp), ), ); + if (await haveAllMembers(messageId, MessageActionType.openedAt)) { + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(openedAt: Value(DateTime.now())), + ); + } } Future handleMessageAckByServer( @@ -221,7 +241,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { String messageId, DateTime timestamp, ) async { - await into(messageActions).insert( + await into(messageActions).insertOnConflictUpdate( MessageActionsCompanion( messageId: Value(messageId), contactId: Value(contactId), @@ -229,14 +249,22 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { actionAt: Value(timestamp), ), ); + if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) { + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(ackByServer: Value(DateTime.now())), + ); + } } Future haveAllMembers( - String groupId, String messageId, MessageActionType action, ) async { - final members = await twonlyDB.groupsDao.getGroupMembers(groupId); + final message = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (message == null) return true; + final members = await twonlyDB.groupsDao.getGroupMembers(message.groupId); final actions = await (select(messageActions) ..where( @@ -291,11 +319,15 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Future insertMessage(MessagesCompanion message) async { try { - final rowId = await into(messages).insert( - message.copyWith( + var insertMessage = message; + + if (message.messageId == const Value.absent()) { + insertMessage = message.copyWith( messageId: Value(uuid.v7()), - ), - ); + ); + } + + final rowId = await into(messages).insert(insertMessage); await twonlyDB.groupsDao.updateGroup( message.groupId.value, @@ -323,19 +355,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .getSingleOrNull(); } - Future reopenedMedia(String messageId) async { - await (delete(messageActions) - ..where( - (t) => - t.messageId.equals(messageId) & - t.contactId.isNull() & - t.type.equals( - MessageActionType.openedAt.name, - ), - )) - .go(); - } - // Future deleteMessagesByContactId(int contactId) { // return (delete(messages) // ..where( diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 03453e2..ecbfeee 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart'; part 'receipts.dao.g.dart'; -@DriftAccessor(tables: [Receipts, Messages, MessageActions]) +@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts]) class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -52,11 +52,13 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { Future insertReceipt(ReceiptsCompanion entry) async { try { - final id = await into(receipts).insert( - entry.copyWith( + var insertEntry = entry; + if (entry.receiptId == const Value.absent()) { + insertEntry = entry.copyWith( receiptId: Value(uuid.v4()), - ), - ); + ); + } + final id = await into(receipts).insert(insertEntry); return await (select(receipts)..where((t) => t.rowId.equals(id))) .getSingle(); } catch (e) { @@ -97,4 +99,26 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { await (update(receipts)..where((c) => c.receiptId.equals(receiptId))) .write(updates); } + + Future isDuplicated(String receiptId) async { + return await (select(receivedReceipts) + ..where((t) => t.receiptId.equals(receiptId))) + .getSingleOrNull() != + null; + // try { + // return await (select() + // ..where( + // (t) => t.receiptId.equals(receiptId), + // )) + // .getSingleOrNull(); + // } catch (e) { + // Log.error(e); + // return null; + // } + } + + Future gotReceipt(String receiptId) async { + await into(receivedReceipts) + .insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId))); + } } diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart index d495737..4230aa8 100644 --- a/lib/src/database/daos/receipts.dao.g.dart +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -10,4 +10,6 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor { $MessagesTable get messages => attachedDatabase.messages; $ReceiptsTable get receipts => attachedDatabase.receipts; $MessageActionsTable get messageActions => attachedDatabase.messageActions; + $ReceivedReceiptsTable get receivedReceipts => + attachedDatabase.receivedReceipts; } diff --git a/lib/src/database/daos/signal.dao.dart b/lib/src/database/daos/signal.dao.dart index 4458f50..2b64aac 100644 --- a/lib/src/database/daos/signal.dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -57,7 +57,7 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { tbl.preKeyId.equals(preKey.preKeyId), )) .go(); - Log.info('Using prekey ${preKey.preKeyId} for $contactId'); + Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId'); return preKey; } return null; @@ -68,6 +68,7 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { List preKeys, ) async { for (final preKey in preKeys) { + Log.info('[PREKEY] Inserting others ${preKey.preKeyId}'); try { await into(signalContactPreKeys).insert(preKey); } catch (e) { diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 1c3da47..18fd5e6 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -19,15 +19,17 @@ class ConnectPreKeyStore extends PreKeyStore { ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .get(); if (preKeyRecord.isEmpty) { - throw InvalidKeyIdException('No such preKey record! - $preKeyId'); + throw InvalidKeyIdException( + '[PREKEY] No such preKey record! - $preKeyId'); } - Log.info('Contact used preKey $preKeyId'); + Log.info('[PREKEY] Contact used my preKey $preKeyId'); final preKey = preKeyRecord.first.preKey; return PreKeyRecord.fromBuffer(preKey); } @override Future removePreKey(int preKeyId) async { + Log.info('[PREKEY] Removing $preKeyId from my own storage.'); await (twonlyDB.delete(twonlyDB.signalPreKeyStores) ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .go(); @@ -40,6 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore { preKey: Value(record.serialize()), ); + Log.info('[PREKEY] Storing $preKeyId from my own storage.'); try { await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); } catch (e) { diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index cff2f6d..cf18f12 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -35,6 +35,8 @@ class Messages extends Table { DateTimeColumn get openedAt => dateTime().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get modifiedAt => dateTime().nullable()(); + DateTimeColumn get ackByUser => dateTime().nullable()(); + DateTimeColumn get ackByServer => dateTime().nullable()(); @override Set get primaryKey => {messageId}; diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart index 997b208..77a76e4 100644 --- a/lib/src/database/tables/receipts.table.dart +++ b/lib/src/database/tables/receipts.table.dart @@ -30,3 +30,13 @@ class Receipts extends Table { @override Set get primaryKey => {receiptId}; } + +@DataClassName('ReceivedReceipt') +class ReceivedReceipts extends Table { + TextColumn get receiptId => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {receiptId}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 22e3907..b5681f2 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -36,6 +36,7 @@ part 'twonly.db.g.dart'; Groups, GroupMembers, Receipts, + ReceivedReceipts, SignalIdentityKeyStores, SignalPreKeyStores, SignalSenderKeyStores, diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 0a25803..c88339a 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2299,6 +2299,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { late final GeneratedColumn modifiedAt = GeneratedColumn( 'modified_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _ackByUserMeta = + const VerificationMeta('ackByUser'); + @override + late final GeneratedColumn ackByUser = GeneratedColumn( + 'ack_by_user', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _ackByServerMeta = + const VerificationMeta('ackByServer'); + @override + late final GeneratedColumn ackByServer = GeneratedColumn( + 'ack_by_server', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); @override List get $columns => [ groupId, @@ -2313,7 +2325,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { isDeletedFromSender, openedAt, createdAt, - modifiedAt + modifiedAt, + ackByUser, + ackByServer ]; @override String get aliasedName => _alias ?? actualTableName; @@ -2387,6 +2401,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { modifiedAt.isAcceptableOrUnknown( data['modified_at']!, _modifiedAtMeta)); } + if (data.containsKey('ack_by_user')) { + context.handle( + _ackByUserMeta, + ackByUser.isAcceptableOrUnknown( + data['ack_by_user']!, _ackByUserMeta)); + } + if (data.containsKey('ack_by_server')) { + context.handle( + _ackByServerMeta, + ackByServer.isAcceptableOrUnknown( + data['ack_by_server']!, _ackByServerMeta)); + } return context; } @@ -2422,6 +2448,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, modifiedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), + ackByUser: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_user']), + ackByServer: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server']), ); } @@ -2448,6 +2478,8 @@ class Message extends DataClass implements Insertable { final DateTime? openedAt; final DateTime createdAt; final DateTime? modifiedAt; + final DateTime? ackByUser; + final DateTime? ackByServer; const Message( {required this.groupId, required this.messageId, @@ -2461,7 +2493,9 @@ class Message extends DataClass implements Insertable { required this.isDeletedFromSender, this.openedAt, required this.createdAt, - this.modifiedAt}); + this.modifiedAt, + this.ackByUser, + this.ackByServer}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2494,6 +2528,12 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || modifiedAt != null) { map['modified_at'] = Variable(modifiedAt); } + if (!nullToAbsent || ackByUser != null) { + map['ack_by_user'] = Variable(ackByUser); + } + if (!nullToAbsent || ackByServer != null) { + map['ack_by_server'] = Variable(ackByServer); + } return map; } @@ -2526,6 +2566,12 @@ class Message extends DataClass implements Insertable { modifiedAt: modifiedAt == null && nullToAbsent ? const Value.absent() : Value(modifiedAt), + ackByUser: ackByUser == null && nullToAbsent + ? const Value.absent() + : Value(ackByUser), + ackByServer: ackByServer == null && nullToAbsent + ? const Value.absent() + : Value(ackByServer), ); } @@ -2548,6 +2594,8 @@ class Message extends DataClass implements Insertable { openedAt: serializer.fromJson(json['openedAt']), createdAt: serializer.fromJson(json['createdAt']), modifiedAt: serializer.fromJson(json['modifiedAt']), + ackByUser: serializer.fromJson(json['ackByUser']), + ackByServer: serializer.fromJson(json['ackByServer']), ); } @override @@ -2568,6 +2616,8 @@ class Message extends DataClass implements Insertable { 'openedAt': serializer.toJson(openedAt), 'createdAt': serializer.toJson(createdAt), 'modifiedAt': serializer.toJson(modifiedAt), + 'ackByUser': serializer.toJson(ackByUser), + 'ackByServer': serializer.toJson(ackByServer), }; } @@ -2584,7 +2634,9 @@ class Message extends DataClass implements Insertable { bool? isDeletedFromSender, Value openedAt = const Value.absent(), DateTime? createdAt, - Value modifiedAt = const Value.absent()}) => + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent()}) => Message( groupId: groupId ?? this.groupId, messageId: messageId ?? this.messageId, @@ -2602,6 +2654,8 @@ class Message extends DataClass implements Insertable { openedAt: openedAt.present ? openedAt.value : this.openedAt, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, + ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer, ); Message copyWithCompanion(MessagesCompanion data) { return Message( @@ -2626,6 +2680,9 @@ class Message extends DataClass implements Insertable { createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, modifiedAt: data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: + data.ackByServer.present ? data.ackByServer.value : this.ackByServer, ); } @@ -2644,7 +2701,9 @@ class Message extends DataClass implements Insertable { ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') - ..write('modifiedAt: $modifiedAt') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer') ..write(')')) .toString(); } @@ -2663,7 +2722,9 @@ class Message extends DataClass implements Insertable { isDeletedFromSender, openedAt, createdAt, - modifiedAt); + modifiedAt, + ackByUser, + ackByServer); @override bool operator ==(Object other) => identical(this, other) || @@ -2680,7 +2741,9 @@ class Message extends DataClass implements Insertable { other.isDeletedFromSender == this.isDeletedFromSender && other.openedAt == this.openedAt && other.createdAt == this.createdAt && - other.modifiedAt == this.modifiedAt); + other.modifiedAt == this.modifiedAt && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer); } class MessagesCompanion extends UpdateCompanion { @@ -2697,6 +2760,8 @@ class MessagesCompanion extends UpdateCompanion { final Value openedAt; final Value createdAt; final Value modifiedAt; + final Value ackByUser; + final Value ackByServer; final Value rowid; const MessagesCompanion({ this.groupId = const Value.absent(), @@ -2712,6 +2777,8 @@ class MessagesCompanion extends UpdateCompanion { this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), this.rowid = const Value.absent(), }); MessagesCompanion.insert({ @@ -2728,6 +2795,8 @@ class MessagesCompanion extends UpdateCompanion { this.openedAt = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), messageId = Value(messageId), @@ -2746,6 +2815,8 @@ class MessagesCompanion extends UpdateCompanion { Expression? openedAt, Expression? createdAt, Expression? modifiedAt, + Expression? ackByUser, + Expression? ackByServer, Expression? rowid, }) { return RawValuesInsertable({ @@ -2763,6 +2834,8 @@ class MessagesCompanion extends UpdateCompanion { if (openedAt != null) 'opened_at': openedAt, if (createdAt != null) 'created_at': createdAt, if (modifiedAt != null) 'modified_at': modifiedAt, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, if (rowid != null) 'rowid': rowid, }); } @@ -2781,6 +2854,8 @@ class MessagesCompanion extends UpdateCompanion { Value? openedAt, Value? createdAt, Value? modifiedAt, + Value? ackByUser, + Value? ackByServer, Value? rowid}) { return MessagesCompanion( groupId: groupId ?? this.groupId, @@ -2796,6 +2871,8 @@ class MessagesCompanion extends UpdateCompanion { openedAt: openedAt ?? this.openedAt, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt ?? this.modifiedAt, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, rowid: rowid ?? this.rowid, ); } @@ -2843,6 +2920,12 @@ class MessagesCompanion extends UpdateCompanion { if (modifiedAt.present) { map['modified_at'] = Variable(modifiedAt.value); } + if (ackByUser.present) { + map['ack_by_user'] = Variable(ackByUser.value); + } + if (ackByServer.present) { + map['ack_by_server'] = Variable(ackByServer.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2865,6 +2948,8 @@ class MessagesCompanion extends UpdateCompanion { ..write('openedAt: $openedAt, ') ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -4243,6 +4328,200 @@ class ReceiptsCompanion extends UpdateCompanion { } } +class $ReceivedReceiptsTable extends ReceivedReceipts + with TableInfo<$ReceivedReceiptsTable, ReceivedReceipt> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReceivedReceiptsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _receiptIdMeta = + const VerificationMeta('receiptId'); + @override + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [receiptId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'received_receipts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('receipt_id')) { + context.handle(_receiptIdMeta, + receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta)); + } else if (isInserting) { + context.missing(_receiptIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {receiptId}; + @override + ReceivedReceipt map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceivedReceipt( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReceivedReceiptsTable createAlias(String alias) { + return $ReceivedReceiptsTable(attachedDatabase, alias); + } +} + +class ReceivedReceipt extends DataClass implements Insertable { + final String receiptId; + final DateTime createdAt; + const ReceivedReceipt({required this.receiptId, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['created_at'] = Variable(createdAt); + return map; + } + + ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceivedReceiptsCompanion( + receiptId: Value(receiptId), + createdAt: Value(createdAt), + ); + } + + factory ReceivedReceipt.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceivedReceipt( + receiptId: serializer.fromJson(json['receiptId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceivedReceipt copyWith({String? receiptId, DateTime? createdAt}) => + ReceivedReceipt( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + ); + ReceivedReceipt copyWithCompanion(ReceivedReceiptsCompanion data) { + return ReceivedReceipt( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceivedReceipt(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(receiptId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceivedReceipt && + other.receiptId == this.receiptId && + other.createdAt == this.createdAt); +} + +class ReceivedReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value createdAt; + final Value rowid; + const ReceivedReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceivedReceiptsCompanion.insert({ + required String receiptId, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId); + static Insertable custom({ + Expression? receiptId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceivedReceiptsCompanion copyWith( + {Value? receiptId, + Value? createdAt, + Value? rowid}) { + return ReceivedReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> { @override @@ -6130,6 +6409,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase { late final $ReactionsTable reactions = $ReactionsTable(this); late final $GroupMembersTable groupMembers = $GroupMembersTable(this); late final $ReceiptsTable receipts = $ReceiptsTable(this); + late final $ReceivedReceiptsTable receivedReceipts = + $ReceivedReceiptsTable(this); late final $SignalIdentityKeyStoresTable signalIdentityKeyStores = $SignalIdentityKeyStoresTable(this); late final $SignalPreKeyStoresTable signalPreKeyStores = @@ -6163,6 +6444,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { reactions, groupMembers, receipts, + receivedReceipts, signalIdentityKeyStores, signalPreKeyStores, signalSenderKeyStores, @@ -7854,6 +8136,8 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value openedAt, Value createdAt, Value modifiedAt, + Value ackByUser, + Value ackByServer, Value rowid, }); typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ @@ -7870,6 +8154,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value openedAt, Value createdAt, Value modifiedAt, + Value ackByUser, + Value ackByServer, Value rowid, }); @@ -8042,6 +8328,12 @@ class $$MessagesTableFilterComposer ColumnFilters get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnFilters(column)); + $$GroupsTableFilterComposer get groupId { final $$GroupsTableFilterComposer composer = $composerBuilder( composer: this, @@ -8245,6 +8537,12 @@ class $$MessagesTableOrderingComposer ColumnOrderings get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnOrderings(column)); + $$GroupsTableOrderingComposer get groupId { final $$GroupsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -8362,6 +8660,12 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => column); + GeneratedColumn get ackByUser => + $composableBuilder(column: $table.ackByUser, builder: (column) => column); + + GeneratedColumn get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => column); + $$GroupsTableAnnotationComposer get groupId { final $$GroupsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -8571,6 +8875,8 @@ class $$MessagesTableTableManager extends RootTableManager< Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion( @@ -8587,6 +8893,8 @@ class $$MessagesTableTableManager extends RootTableManager< openedAt: openedAt, createdAt: createdAt, modifiedAt: modifiedAt, + ackByUser: ackByUser, + ackByServer: ackByServer, rowid: rowid, ), createCompanionCallback: ({ @@ -8603,6 +8911,8 @@ class $$MessagesTableTableManager extends RootTableManager< Value openedAt = const Value.absent(), Value createdAt = const Value.absent(), Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion.insert( @@ -8619,6 +8929,8 @@ class $$MessagesTableTableManager extends RootTableManager< openedAt: openedAt, createdAt: createdAt, modifiedAt: modifiedAt, + ackByUser: ackByUser, + ackByServer: ackByServer, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -10060,6 +10372,135 @@ typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager< (Receipt, $$ReceiptsTableReferences), Receipt, PrefetchHooks Function({bool contactId, bool messageId})>; +typedef $$ReceivedReceiptsTableCreateCompanionBuilder + = ReceivedReceiptsCompanion Function({ + required String receiptId, + Value createdAt, + Value rowid, +}); +typedef $$ReceivedReceiptsTableUpdateCompanionBuilder + = ReceivedReceiptsCompanion Function({ + Value receiptId, + Value createdAt, + Value rowid, +}); + +class $$ReceivedReceiptsTableFilterComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$ReceivedReceiptsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$ReceivedReceiptsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get receiptId => + $composableBuilder(column: $table.receiptId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$ReceivedReceiptsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReceivedReceiptsTable, + ReceivedReceipt, + $$ReceivedReceiptsTableFilterComposer, + $$ReceivedReceiptsTableOrderingComposer, + $$ReceivedReceiptsTableAnnotationComposer, + $$ReceivedReceiptsTableCreateCompanionBuilder, + $$ReceivedReceiptsTableUpdateCompanionBuilder, + ( + ReceivedReceipt, + BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt> + ), + ReceivedReceipt, + PrefetchHooks Function()> { + $$ReceivedReceiptsTableTableManager( + _$TwonlyDB db, $ReceivedReceiptsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReceivedReceiptsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReceivedReceiptsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReceivedReceiptsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value receiptId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceivedReceiptsCompanion( + receiptId: receiptId, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String receiptId, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceivedReceiptsCompanion.insert( + receiptId: receiptId, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ReceivedReceiptsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReceivedReceiptsTable, + ReceivedReceipt, + $$ReceivedReceiptsTableFilterComposer, + $$ReceivedReceiptsTableOrderingComposer, + $$ReceivedReceiptsTableAnnotationComposer, + $$ReceivedReceiptsTableCreateCompanionBuilder, + $$ReceivedReceiptsTableUpdateCompanionBuilder, + ( + ReceivedReceipt, + BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt> + ), + ReceivedReceipt, + PrefetchHooks Function()>; typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder = SignalIdentityKeyStoresCompanion Function({ required int deviceId, @@ -11497,6 +11938,8 @@ class $TwonlyDBManager { $$GroupMembersTableTableManager(_db, _db.groupMembers); $$ReceiptsTableTableManager get receipts => $$ReceiptsTableTableManager(_db, _db.receipts); + $$ReceivedReceiptsTableTableManager get receivedReceipts => + $$ReceivedReceiptsTableTableManager(_db, _db.receivedReceipts); $$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores => $$SignalIdentityKeyStoresTableTableManager( _db, _db.signalIdentityKeyStores); diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 98a9cd9..2a921f3 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -22,6 +22,9 @@ class UserData { String? avatarSvg; String? avatarJson; + @JsonKey(defaultValue: 0) + int appVersion = 0; + @JsonKey(defaultValue: 0) int avatarCounter = 0; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 9336cdf..17cf5cf 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -14,6 +14,7 @@ UserData _$UserDataFromJson(Map json) => UserData( ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? + ..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..isDeveloper = json['isDeveloper'] as bool? ?? false ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 @@ -77,6 +78,7 @@ Map _$UserDataToJson(UserData instance) => { 'displayName': instance.displayName, 'avatarSvg': instance.avatarSvg, 'avatarJson': instance.avatarJson, + 'appVersion': instance.appVersion, 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, 'deviceId': instance.deviceId, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index 2f17535..f0a9532 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -19,6 +19,10 @@ class MemoryItem { final mediaService = await MediaFileService.fromMediaId(message.mediaId!); if (mediaService == null) continue; + if (!mediaService.imagePreviewAvailable) { + continue; + } + items .putIfAbsent( message.mediaId!, diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index a209170..e777cc5 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -388,7 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { factory EncryptedContent_MessageUpdate({ EncryptedContent_MessageUpdate_Type? type, $core.String? senderMessageId, - $core.Iterable<$core.String>? multipleSenderMessageIds, + $core.Iterable<$core.String>? multipleTargetMessageIds, $core.String? text, $fixnum.Int64? timestamp, }) { @@ -399,8 +399,8 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { if (senderMessageId != null) { $result.senderMessageId = senderMessageId; } - if (multipleSenderMessageIds != null) { - $result.multipleSenderMessageIds.addAll(multipleSenderMessageIds); + if (multipleTargetMessageIds != null) { + $result.multipleTargetMessageIds.addAll(multipleTargetMessageIds); } if (text != null) { $result.text = text; @@ -417,7 +417,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values) ..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') - ..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds') + ..pPS(3, _omitFieldNames ? '' : 'multipleTargetMessageIds', protoName: 'multipleTargetMessageIds') ..aOS(4, _omitFieldNames ? '' : 'text') ..aInt64(5, _omitFieldNames ? '' : 'timestamp') ..hasRequiredFields = false @@ -463,7 +463,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { void clearSenderMessageId() => clearField(2); @$pb.TagNumber(3) - $core.List<$core.String> get multipleSenderMessageIds => $_getList(2); + $core.List<$core.String> get multipleTargetMessageIds => $_getList(2); @$pb.TagNumber(4) $core.String get text => $_getSZ(3); @@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage { class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { factory EncryptedContent_MediaUpdate({ EncryptedContent_MediaUpdate_Type? type, - $core.String? targetMediaId, + $core.String? targetMessageId, }) { final $result = create(); if (type != null) { $result.type = type; } - if (targetMediaId != null) { - $result.targetMediaId = targetMediaId; + if (targetMessageId != null) { + $result.targetMessageId = targetMessageId; } return $result; } @@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values) - ..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId') + ..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') ..hasRequiredFields = false ; @@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { void clearType() => clearField(1); @$pb.TagNumber(2) - $core.String get targetMediaId => $_getSZ(1); + $core.String get targetMessageId => $_getSZ(1); @$pb.TagNumber(2) - set targetMediaId($core.String v) { $_setString(1, v); } + set targetMessageId($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) - $core.bool hasTargetMediaId() => $_has(1); + $core.bool hasTargetMessageId() => $_has(1); @$pb.TagNumber(2) - void clearTargetMediaId() => clearField(2); + void clearTargetMessageId() => clearField(2); } class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index f4c557c..483604a 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -158,7 +158,7 @@ const EncryptedContent_MessageUpdate$json = { '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'}, {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true}, - {'1': 'multipleSenderMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleSenderMessageIds'}, + {'1': 'multipleTargetMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleTargetMessageIds'}, {'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true}, {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, ], @@ -221,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = { '1': 'MediaUpdate', '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, - {'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'}, + {'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'}, ], '4': [EncryptedContent_MediaUpdate_Type$json], }; @@ -338,8 +338,8 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3' 'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu' 'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3' - 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11' - 'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX' + 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11' + 'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX' 'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ' 'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg' '9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu' @@ -353,24 +353,24 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ' 'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX' 'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu' - 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR' - 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn' - 'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg' - 'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw' - 'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg' - 'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw' - 'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG' - 'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt' - 'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE' - 'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB' - 'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA' - 'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo' - 'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2' - 'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl' - 'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm' - 'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK' - 'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg' - '5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk' - 'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm' - 'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl'); + 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR' + 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn' + 'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA' + 'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK' + 'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG' + 'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa0gEKDUNvbnRh' + 'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS' + '5UeXBlUgR0eXBlEiEKCWF2YXRhclN2ZxgCIAEoCUgAUglhdmF0YXJTdmeIAQESJQoLZGlzcGxh' + 'eU5hbWUYAyABKAlIAVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVV' + 'BEQVRFEAFCDAoKX2F2YXRhclN2Z0IOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5' + 'cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SW' + 'QYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQY' + 'BCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQg' + 'gKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNv' + 'dW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgAS' + 'gDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmll' + 'bmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZX' + 'JCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFj' + 'dFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCw' + 'oJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZQ=='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index cd37d58..890ef28 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -66,7 +66,7 @@ message EncryptedContent { } Type type = 1; optional string senderMessageId = 2; - repeated string multipleSenderMessageIds = 3; + repeated string multipleTargetMessageIds = 3; optional string text = 4; int64 timestamp = 5; } @@ -99,7 +99,7 @@ message EncryptedContent { DECRYPTION_ERROR = 2; } Type type = 1; - string targetMediaId = 2; + string targetMessageId = 2; } message ContactRequest { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 79a9dc3..4d6ff9e 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -156,11 +156,6 @@ class ApiService { } reconnectionTimer?.cancel(); reconnectionTimer = null; - final user = await getUser(); - if (user != null) { - globalCallbackConnectionState(isConnected: true); - return false; - } return lockConnecting.protect(() async { if (_channel != null) { return true; diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 8da0d48..9f4e910 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -95,6 +95,7 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { } if (failed) { + Log.error('Background media upload failed: ${update.status}'); await requestMediaReupload(mediaId); } else { await handleEncryptedFile(mediaId); @@ -194,6 +195,9 @@ Future downloadFileFast( if (response.statusCode == 404 || response.statusCode == 403 || response.statusCode == 400) { + Log.error( + 'Got ${response.statusCode} from server. Requesting upload again', + ); // Message was deleted from the server. Requesting it again from the sender to upload it again... await requestMediaReupload(media.mediaId); return; @@ -217,7 +221,7 @@ Future requestMediaReupload(String mediaId) async { EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMediaId: mediaId, + targetMessageId: messages.first.messageId, ), ), ); diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 7b9102b..39f5eb7 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -58,6 +58,12 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { final mediaId = update.task.taskId.replaceAll('upload_', ''); final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + if (update.status == TaskStatus.enqueued || + update.status == TaskStatus.running) { + // Ignore these updates + return; + } + if (media == null) { Log.error( 'Got an upload task but no upload media in the media upload database', @@ -115,7 +121,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { final mediaService = await MediaFileService.fromMedia(media); - await mediaService.setUploadState(UploadState.uploading); + await mediaService.setUploadState(UploadState.uploaded); // In all other cases just try the upload again... await startBackgroundMediaUpload(mediaService); } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 6c8c783..22326f0 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -6,9 +6,11 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; 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/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; @@ -47,6 +49,7 @@ Future insertMediaFileInMessagesTable( MessagesCompanion( groupId: Value(groupId), mediaId: Value(mediaService.mediaFile.mediaId), + type: const Value(MessageType.media), ), ); if (message != null) { @@ -147,12 +150,13 @@ Future _createUploadRequest(MediaFileService media) async { } final notEncryptedContent = EncryptedContent( + groupId: message.groupId, media: EncryptedContent_Media( senderMessageId: message.messageId, type: type, requiresAuthentication: media.mediaFile.requiresAuthentication, timestamp: Int64(message.createdAt.millisecondsSinceEpoch), - downloadToken: media.mediaFile.downloadToken, + downloadToken: downloadToken.toList(), encryptionKey: media.mediaFile.encryptionKey, encryptionNonce: media.mediaFile.encryptionNonce, encryptionMac: media.mediaFile.encryptionMac, @@ -202,36 +206,50 @@ Future _createUploadRequest(MediaFileService media) async { await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); } +Mutex protectUpload = Mutex(); + Future _uploadUploadRequest(MediaFileService media) async { - final apiAuthTokenRaw = await const FlutterSecureStorage() - .read(key: SecureStorageKeys.apiAuthToken); - if (apiAuthTokenRaw == null) { - Log.error('api auth token not defined.'); - return; - } - final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); + await protectUpload.protect(() async { + final currentMedia = + await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId); - final apiUrl = - 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; + if (currentMedia == null || + currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) { + Log.info('Download for ${media.mediaFile.mediaId} already started.'); + return null; + } - // try { - Log.info('Starting upload from ${media.mediaFile.mediaId}'); + final apiAuthTokenRaw = await const FlutterSecureStorage() + .read(key: SecureStorageKeys.apiAuthToken); - final task = UploadTask.fromFile( - taskId: 'upload_${media.mediaFile.mediaId}', - displayName: media.mediaFile.type.name, - file: media.uploadRequestPath, - url: apiUrl, - priority: 0, - retries: 10, - headers: { - 'x-twonly-auth-token': apiAuthToken, - }, - ); + if (apiAuthTokenRaw == null) { + Log.error('api auth token not defined.'); + return null; + } + final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); - Log.info('Enqueue upload task: ${task.taskId}'); + final apiUrl = + 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; - await FileDownloader().enqueue(task); + // try { + Log.info('Starting upload from ${media.mediaFile.mediaId}'); - await media.setUploadState(UploadState.backgroundUploadTaskStarted); + final task = UploadTask.fromFile( + taskId: 'upload_${media.mediaFile.mediaId}', + displayName: media.mediaFile.type.name, + file: media.uploadRequestPath, + url: apiUrl, + priority: 0, + retries: 10, + headers: { + 'x-twonly-auth-token': apiAuthToken, + }, + ); + + Log.info('Enqueue upload task: ${task.taskId}'); + + await FileDownloader().enqueue(task); + + await media.setUploadState(UploadState.backgroundUploadTaskStarted); + }); } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 1298115..8c3408c 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -4,6 +4,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' @@ -35,7 +36,6 @@ Future tryTransmitMessages() async { Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ String? receiptId, Receipt? receipt, - bool reupload = false, bool onlyReturnEncryptedData = false, }) async { try { @@ -49,15 +49,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ } receiptId = receipt.receiptId; - if (reupload) { - await twonlyDB.receiptsDao.updateReceipt( - receiptId, - const ReceiptsCompanion( - ackByServerAt: Value(null), - ), - ); - } - if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { Log.error('$receiptId message already uploaded!'); return null; @@ -71,12 +62,18 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ final encryptedContent = pb.EncryptedContent.fromBuffer(message.encryptedContent); - var pushData = await getPushDataFromEncryptedContent( + final pushNotification = await getPushNotificationFromEncryptedContent( receipt.contactId, receipt.messageId, encryptedContent, ); + Uint8List? pushData; + if (pushNotification != null) { + pushData = + await encryptPushNotification(receipt.contactId, pushNotification); + } + if (message.type == pb.Message_Type.TEST_NOTIFICATION) { pushData = (PushNotification()..kind = PushKind.testNotification) .writeToBuffer(); @@ -167,6 +164,7 @@ Future insertAndSendTextMessage( MessagesCompanion( groupId: Value(groupId), content: Value(textMessage), + type: const Value(MessageType.text), quotesMessageId: Value(quotesMessageId), ), ); @@ -187,26 +185,24 @@ Future insertAndSendTextMessage( encryptedContent.textMessage.quoteMessageId = quotesMessageId; } - await sendCipherTextToGroup(groupId, encryptedContent); + await sendCipherTextToGroup(groupId, encryptedContent, message.messageId); } Future sendCipherTextToGroup( String groupId, pb.EncryptedContent encryptedContent, + String? messageId, ) async { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null) return; - encryptedContent - ..groupId = groupId - ..isDirectChat = group.isDirectChat; + encryptedContent.groupId = groupId; for (final groupMember in groupMembers) { unawaited( sendCipherText( groupMember.contactId, encryptedContent, + messageId: messageId, ), ); } @@ -216,6 +212,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( int contactId, pb.EncryptedContent encryptedContent, { bool onlyReturnEncryptedData = false, + String? messageId, }) async { final response = pb.Message() ..type = pb.Message_Type.CIPHERTEXT @@ -225,6 +222,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( ReceiptsCompanion( contactId: Value(contactId), message: Value(response.writeToBuffer()), + messageId: Value(messageId), ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), ), ); @@ -249,15 +247,23 @@ Future notifyContactAboutOpeningMessage( biggestMessageId = messageOtherId; } } + Log.info('Opened messages: $messageOtherIds'); + await sendCipherText( contactId, pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb.EncryptedContent_MessageUpdate_Type.OPENED, - multipleSenderMessageIds: messageOtherIds, + multipleTargetMessageIds: messageOtherIds, ), ), ); + for (final messageId in messageOtherIds) { + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(openedAt: Value(DateTime.now())), + ); + } await updateLastMessageId(contactId, biggestMessageId); } diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 74aa8e5..9484785 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart' hide Message; @@ -50,10 +51,20 @@ Future handleServerMessage(server.ServerToClient msg) async { DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); +Mutex protectReceiptCheck = Mutex(); + Future handleNewMessage(int fromUserId, Uint8List body) async { final message = Message.fromBuffer(body); final receiptId = message.receiptId; + await protectReceiptCheck.protect(() async { + if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { + Log.error('Got duplicated message from the server. Ignoring it.'); + return; + } + await twonlyDB.receiptsDao.gotReceipt(receiptId); + }); + switch (message.type) { case Message_Type.SENDER_DELIVERY_RECEIPT: Log.info('Got delivery receipt for $receiptId!'); @@ -65,7 +76,15 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { Log.info( 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', ); - await tryToSendCompleteMessage(receiptId: receiptId, reupload: true); + final newReceiptId = uuid.v4(); + await twonlyDB.receiptsDao.updateReceipt( + receiptId, + ReceiptsCompanion( + receiptId: Value(newReceiptId), + ackByServerAt: const Value(null), + ), + ); + await tryToSendCompleteMessage(receiptId: newReceiptId); } case Message_Type.CIPHERTEXT: @@ -112,7 +131,7 @@ Future handleEncryptedMessage( final (content, decryptionErrorType) = await signalDecryptMessage( fromUserId, encryptedContentRaw, - messageType as int, + messageType.value, ); if (content == null) { @@ -147,7 +166,24 @@ Future handleEncryptedMessage( return null; } + if (content.hasMessageUpdate()) { + await handleMessageUpdate( + fromUserId, + content.messageUpdate, + ); + return null; + } + + if (content.hasMediaUpdate()) { + await handleMediaUpdate( + fromUserId, + content.mediaUpdate, + ); + return null; + } + if (!content.hasGroupId()) { + Log.error('Messages should have a groupId $fromUserId.'); return null; } @@ -157,14 +193,6 @@ Future handleEncryptedMessage( return null; } - if (content.hasMessageUpdate()) { - await handleMessageUpdate( - fromUserId, - content.messageUpdate, - ); - return null; - } - if (content.hasTextMessage()) { await handleTextMessage( fromUserId, @@ -192,14 +220,5 @@ Future handleEncryptedMessage( return null; } - if (content.hasMediaUpdate()) { - await handleMediaUpdate( - fromUserId, - content.groupId, - content.mediaUpdate, - ); - return null; - } - return null; } diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index b822fb4..5d2ee34 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; 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/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; @@ -92,6 +93,7 @@ Future handleMedia( senderId: Value(fromUserId), groupId: Value(groupId), mediaId: Value(mediaFile.mediaId), + type: const Value(MessageType.media), quotesMessageId: Value( media.hasQuoteMessageId() ? media.quoteMessageId : null, ), @@ -112,16 +114,17 @@ Future handleMedia( Future handleMediaUpdate( int fromUserId, - String groupId, EncryptedContent_MediaUpdate mediaUpdate, ) async { - final messages = await twonlyDB.messagesDao - .getMessagesByMediaId(mediaUpdate.targetMediaId); - if (messages.length != 1) return; - final message = messages.first; - if (message.senderId != fromUserId) return; + final message = await twonlyDB.messagesDao + .getMessageById(mediaUpdate.targetMessageId) + .getSingleOrNull(); + if (message == null) { + Log.error( + 'Got media update to message ${mediaUpdate.targetMessageId} but message not found.'); + } final mediaFile = - await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!); if (mediaFile == null) { Log.info( 'Got media file update, but media file was not found ${message.mediaId}', @@ -140,15 +143,8 @@ Future handleMediaUpdate( ); case EncryptedContent_MediaUpdate_Type.STORED: Log.info('Got media file stored ${mediaFile.mediaId}'); - await twonlyDB.mediaFilesDao.updateMedia( - mediaFile.mediaId, - const MediaFilesCompanion( - stored: Value(true), - ), - ); - final mediaService = await MediaFileService.fromMedia(mediaFile); - unawaited(mediaService.createThumbnail()); + await mediaService.storeMediaFile(); case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: Log.info('Got media file decryption error ${mediaFile.mediaId}'); diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/server_messages/messages.server_messages.dart index b0b2457..ff87010 100644 --- a/lib/src/services/api/server_messages/messages.server_messages.dart +++ b/lib/src/services/api/server_messages/messages.server_messages.dart @@ -9,17 +9,20 @@ Future handleMessageUpdate( ) async { switch (messageUpdate.type) { case EncryptedContent_MessageUpdate_Type.OPENED: - Log.info( - 'Opened message ${messageUpdate.multipleSenderMessageIds.length}', - ); - for (final senderMessageId in messageUpdate.multipleSenderMessageIds) { + for (final targetMessageId in messageUpdate.multipleTargetMessageIds) { + Log.info( + 'Opened message $targetMessageId', + ); await twonlyDB.messagesDao.handleMessageOpened( contactId, - senderMessageId, + targetMessageId, fromTimestamp(messageUpdate.timestamp), ); } case EncryptedContent_MessageUpdate_Type.DELETE: + if (!await isSender(contactId, messageUpdate.senderMessageId)) { + return; + } Log.info('Delete message ${messageUpdate.senderMessageId}'); await twonlyDB.messagesDao.handleMessageDeletion( contactId, @@ -27,6 +30,9 @@ Future handleMessageUpdate( fromTimestamp(messageUpdate.timestamp), ); case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: + if (!await isSender(contactId, messageUpdate.senderMessageId)) { + return; + } Log.info('Edit message ${messageUpdate.senderMessageId}'); await twonlyDB.messagesDao.handleTextEdit( contactId, @@ -36,3 +42,14 @@ Future handleMessageUpdate( ); } } + +Future isSender(int fromUserId, String messageId) async { + final message = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (message == null) return false; + if (message.senderId == fromUserId) { + return true; + } + Log.error('Contact $fromUserId tried to modify the message $messageId'); + return false; +} diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index 7efbdfd..c20d803 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/utils.dart'; @@ -20,6 +21,7 @@ Future handleTextMessage( senderId: Value(fromUserId), groupId: Value(groupId), content: Value(textMessage.text), + type: const Value(MessageType.text), quotesMessageId: Value( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index e0a101e..c7af9bc 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -26,7 +26,7 @@ class Result { } DateTime fromTimestamp(Int64 timeStamp) { - return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt() * 1000); + return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt()); } // ignore: strict_raw_type @@ -88,7 +88,7 @@ Future handleMediaError(MediaFile media) async { EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMediaId: message.mediaId, + targetMessageId: message.messageId, ), ), ); diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 73d5a78..744c54d 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:path/path.dart'; @@ -129,6 +130,9 @@ class MediaFileService { } } + bool get imagePreviewAvailable => + thumbnailPath.existsSync() || storedPath.existsSync(); + Future storeMediaFile() async { Log.info('Storing media file ${mediaFile.mediaId}'); await twonlyDB.mediaFilesDao.updateMedia( @@ -137,7 +141,25 @@ class MediaFileService { stored: Value(true), ), ); - await tempPath.copy(storedPath.path); + await twonlyDB.messagesDao.updateMessagesByMediaId( + mediaFile.mediaId, + const MessagesCompanion( + mediaStored: Value(true), + ), + ); + + if (originalPath.existsSync()) { + await originalPath.copy(tempPath.path); + await compressMedia(); + } + if (tempPath.existsSync()) { + await tempPath.copy(storedPath.path); + } else { + Log.error( + 'Could not store image neither tempPath nor originalPath exists.', + ); + } + unawaited(createThumbnail()); await updateFromDB(); } diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 1abf6fc..4a50c61 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -195,12 +195,12 @@ Future updateLastMessageId(int fromUserId, String messageId) async { } } -Future getPushDataFromEncryptedContent( +Future getPushNotificationFromEncryptedContent( int toUserId, String? messageId, EncryptedContent content, ) async { - late PushKind kind; + PushKind? kind; String? reactionContent; if (content.hasReaction()) { @@ -270,6 +270,8 @@ Future getPushDataFromEncryptedContent( } } + if (kind == null) return null; + final pushNotification = PushNotification()..kind = kind; if (reactionContent != null) { pushNotification.reactionContent = reactionContent; @@ -277,7 +279,7 @@ Future getPushDataFromEncryptedContent( if (messageId != null) { pushNotification.messageId = messageId; } - return encryptPushNotification(toUserId, pushNotification); + return pushNotification; } /// this will trigger a push notification diff --git a/lib/src/services/signal/prekeys.signal.dart b/lib/src/services/signal/prekeys.signal.dart index ccee8ec..e70d239 100644 --- a/lib/src/services/signal/prekeys.signal.dart +++ b/lib/src/services/signal/prekeys.signal.dart @@ -31,13 +31,13 @@ Future requestNewPrekeysForContact(int contactId) async { .isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { return; } - Log.info('Requesting new PREKEYS for $contactId'); + Log.info('[PREKEY] Requesting new PREKEYS for $contactId'); lastPreKeyRequest = DateTime.now(); await requestNewKeys.protect(() async { final otherKeys = await apiService.getPreKeysByUserId(contactId); if (otherKeys != null) { Log.info( - 'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!', + '[PREKEY] Got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!', ); final preKeys = otherKeys.preKeys .map( @@ -50,7 +50,8 @@ Future requestNewPrekeysForContact(int contactId) async { .toList(); await twonlyDB.signalDao.insertPreKeys(preKeys); } else { - Log.error('could not load new pre keys for user $contactId'); + // 104400 + Log.error('[PREKEY] Could not load new pre keys for user $contactId'); } }); } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 79848fd..bd34ec1 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:local_auth/local_auth.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:provider/provider.dart'; @@ -246,11 +247,14 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { } bool isUUIDNewer(String uuid1, String uuid2) { - final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); - final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); - print(timestamp1); - print(timestamp2); - return timestamp1 > timestamp2; + try { + final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); + final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); + return timestamp1 > timestamp2; + } catch (e) { + Log.error(e); + return true; + } } String uint8ListToHex(List bytes) { @@ -313,3 +317,34 @@ Color getMessageColorFromType( } return color; } + +String getUUIDforDirectChat(int a, int b) { + if (a < 0 || b < 0) { + throw ArgumentError('Inputs must be non-negative integers.'); + } + if (a > integerMax || b > integerMax) { + throw ArgumentError('Inputs must be <= 0x7fffffff.'); + } + + // Mask to 64 bits in case inputs exceed 64 bits + final mask64 = (BigInt.one << 64) - BigInt.one; + final ai = BigInt.from(a) & mask64; + final bi = BigInt.from(b) & mask64; + + // Ensure the bigger integer is in front (high 64 bits) + final hi = ai >= bi ? ai : bi; + final lo = ai >= bi ? bi : ai; + + final combined = (hi << 64) | lo; + + final hex = combined.toRadixString(16).padLeft(32, '0'); + + final parts = [ + hex.substring(0, 8), + hex.substring(8, 12), + hex.substring(12, 16), + hex.substring(16, 20), + hex.substring(20, 32), + ]; + return parts.join('-'); +} diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 6824617..5c81e0f 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,20 +1,20 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ - required this.getMergedImage, + required this.storeImageAsOriginal, required this.isLoading, required this.displayButtonLabel, required this.mediaService, super.key, }); - final Future Function() getMergedImage; + final Future Function() storeImageAsOriginal; final bool displayButtonLabel; final MediaFileService mediaService; final bool isLoading; @@ -45,6 +45,10 @@ class SaveToGalleryButtonState extends State { _imageSaving = true; }); + if (widget.mediaService.mediaFile.type == MediaType.image) { + await widget.storeImageAsOriginal(); + } + String? res; final storedMediaPath = widget.mediaService.storedPath; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index e5d787a..6cf4910 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -171,20 +171,20 @@ class _ShareImageEditorView extends State { ), const SizedBox(height: 8), NotificationBadge( - count: (media.type != MediaType.video) + count: (media.type == MediaType.video) ? '0' : media.displayLimitInMilliseconds == null ? '∞' : media.displayLimitInMilliseconds.toString(), child: ActionButton( - (media.type != MediaType.video) + (media.type == MediaType.video) ? media.displayLimitInMilliseconds == null ? Icons.repeat_rounded : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, onPressed: () async { - if (media.type != MediaType.video) { + if (media.type == MediaType.video) { await mediaService.setDisplayLimit( (media.displayLimitInMilliseconds == null) ? 0 : null, ); @@ -311,7 +311,7 @@ class _ShareImageEditorView extends State { } } - if (layers.length > 1 || media.type != MediaType.video) { + if (layers.length > 1 || media.type == MediaType.video) { for (final x in layers) { x.showCustomButtons = false; } @@ -434,7 +434,7 @@ class _ShareImageEditorView extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SaveToGalleryButton( - getMergedImage: getEditedImageBytes, + storeImageAsOriginal: storeImageAsOriginal, mediaService: mediaService, displayButtonLabel: widget.sendToGroup == null, isLoading: loadingImage, diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index adde0ea..f13fac1 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -209,31 +209,32 @@ class _ChatListViewState extends State { child: isConnected ? Container() : const ConnectionInfo(), ), Positioned.fill( - child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - label: Text(context.lang.chatListViewSearchUserNameBtn), + child: RefreshIndicator( + onRefresh: () async { + await apiService.close(() {}); + await apiService.connect(force: true); + await Future.delayed(const Duration(seconds: 1)); + }, + child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + label: + Text(context.lang.chatListViewSearchUserNameBtn), + ), ), - ), - ) - : RefreshIndicator( - onRefresh: () async { - await apiService.close(() {}); - await apiService.connect(force: true); - await Future.delayed(const Duration(seconds: 1)); - }, - child: ListView.builder( + ) + : ListView.builder( itemCount: _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0) + _groupsNotPinned.length + @@ -276,7 +277,7 @@ class _ChatListViewState extends State { ); }, ), - ), + ), ), ], ), diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index dbb2fa4..dd4bab5 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @@ -36,10 +37,12 @@ class _UserListItem extends State { List messagesNotOpened = []; late StreamSubscription> messagesNotOpenedStream; - List lastMessages = []; - late StreamSubscription> lastMessageStream; + Message? lastMessage; + late StreamSubscription lastMessageStream; + late StreamSubscription> lastMediaFilesStream; List previewMessages = []; + List previewMediaFiles = []; bool hasNonOpenedMediaFile = false; @override @@ -52,6 +55,7 @@ class _UserListItem extends State { void dispose() { messagesNotOpenedStream.cancel(); lastMessageStream.cancel(); + lastMediaFilesStream.cancel(); super.dispose(); } @@ -59,30 +63,44 @@ class _UserListItem extends State { lastMessageStream = twonlyDB.messagesDao .watchLastMessage(widget.group.groupId) .listen((update) { - updateState(update, messagesNotOpened); + protectUpdateState.protect(() async { + await updateState(update, messagesNotOpened); + }); }); messagesNotOpenedStream = twonlyDB.messagesDao .watchMessageNotOpened(widget.group.groupId) .listen((update) { - updateState(lastMessages, update); + protectUpdateState.protect(() async { + await updateState(lastMessage, update); + }); + }); + + lastMediaFilesStream = + twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { + for (final mediaFile in mediaFiles) { + final index = + previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId); + if (index >= 0) { + previewMediaFiles[index] = mediaFile; + } + } + setState(() {}); }); } - void updateState( - List newLastMessages, + Mutex protectUpdateState = Mutex(); + + Future updateState( + Message? newLastMessage, List newMessagesNotOpened, - ) { - if (newLastMessages.isEmpty) { + ) async { + if (newLastMessage == null) { // there are no messages at all currentMessage = null; previewMessages = []; - } else if (newMessagesNotOpened.isEmpty) { - // there are no not opened messages show just the last message in the table - currentMessage = newLastMessages.last; - previewMessages = newLastMessages; - } else { - // filter first for received messages + } else if (newMessagesNotOpened.isNotEmpty) { + // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); @@ -93,6 +111,10 @@ class _UserListItem extends State { previewMessages = newMessagesNotOpened; currentMessage = newMessagesNotOpened.first; } + } else { + // there are no not opened messages show just the last message in the table + currentMessage = newLastMessage; + previewMessages = [newLastMessage]; } final msgs = @@ -106,7 +128,18 @@ class _UserListItem extends State { hasNonOpenedMediaFile = false; } - lastMessages = newLastMessages; + for (final message in previewMessages) { + if (message.mediaId != null && + !previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + if (mediaFile != null) { + previewMediaFiles.add(mediaFile); + } + } + } + + lastMessage = newLastMessage; messagesNotOpened = newMessagesNotOpened; setState(() { // sets lastMessages, messagesNotOpened and currentMessage @@ -136,7 +169,7 @@ class _UserListItem extends State { await startDownloadMedia(mediaFile, true); return; } - if (mediaFile.downloadState! == DownloadState.downloaded) { + if (mediaFile.downloadState! == DownloadState.ready) { if (!mounted) return; await Navigator.push( context, @@ -184,7 +217,7 @@ class _UserListItem extends State { ? Text(context.lang.chatsTapToSend) : Row( children: [ - MessageSendStateIcon(previewMessages), + MessageSendStateIcon(previewMessages, previewMediaFiles), const Text('•'), const SizedBox(width: 5), if (currentMessage != null) diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index a318f43..538d9b7 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mutex/mutex.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.dart'; @@ -94,6 +95,8 @@ class _ChatMessagesViewState extends State { super.dispose(); } + Mutex protectMessageUpdating = Mutex(); + Future initStreams() async { final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId); userSub = groupStream.listen((newGroup) { @@ -105,55 +108,64 @@ class _ChatMessagesViewState extends State { final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); messageSub = msgStream.listen((newMessages) async { - await flutterLocalNotificationsPlugin.cancelAll(); - - final chatItems = []; - final storedMediaFiles = []; - - DateTime? lastDate; - - final openedMessages = >{}; - - for (final msg in newMessages) { - if (msg.type == MessageType.text && - msg.senderId != null && - msg.openedAt == null) { - openedMessages[msg.senderId!]!.add(msg.messageId); - } - - if (msg.type == MessageType.media && msg.mediaStored) { - storedMediaFiles.add(msg); - } - - if (lastDate == null || - msg.createdAt.day != lastDate.day || - msg.createdAt.month != lastDate.month || - msg.createdAt.year != lastDate.year) { - chatItems.add(ChatItem.date(msg.createdAt)); - lastDate = msg.createdAt; - } else if (msg.createdAt.difference(lastDate).inMinutes >= 20) { - chatItems.add(ChatItem.time(msg.createdAt)); - lastDate = msg.createdAt; - } - chatItems.add(ChatItem.message(msg)); + /// 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 flutterLocalNotificationsPlugin.cancelAll(); - for (final contactId in openedMessages.keys) { - await notifyContactAboutOpeningMessage( - contactId, - openedMessages[contactId]!, - ); - } + final chatItems = []; + final storedMediaFiles = []; - await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId); + DateTime? lastDate; - setState(() { - messages = chatItems.reversed.toList(); + final openedMessages = >{}; + + for (final msg in newMessages) { + if (msg.type == MessageType.text && + msg.senderId != null && + msg.openedAt == null) { + if (openedMessages[msg.senderId!] == null) { + openedMessages[msg.senderId!] = []; + } + openedMessages[msg.senderId!]!.add(msg.messageId); + } + + if (msg.type == MessageType.media && msg.mediaStored) { + storedMediaFiles.add(msg); + } + + if (lastDate == null || + msg.createdAt.day != lastDate.day || + msg.createdAt.month != lastDate.month || + msg.createdAt.year != lastDate.year) { + chatItems.add(ChatItem.date(msg.createdAt)); + lastDate = msg.createdAt; + } else if (msg.createdAt.difference(lastDate).inMinutes >= 20) { + chatItems.add(ChatItem.time(msg.createdAt)); + lastDate = msg.createdAt; + } + chatItems.add(ChatItem.message(msg)); + } + + for (final contactId in openedMessages.keys) { + await notifyContactAboutOpeningMessage( + contactId, + openedMessages[contactId]!, + ); + } + + if (!mounted) return; + setState(() { + messages = chatItems.reversed.toList(); + }); + + final items = await MemoryItem.convertFromMessages(storedMediaFiles); + galleryItems = items.values.toList(); + setState(() {}); }); - - final items = await MemoryItem.convertFromMessages(storedMediaFiles); - galleryItems = items.values.toList(); - setState(() {}); }); } @@ -396,18 +408,8 @@ bool isLastMessageFromSameUser(List messages, int index) { if (index <= 0) { return true; // If there is no previous message, return true } - - final lastMessage = messages[index - 1]; - final currentMessage = messages[index]; - - if (lastMessage.isMessage && currentMessage.isMessage) { - // Check if both messages have the same quotesMessageId (or both are null) - return (lastMessage.message!.quotesMessageId == null && - currentMessage.message!.quotesMessageId == null) || - (lastMessage.message!.quotesMessageId != null && - currentMessage.message!.quotesMessageId != null); - } - return false; + return (messages[index - 1].message?.senderId == + messages[index].message?.senderId); } double calculateNumberOfLines(String text, double width, double fontSize) { diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 990f127..9c01271 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages.table.dart' hide MessageActions; import 'package:twonly/src/database/twonly.db.dart'; @@ -35,14 +38,31 @@ class ChatListEntry extends StatefulWidget { class _ChatListEntryState extends State { MediaFileService? mediaService; + StreamSubscription? mediaFileSub; + @override void initState() { initAsync(); super.initState(); } + @override + void dispose() { + mediaFileSub?.cancel(); + super.dispose(); + } + Future initAsync() async { - mediaService = await MediaFileService.fromMediaId(widget.message.messageId); + if (widget.message.mediaId != null) { + final mediaFileStream = + twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!); + mediaFileSub = mediaFileStream.listen((mediaFiles) async { + if (mediaFiles != null) { + mediaService = await MediaFileService.fromMedia(mediaFiles); + if (mounted) setState(() {}); + } + }); + } setState(() {}); } diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 32fa297..8afe01a 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -47,17 +48,20 @@ class _ChatMediaEntryState extends State { EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.REOPENED, - targetMediaId: widget.message.mediaId, + targetMessageId: widget.message.messageId, ), ), + null, + ); + await twonlyDB.messagesDao.updateMessageId( + widget.message.messageId, + const MessagesCompanion(openedAt: Value(null)), ); - await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId); } } Future onTap() async { - if (widget.mediaService.mediaFile.downloadState == - DownloadState.downloaded && + if (widget.mediaService.mediaFile.downloadState == DownloadState.ready && widget.message.openedAt == null) { if (!mounted) return; await Navigator.push( @@ -91,7 +95,10 @@ class _ChatMediaEntryState extends State { onTap: (widget.message.type == MessageType.media) ? onTap : null, child: SizedBox( width: 150, - height: widget.message.mediaStored ? 271 : null, + height: (widget.message.mediaStored && + widget.mediaService.imagePreviewAvailable) + ? 271 + : null, child: Align( alignment: Alignment.centerRight, child: ClipRRect( diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index 255cac9..d3a01d7 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -58,7 +58,7 @@ class _InChatMediaViewerState extends State { bool loadIndex() { if (widget.message.mediaStored) { final index = widget.galleryItems.indexWhere( - (x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId), + (x) => x.mediaService.mediaFile.mediaId == (widget.message.mediaId), ); if (index != -1) { galleryItemIndex = index; @@ -112,7 +112,8 @@ class _InChatMediaViewerState extends State { @override Widget build(BuildContext context) { - if (!widget.message.mediaStored) { + if (!widget.message.mediaStored || + !widget.mediaService.imagePreviewAvailable) { return Container( constraints: const BoxConstraints( minHeight: 39, @@ -130,6 +131,7 @@ class _InChatMediaViewerState extends State { ), child: MessageSendStateIcon( [widget.message], + [widget.mediaService.mediaFile], mainAxisAlignment: MainAxisAlignment.center, canBeReopened: widget.canBeReopened, ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 542fcdd..349f349 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -64,6 +64,7 @@ class MessageContextMenu extends StatelessWidget { remove: false, ), ), + null, ); }, child: const FaIcon(FontAwesomeIcons.faceLaugh), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index d423c10..2354fd0 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -1,11 +1,10 @@ import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/globals.dart'; 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/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -18,49 +17,37 @@ enum MessageSendState { sending, } -Future messageSendStateFromMessage(Message msg) async { - MessageSendState state; - - final ackByServer = await twonlyDB.messagesDao.haveAllMembers( - msg.groupId, - msg.messageId, - MessageActionType.ackByServerAt, - ); - - if (!ackByServer) { - if (msg.senderId == null) { - state = MessageSendState.sending; - } else { - state = MessageSendState.receiving; +MessageSendState messageSendStateFromMessage(Message msg) { + if (msg.senderId == null) { + /// messages was send by me, look up if every messages was received by the server... + if (msg.ackByServer == null) { + return MessageSendState.sending; } - } else { - if (msg.senderId == null) { - // message send - if (msg.openedAt == null) { - state = MessageSendState.send; - } else { - state = MessageSendState.sendOpened; - } + if (msg.openedAt != null) { + return MessageSendState.sendOpened; } else { - // message received - if (msg.openedAt == null) { - state = MessageSendState.received; - } else { - state = MessageSendState.receivedOpened; - } + return MessageSendState.send; } } - return state; + + // message received + if (msg.openedAt == null) { + return MessageSendState.received; + } else { + return MessageSendState.receivedOpened; + } } class MessageSendStateIcon extends StatefulWidget { const MessageSendStateIcon( - this.messages, { + this.messages, + this.mediaFiles, { super.key, this.canBeReopened = false, this.mainAxisAlignment = MainAxisAlignment.end, }); final List messages; + final List mediaFiles; final MainAxisAlignment mainAxisAlignment; final bool canBeReopened; @@ -69,17 +56,30 @@ class MessageSendStateIcon extends StatefulWidget { } class _MessageSendStateIconState extends State { - List icons = []; - String text = ''; - Widget? textWidget; - @override void initState() { super.initState(); - initAsync(); } - Future initAsync() async { + Widget getLoaderIcon(Color color) { + return Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator(strokeWidth: 1, color: color), + ), + const SizedBox(width: 2), + ], + ); + } + + @override + Widget build(BuildContext context) { + final icons = []; + var text = ''; + Widget? textWidget; + textWidget = null; final kindsAlreadyShown = HashSet(); for (final message in widget.messages) { @@ -87,16 +87,14 @@ class _MessageSendStateIconState extends State { if (kindsAlreadyShown.contains(message.type)) continue; kindsAlreadyShown.add(message.type); - final state = await messageSendStateFromMessage(message); + final state = messageSendStateFromMessage(message); final mediaFile = message.mediaId == null ? null - : await MediaFileService.fromMediaId(message.mediaId!); + : widget.mediaFiles + .firstWhereOrNull((t) => t.mediaId == message.mediaId); - if (!mounted) return; - - final color = - getMessageColorFromType(message, mediaFile?.mediaFile, context); + final color = getMessageColorFromType(message, mediaFile, context); Widget icon = const Placeholder(); textWidget = null; @@ -126,11 +124,10 @@ class _MessageSendStateIconState extends State { icon = Icon(Icons.square_rounded, size: 14, color: color); text = context.lang.messageSendState_Received; if (message.type == MessageType.media) { - if (mediaFile!.mediaFile.downloadState == DownloadState.pending) { + if (mediaFile!.downloadState == DownloadState.pending) { text = context.lang.messageSendState_TapToLoad; } - if (mediaFile.mediaFile.downloadState == - DownloadState.downloading) { + if (mediaFile.downloadState == DownloadState.downloading) { text = context.lang.messageSendState_Loading; icon = getLoaderIcon(color); } @@ -153,12 +150,12 @@ class _MessageSendStateIconState extends State { } if (mediaFile != null) { - if (mediaFile.mediaFile.stored) { + if (mediaFile.reopenByContact) { icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); text = context.lang.messageReopened; } - if (mediaFile.mediaFile.reuploadRequestedBy != null) { + if (mediaFile.downloadState == DownloadState.reuploadRequested) { icon = FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); textWidget = Text( @@ -175,24 +172,6 @@ class _MessageSendStateIconState extends State { } } - setState(() {}); - } - - Widget getLoaderIcon(Color color) { - return Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator(strokeWidth: 1, color: color), - ), - const SizedBox(width: 2), - ], - ); - } - - @override - Widget build(BuildContext context) { if (icons.isEmpty) return Container(); var icon = icons[0]; @@ -201,18 +180,11 @@ class _MessageSendStateIconState extends State { icon = Stack( alignment: Alignment.center, children: [ - // First icon (bottom icon) - icons[0], - - Transform( - transform: Matrix4.identity() - ..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half - ..translateByDouble(3, 5, 0, 1), - // Move down by 10 pixels (adjust as needed) - alignment: Alignment.center, + Transform.scale( + scale: 1.3, child: icons[1], ), - // Second icon (top icon, slightly offset) + icons[0], ], ); } @@ -223,7 +195,7 @@ class _MessageSendStateIconState extends State { icon, const SizedBox(width: 3), if (textWidget != null) - textWidget! + textWidget else Text( text, diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 5a17af7..626ea9b 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:lottie/lottie.dart'; @@ -163,17 +162,19 @@ class _MediaViewerViewState extends State { // } final stream = - twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId); + twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!); var downloadTriggered = false; await downloadStateListener?.cancel(); downloadStateListener = stream.listen((updated) async { if (updated == null) return; - if (updated.downloadState != DownloadState.downloaded) { + if (updated.downloadState != DownloadState.ready) { if (!downloadTriggered) { downloadTriggered = true; - await startDownloadMedia(currentMedia!.mediaFile, true); + final mediaFile = await twonlyDB.mediaFilesDao + .getMediaFileById(allMediaFiles.first.mediaId!); + await startDownloadMedia(mediaFile!, true); unawaited(tryDownloadAllMediaFiles(force: true)); } return; @@ -211,11 +212,6 @@ class _MediaViewerViewState extends State { [currentMessage!.messageId], ); - await twonlyDB.messagesDao.updateMessageId( - currentMessage!.messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), - ); - if (!currentMediaLocal.tempPath.existsSync()) { Log.error('Temp media file not found...'); await handleMediaError(currentMediaLocal.mediaFile); @@ -289,9 +285,10 @@ class _MediaViewerViewState extends State { pb.EncryptedContent( mediaUpdate: pb.EncryptedContent_MediaUpdate( type: pb.EncryptedContent_MediaUpdate_Type.STORED, - targetMediaId: currentMedia!.mediaFile.mediaId, + targetMessageId: currentMessage!.messageId, ), ), + null, ); setState(() { imageSaved = true; @@ -537,8 +534,7 @@ class _MediaViewerViewState extends State { ], ), ), - if (currentMedia?.mediaFile.downloadState != - DownloadState.downloaded) + if (currentMedia?.mediaFile.downloadState != DownloadState.ready) const Positioned.fill( child: Center( child: SizedBox( diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart index bea166e..524f8bd 100644 --- a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -52,6 +52,7 @@ class _EmojiReactionWidgetState extends State { emoji: widget.emoji, ), ), + null, ); setState(() { diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 2a0829c..0aad997 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -167,9 +167,9 @@ class UserList extends StatelessWidget { var directChat = await twonlyDB.groupsDao.getDirectChat(user.userId); if (directChat == null) { - await twonlyDB.groupsDao.insertGroup( + await twonlyDB.groupsDao.createNewDirectChat( + user.userId, GroupsCompanion( - isDirectChat: const Value(true), groupName: Value( getContactDisplayName(user), ), diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 2438382..9a2d263 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -49,12 +49,14 @@ class MemoriesViewState extends State { final applicationSupportDirectory = await getApplicationSupportDirectory(); for (final mediaFile in mediaFiles) { + final mediaService = MediaFileService( + mediaFile, + applicationSupportDirectory: applicationSupportDirectory, + ); + if (!mediaService.imagePreviewAvailable) continue; galleryItems.add( MemoryItem( - mediaService: MediaFileService( - mediaFile, - applicationSupportDirectory: applicationSupportDirectory, - ), + mediaService: mediaService, messages: [], ), ); diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index cab777e..d4434d4 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -37,13 +37,19 @@ class _MemoriesItemThumbnailState extends State { @override Widget build(BuildContext context) { + final media = widget.galleryItem.mediaService; return GestureDetector( onTap: widget.onTap, child: Hero( - tag: widget.galleryItem.mediaService.mediaFile.mediaId, + tag: media.mediaFile.mediaId, child: Stack( children: [ - Image.file(widget.galleryItem.mediaService.thumbnailPath), + if (media.thumbnailPath.existsSync()) + Image.file(media.thumbnailPath) + else if (media.storedPath.existsSync()) + Image.file(media.storedPath) + else + const Text('Media file removed.'), if (widget.galleryItem.mediaService.mediaFile.type == MediaType.video) const Positioned.fill( diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 7f6dd27..4a1b97a 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -84,7 +84,7 @@ class _RegisterViewState extends State { username: username, displayName: username, subscriptionPlan: 'Preview', - ); + )..appVersion = 62; await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index 52e17d8..89f3774 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; class AutomatedTestingView extends StatefulWidget { @@ -38,11 +39,13 @@ class _AutomatedTestingViewState extends State { 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); + 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++) { @@ -67,10 +70,10 @@ class _AutomatedTestingViewState extends State { Future showUserNameDialog( BuildContext context, -) { +) async { final controller = TextEditingController(); - return showDialog( + await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( @@ -97,4 +100,5 @@ Future showUserNameDialog( ); }, ); + return controller.text; } diff --git a/lib/src/views/settings/developer/retransmission_data.view.dart b/lib/src/views/settings/developer/retransmission_data.view.dart index 66fe34d..7beca18 100644 --- a/lib/src/views/settings/developer/retransmission_data.view.dart +++ b/lib/src/views/settings/developer/retransmission_data.view.dart @@ -1,7 +1,14 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; class RetransmissionDataView extends StatefulWidget { const RetransmissionDataView({super.key}); @@ -101,11 +108,48 @@ class _RetransmissionDataViewState extends State { Text( 'Server-Ack: ${retrans.receipt.ackByServerAt}', ), + if (retrans.receipt.messageId != null) + Text( + 'MessageId: ${retrans.receipt.messageId}', + ), + if (retrans.receipt.messageId != null) + FutureBuilder( + future: getPushNotificationFromEncryptedContent( + retrans.receipt.contactId, + retrans.receipt.messageId, + pb.EncryptedContent.fromBuffer( + pb.Message.fromBuffer(retrans.receipt.message) + .encryptedContent, + ), + ), + builder: (d, a) { + if (!a.hasData) return Container(); + return Text( + 'PushKind: ${a.data?.kind}', + ); + }, + ), Text( 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', ), ], ), + trailing: FilledButton.icon( + onPressed: () async { + final newReceiptId = uuid.v4(); + await twonlyDB.receiptsDao.updateReceipt( + retrans.receipt.receiptId, + ReceiptsCompanion( + receiptId: Value(newReceiptId), + ackByServerAt: const Value(null), + ), + ); + await tryToSendCompleteMessage( + receiptId: newReceiptId, + ); + }, + label: const FaIcon(FontAwesomeIcons.arrowRotateRight), + ), ), ) .toList(), diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index 6e8e663..f67943a 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -1,4 +1,20 @@ +import 'dart:io'; +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +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/database/twonly_database_old.dart' + show TwonlyDatabaseOld; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; class DatabaseMigrationView extends StatefulWidget { const DatabaseMigrationView({super.key}); @@ -8,8 +24,426 @@ class DatabaseMigrationView extends StatefulWidget { } class _DatabaseMigrationViewState extends State { + bool _isMigrating = false; + bool _isMigratingFinished = false; + int _contactsMigrated = 0; + int _storedMediaFiles = 0; + + Future startMigration() async { + setState(() { + _isMigrating = true; + }); + + final oldDatabase = TwonlyDatabaseOld(); + final oldContacts = await oldDatabase.contacts.select().get(); + final oldMessages = await oldDatabase.messages.select().get(); + + for (final oldContact in oldContacts) { + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + userId: Value(oldContact.userId), + username: Value(oldContact.username), + displayName: Value(oldContact.displayName), + nickName: Value(oldContact.nickName), + avatarSvg: Value(oldContact.avatarSvg), + senderProfileCounter: const Value(0), + accepted: Value(oldContact.accepted), + requested: Value(oldContact.requested), + blocked: Value(oldContact.blocked), + verified: Value(oldContact.verified), + deleted: Value(oldContact.deleted), + createdAt: Value(oldContact.createdAt), + ), + ); + setState(() { + _contactsMigrated += 1; + }); + if (!oldContact.deleted) { + final group = await twonlyDB.groupsDao.createNewDirectChat( + oldContact.userId, + GroupsCompanion( + pinned: Value(oldContact.pinned), + archived: Value(oldContact.archived), + groupName: Value(getContactDisplayNameOld(oldContact)), + totalMediaCounter: Value(oldContact.totalMediaCounter), + alsoBestFriend: Value(oldContact.alsoBestFriend), + createdAt: Value(oldContact.createdAt), + lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), + lastFlameSync: Value(oldContact.lastFlameSync), + lastMessageExchange: Value(oldContact.lastMessageExchange), + lastMessageReceived: Value(oldContact.lastMessageReceived), + lastMessageSend: Value(oldContact.lastMessageSend), + flameCounter: Value(oldContact.flameCounter), + ), + ); + if (group == null) continue; + for (final oldMessage in oldMessages) { + if (oldMessage.mediaUploadId == null && + oldMessage.mediaDownloadId == null) { + /// only interested in media files... + continue; + } + if (oldMessage.contactId != oldContact.userId) continue; + if (!oldMessage.mediaStored) continue; + + var storedMediaPath = + join((await getApplicationSupportDirectory()).path, 'media'); + if (oldMessage.mediaDownloadId != null) { + storedMediaPath = + '${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}'; + } else { + storedMediaPath = + '${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}'; + } + + var type = MediaType.image; + if (File('$storedMediaPath.mp4').existsSync()) { + type = MediaType.video; + storedMediaPath = '$storedMediaPath.mp4'; + } else if (File('$storedMediaPath.png').existsSync()) { + type = MediaType.image; + storedMediaPath = '$storedMediaPath.png'; + } else if (File('$storedMediaPath.webp').existsSync()) { + type = MediaType.image; + storedMediaPath = '$storedMediaPath.webp'; + } else { + continue; + } + + final uniqueId = Value( + getUUIDforDirectChat( + oldMessage.messageOtherId ?? oldMessage.messageId, + oldMessage.contactId ^ gUser.userId, + ), + ); + + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + mediaId: uniqueId, + stored: const Value(true), + type: Value(type), + createdAt: Value(oldMessage.sendAt), + ), + ); + if (mediaFile == null) continue; + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: uniqueId, + groupId: Value(group.groupId), + mediaId: uniqueId, + type: const Value(MessageType.media), + ), + ); + if (message == null) continue; + + final mediaService = await MediaFileService.fromMedia(mediaFile); + File(storedMediaPath).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); + } + } + } + + final memoriesPath = Directory( + join((await getApplicationSupportDirectory()).path, 'media', 'memories'), + ); + final files = memoriesPath.listSync(); + for (final file in files) { + if (file.path.contains('thumbnail')) continue; + final type = + file.path.contains('mp4') ? MediaType.video : MediaType.image; + final stat = FileStat.statSync(file.path); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + type: Value(type), + createdAt: Value(stat.modified), + ), + ); + final mediaService = await MediaFileService.fromMedia(mediaFile!); + File(file.path).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); + } + + final oldContactPreKeys = + await oldDatabase.signalContactPreKeys.select().get(); + for (final oldContactPreKey in oldContactPreKeys) { + try { + await twonlyDB + .into(twonlyDB.signalContactPreKeys) + .insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson())); + } catch (e) { + Log.error(e); + } + } + + final oldSignalSessionStores = + await oldDatabase.signalSessionStores.select().get(); + for (final oldSignalSessionStore in oldSignalSessionStores) { + try { + await twonlyDB.into(twonlyDB.signalSessionStores).insert( + SignalSessionStore.fromJson(oldSignalSessionStore.toJson())); + } catch (e) { + Log.error(e); + } + } + + final oldSignalSenderKeyStores = + await oldDatabase.signalSenderKeyStores.select().get(); + for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) { + try { + await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( + SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()), + ); + } catch (e) { + Log.error(e); + } + } + + final oldSignalPreyKeyStores = + await oldDatabase.signalPreKeyStores.select().get(); + for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) { + try { + await twonlyDB + .into(twonlyDB.signalPreKeyStores) + .insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson())); + } catch (e) { + Log.error(e); + } + } + + final oldSignalIdentityKeyStores = + await oldDatabase.signalIdentityKeyStores.select().get(); + for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) { + try { + await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( + SignalIdentityKeyStore.fromJson( + oldSignalIdentityKeyStore.toJson()), + ); + } catch (e) { + Log.error(e); + } + } + + final oldSignalContactSignedPreKeys = + await oldDatabase.signalContactSignedPreKeys.select().get(); + for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) { + try { + await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert( + SignalContactSignedPreKey.fromJson( + oldSignalContactSignedPreKey.toJson(), + ), + ); + } catch (e) { + Log.error(e); + } + } + + await updateUserdata((u) { + u.appVersion = 62; + return u; + }); + + setState(() { + _isMigratingFinished = true; + }); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(12), + child: _isMigratingFinished + ? ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'Deine Daten wurden migriert.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 40), + ...[ + '${_contactsMigrated} Kontakte', + '${_storedMediaFiles} gespeicherte Mediendateien', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + const SizedBox(height: 40), + const Text( + 'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12), + ), + const SizedBox(height: 30), + FilledButton( + onPressed: () { + Restart.restartApp( + notificationTitle: 'Deine Daten wurden migriert.', + notificationBody: 'Click here to open the app again', + ); + }, + child: const Text( + 'App neu starten', + ), + ), + ], + ) + : _isMigrating + ? ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'Deine Daten werden migriert.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 40), + const Center( + child: SizedBox( + width: 80, + height: 80, + child: CircularProgressIndicator(), + ), + ), + const SizedBox(height: 40), + const Text( + 'twonly während der Migration NICHT schließen!', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20, color: Colors.red), + ), + const SizedBox(height: 40), + const Text( + 'Aktueller Status', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + ...[ + '${_contactsMigrated} Kontakte', + '${_storedMediaFiles} gespeicherte Mediendateien', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + ], + ) + : ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'twonly. Jetzt besser als je zuvor.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 30), + const Text( + 'Das sind die neuen Features.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + const SizedBox(height: 10), + ...[ + 'Gruppen', + 'Nachrichten bearbeiten & löschen', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + const Text( + 'Technische Neuerungen', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 17), + ), + ...[ + 'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.', + 'Verwendung von UUIDs in der Datenbank', + 'Von Grund auf neues Datenbank-Schema', + 'Verbesserung der Zuverlässigkeit von C2C Nachrichten', + 'Verbesserung von Videos', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10), + ), + ), + const SizedBox(height: 50), + const Text( + 'Was bedeutet das für dich?', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + const Text( + 'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14), + ), + const SizedBox(height: 10), + const Text( + 'Was nach der Migration erhalten bleibt.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ...[ + 'Gespeicherte Bilder', + 'Kontakte', + 'Flammen', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(height: 10), + const Text( + 'Was durch die Migration verloren geht.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15, color: Colors.red), + ), + ...[ + 'Text-Nachrichten und Reaktionen', + 'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(height: 30), + FilledButton( + onPressed: startMigration, + child: const Text( + 'Jetzt starten', + ), + ), + ], + ), + ), + ); } } diff --git a/test/unit_test.dart b/test/unit_test.dart index 3466630..dafea36 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -21,5 +22,43 @@ void main() { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1))); }); + + test('Zero inputs produce all-zero UUID', () { + expect( + getUUIDforDirectChat(0, 0), + '00000000-0000-0000-0000-000000000000', + ); + expect(getUUIDforDirectChat(0, 0).length, uuid.v1().length); + }); + + test('Max int values (0x7fffffff)', () { + const max32 = 0x7fffffff; // 2147483647 + expect( + getUUIDforDirectChat(max32, max32), + '00000000-7fff-ffff-0000-00007fffffff', + ); + }); + + test('Bigger goes front', () { + expect( + getUUIDforDirectChat(1, 0), + '00000000-0000-0001-0000-000000000000', + ); + expect( + getUUIDforDirectChat(0, 1), + '00000000-0000-0001-0000-000000000000', + ); + }); + + test('Arbitrary within 32-bit range', () { + expect( + getUUIDforDirectChat(0x12345678, 0x0abcdef0), + '00000000-1234-5678-0000-00000abcdef0', + ); + }); + + test('Reject values > 0x7fffffff', () { + expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError); + }); }); } From b03bcfe6e1c024901c724d5f5a4f3a7fb016aead Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 01:15:54 +0200 Subject: [PATCH 14/76] fix analyser issues --- lib/main.dart | 6 +++--- lib/src/database/daos/groups.dao.dart | 12 +++++++----- lib/src/database/signal/connect_pre_key_store.dart | 3 ++- .../api/server_messages/media.server_messages.dart | 3 ++- .../views/updates/62_database_migration.view.dart | 14 ++++++++------ 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ce9f360..84a9378 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,9 @@ -import 'dart:io'; +// import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; +// import 'package:path/path.dart'; +// import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 92c24cd..6c902e4 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -57,12 +57,14 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { final result = await _insertGroup(insertGroup); if (result != null) { - await into(groupMembers).insert(GroupMembersCompanion( - groupId: Value(result.groupId), - contactId: Value( - contactId, + await into(groupMembers).insert( + GroupMembersCompanion( + groupId: Value(result.groupId), + contactId: Value( + contactId, + ), ), - )); + ); } return result; } diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 18fd5e6..60018c5 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -20,7 +20,8 @@ class ConnectPreKeyStore extends PreKeyStore { .get(); if (preKeyRecord.isEmpty) { throw InvalidKeyIdException( - '[PREKEY] No such preKey record! - $preKeyId'); + '[PREKEY] No such preKey record! - $preKeyId', + ); } Log.info('[PREKEY] Contact used my preKey $preKeyId'); final preKey = preKeyRecord.first.preKey; diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 5d2ee34..29afe75 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -121,7 +121,8 @@ Future handleMediaUpdate( .getSingleOrNull(); if (message == null) { Log.error( - 'Got media update to message ${mediaUpdate.targetMessageId} but message not found.'); + 'Got media update to message ${mediaUpdate.targetMessageId} but message not found.', + ); } final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!); diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index f67943a..1b7bd14 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -185,7 +185,8 @@ class _DatabaseMigrationViewState extends State { for (final oldSignalSessionStore in oldSignalSessionStores) { try { await twonlyDB.into(twonlyDB.signalSessionStores).insert( - SignalSessionStore.fromJson(oldSignalSessionStore.toJson())); + SignalSessionStore.fromJson(oldSignalSessionStore.toJson()), + ); } catch (e) { Log.error(e); } @@ -221,7 +222,8 @@ class _DatabaseMigrationViewState extends State { try { await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( SignalIdentityKeyStore.fromJson( - oldSignalIdentityKeyStore.toJson()), + oldSignalIdentityKeyStore.toJson(), + ), ); } catch (e) { Log.error(e); @@ -271,8 +273,8 @@ class _DatabaseMigrationViewState extends State { ), const SizedBox(height: 40), ...[ - '${_contactsMigrated} Kontakte', - '${_storedMediaFiles} gespeicherte Mediendateien', + '$_contactsMigrated Kontakte', + '$_storedMediaFiles gespeicherte Mediendateien', ].map( (e) => Text( e, @@ -333,8 +335,8 @@ class _DatabaseMigrationViewState extends State { style: TextStyle(fontSize: 20), ), ...[ - '${_contactsMigrated} Kontakte', - '${_storedMediaFiles} gespeicherte Mediendateien', + '$_contactsMigrated Kontakte', + '$_storedMediaFiles gespeicherte Mediendateien', ].map( (e) => Text( e, From edf4209448bc075eebef1850f61e3e0742ade763 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 18:54:54 +0100 Subject: [PATCH 15/76] add time to messages --- lib/src/localization/app_de.arb | 5 + lib/src/localization/app_en.arb | 5 + .../generated/app_localizations.dart | 30 ++++++ .../generated/app_localizations_de.dart | 15 +++ .../generated/app_localizations_en.dart | 15 +++ lib/src/views/chats/chat_messages.view.dart | 43 ++------- .../chat_date_chip.dart | 7 +- .../chat_list_entry.dart | 81 ++++++++++++++-- .../chat_text_entry.dart | 94 ++++++++++++++++++- .../response_container.dart | 11 ++- lib/src/views/components/better_text.dart | 8 +- 11 files changed, 260 insertions(+), 54 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 88444e2..6ca346c 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -175,6 +175,11 @@ "close": "Schließen", "cancel": "Abbrechen", "ok": "Ok", + "now": "Jetzt", + "you": "Du", + "minutesShort": "Min.", + "image": "Bild", + "video": "Video", "react": "Reagieren", "reply": "Antworten", "copy": "Kopieren", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 63ccda9..0e6e0dc 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -299,6 +299,11 @@ "disable": "Disable", "enable": "Enable", "cancel": "Cancel", + "now": "Now", + "you": "You", + "minutesShort": "min.", + "image": "Image", + "video": "Video", "react": "React", "reply": "Reply", "copy": "Copy", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 5f34009..443e715 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1058,6 +1058,36 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// No description provided for @now. + /// + /// In en, this message translates to: + /// **'Now'** + String get now; + + /// No description provided for @you. + /// + /// In en, this message translates to: + /// **'You'** + String get you; + + /// No description provided for @minutesShort. + /// + /// In en, this message translates to: + /// **'min.'** + String get minutesShort; + + /// No description provided for @image. + /// + /// In en, this message translates to: + /// **'Image'** + String get image; + + /// No description provided for @video. + /// + /// In en, this message translates to: + /// **'Video'** + String get video; + /// No description provided for @react. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 598a761..da18ff0 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -536,6 +536,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get now => 'Jetzt'; + + @override + String get you => 'Du'; + + @override + String get minutesShort => 'Min.'; + + @override + String get image => 'Bild'; + + @override + String get video => 'Video'; + @override String get react => 'Reagieren'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index e199353..32e6e2b 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -531,6 +531,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get now => 'Now'; + + @override + String get you => 'You'; + + @override + String get minutesShort => 'min.'; + + @override + String get image => 'Image'; + + @override + String get video => 'Video'; + @override String get react => 'React'; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 538d9b7..63f236a 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -28,22 +28,17 @@ Color getMessageColor(Message message) { } class ChatItem { - const ChatItem._({this.message, this.date, this.time}); + const ChatItem._({this.message, this.date}); factory ChatItem.date(DateTime date) { return ChatItem._(date: date); } - factory ChatItem.time(DateTime time) { - return ChatItem._(time: time); - } factory ChatItem.message(Message message) { return ChatItem._(message: message); } final Message? message; final DateTime? date; - final DateTime? time; bool get isMessage => message != null; bool get isDate => date != null; - bool get isTime => time != null; } /// Displays detailed information about a SampleItem. @@ -143,9 +138,6 @@ class _ChatMessagesViewState extends State { msg.createdAt.year != lastDate.year) { chatItems.add(ChatItem.date(msg.createdAt)); lastDate = msg.createdAt; - } else if (msg.createdAt.difference(lastDate).inMinutes >= 20) { - chatItems.add(ChatItem.time(msg.createdAt)); - lastDate = msg.createdAt; } chatItems.add(ChatItem.message(msg)); } @@ -276,7 +268,7 @@ class _ChatMessagesViewState extends State { padding: EdgeInsetsGeometry.only(top: 10), ); } - if (messages[i].isDate || messages[i].isTime) { + if (messages[i].isDate) { return ChatDateChip( item: messages[i], ); @@ -295,10 +287,14 @@ class _ChatMessagesViewState extends State { scale: (focusedScrollItem == i) ? 1.05 : 1, child: ChatListEntry( key: Key(chatMessage.messageId), - chatMessage, - group, - galleryItems, - isLastMessageFromSameUser(messages, i), + message: messages[i].message!, + nextMessage: + (i > 0) ? messages[i - 1].message : null, + prevMessage: ((i + 1) < messages.length) + ? messages[i + 1].message + : null, + group: group, + galleryItems: galleryItems, scrollToMessage: scrollToMessage, onResponseTriggered: () { setState(() { @@ -403,22 +399,3 @@ class _ChatMessagesViewState extends State { ); } } - -bool isLastMessageFromSameUser(List messages, int index) { - if (index <= 0) { - return true; // If there is no previous message, return true - } - return (messages[index - 1].message?.senderId == - messages[index].message?.senderId); -} - -double calculateNumberOfLines(String text, double width, double fontSize) { - final textPainter = TextPainter( - text: TextSpan( - text: text, - style: TextStyle(fontSize: fontSize), - ), - textDirection: TextDirection.ltr, - )..layout(maxWidth: width - 32); - return textPainter.computeLineMetrics().length.toDouble(); -} diff --git a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart b/lib/src/views/chats/chat_messages_components/chat_date_chip.dart index bfaeaa4..4df7be7 100644 --- a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart +++ b/lib/src/views/chats/chat_messages_components/chat_date_chip.dart @@ -9,10 +9,8 @@ class ChatDateChip extends StatelessWidget { @override Widget build(BuildContext context) { - final formattedDate = item.isTime - ? DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()) - .format(item.time!) - : '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}'; + final formattedDate = + '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}'; return Center( child: Container( @@ -23,6 +21,7 @@ class ChatDateChip extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + margin: const EdgeInsets.only(bottom: 20), child: Text( formattedDate, textAlign: TextAlign.center, diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 9c01271..e7a84d5 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -15,18 +15,20 @@ import 'package:twonly/src/views/chats/chat_messages_components/message_context_ import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; class ChatListEntry extends StatefulWidget { - const ChatListEntry( - this.message, - this.group, - this.galleryItems, - this.lastMessageFromSameUser, { + const ChatListEntry({ + required this.group, + required this.galleryItems, + required this.prevMessage, + required this.message, + required this.nextMessage, required this.onResponseTriggered, required this.scrollToMessage, super.key, }); + final Message? prevMessage; + final Message? nextMessage; final Message message; final Group group; - final bool lastMessageFromSameUser; final List galleryItems; final void Function(String) scrollToMessage; final void Function() onResponseTriggered; @@ -70,12 +72,16 @@ class _ChatListEntryState extends State { Widget build(BuildContext context) { final right = widget.message.senderId == null; + final (padding, borderRadius) = getMessageLayout( + widget.message, + widget.prevMessage, + widget.nextMessage, + ); + return Align( alignment: right ? Alignment.centerRight : Alignment.centerLeft, child: Padding( - padding: widget.lastMessageFromSameUser - ? const EdgeInsets.only(top: 5, right: 10, left: 10) - : const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), + padding: padding, child: MessageContextMenu( message: widget.message, onResponseTriggered: widget.onResponseTriggered, @@ -96,10 +102,13 @@ class _ChatListEntryState extends State { msg: widget.message, group: widget.group, mediaService: mediaService, + borderRadius: borderRadius, scrollToMessage: widget.scrollToMessage, child: (widget.message.type == MessageType.text) ? ChatTextEntry( message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, ) : (mediaService == null) ? null @@ -128,3 +137,57 @@ class _ChatListEntryState extends State { ); } } + +(EdgeInsetsGeometry, BorderRadius) getMessageLayout( + Message message, + Message? prevMessage, + Message? nextMessage, +) { + var bottom = 30.0; + var top = 0.0; + + var topLeft = 12.0; + var topRight = 12.0; + var bottomRight = 12.0; + var bottomLeft = 12.0; + + if (nextMessage != null) { + if (message.senderId == nextMessage.senderId) { + bottom = 10; + } + } + + if (prevMessage != null) { + final combinesWidthNext = combineTextMessageWithNext(prevMessage, message); + if (combinesWidthNext) { + top = 1; + topLeft = 5.0; + } + } + + final combinesWidthNext = combineTextMessageWithNext(message, nextMessage); + if (combinesWidthNext) { + bottom = 1; + bottomLeft = 5.0; + } + + if (message.senderId == null) { + final tmp = topLeft; + topLeft = topRight; + topRight = tmp; + + final tmp2 = bottomLeft; + bottomLeft = bottomRight; + bottomRight = tmp2; + } + + return ( + EdgeInsets.only(top: top, bottom: bottom, right: 10, left: 10), + BorderRadius.only( + topLeft: Radius.circular(topLeft), + topRight: Radius.circular(topRight), + bottomRight: Radius.circular(bottomRight), + bottomLeft: Radius.circular(bottomLeft), + ) + ); +} diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index c730270..47fd528 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' hide TextDirection; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/better_text.dart'; @@ -7,10 +10,14 @@ import 'package:twonly/src/views/components/better_text.dart'; class ChatTextEntry extends StatelessWidget { const ChatTextEntry({ required this.message, + required this.nextMessage, + required this.borderRadius, super.key, }); final Message message; + final Message? nextMessage; + final BorderRadius borderRadius; @override Widget build(BuildContext context) { @@ -27,17 +34,98 @@ class ChatTextEntry extends StatelessWidget { child: EmojiAnimation(emoji: text), ); } + + final displayTime = !combineTextMessageWithNext(message, nextMessage); + return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), - padding: const EdgeInsets.only(left: 10, top: 4, bottom: 4), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), decoration: BoxDecoration( color: message.quotesMessageId == null ? getMessageColor(message) : null, - borderRadius: BorderRadius.circular(12), + borderRadius: borderRadius, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (measureTextWidth(text) > 270) + Expanded( + child: BetterText(text: text), + ) + else + BetterText(text: text), + if (displayTime) + Padding( + padding: const EdgeInsets.only(left: 6), + child: Text( + friendlyTime(context, message.createdAt), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + ), + ), + ) + ], ), - child: BetterText(text: text), ); } } + +double measureTextWidth( + String text, +) { + final tp = TextPainter( + text: TextSpan(text: text, style: const TextStyle(fontSize: 17)), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return tp.size.width; +} + +bool combineTextMessageWithNext(Message message, Message? nextMessage) { + if (nextMessage != null && nextMessage.content != null) { + if (nextMessage.senderId == message.senderId) { + if (nextMessage.type == MessageType.text && + message.type == MessageType.text) { + if (!EmojiAnimation.supported(nextMessage.content!)) { + final diff = + nextMessage.createdAt.difference(message.createdAt).inMinutes; + if (diff <= 1) { + return true; + } + } + } + } + } + return false; +} + +String friendlyTime(BuildContext context, DateTime dt) { + final now = DateTime.now(); + final diff = now.difference(dt); + + if (diff.inMinutes >= 0 && diff.inMinutes < 60) { + final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes; + if (minutes <= 1) { + return context.lang.now; + } + return '$minutes ${context.lang.minutesShort}'; + } + + // Determine 24h vs 12h from system/local settings + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + if (!use24Hour) { + // 12-hour format with locale-aware AM/PM + final format = DateFormat.jm(Localizations.localeOf(context).toString()); + return format.format(dt); + } else { + // 24-hour HH:mm, locale-aware + final format = DateFormat.Hm(Localizations.localeOf(context).toString()); + return format.format(dt); + } +} diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index b46d6fd..ca4b9bb 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -14,6 +14,7 @@ class ResponseContainer extends StatefulWidget { required this.child, required this.scrollToMessage, required this.mediaService, + required this.borderRadius, super.key, }); @@ -21,6 +22,7 @@ class ResponseContainer extends StatefulWidget { final Widget? child; final Group group; final MediaFileService? mediaService; + final BorderRadius borderRadius; final void Function(String) scrollToMessage; @override @@ -69,7 +71,7 @@ class _ResponseContainerState extends State { ), decoration: BoxDecoration( color: getMessageColor(widget.msg), - borderRadius: BorderRadius.circular(12), + borderRadius: widget.borderRadius, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -157,11 +159,12 @@ class _ResponsePreviewState extends State { } } if (message!.type == MessageType.media && mediaService != null) { - subtitle = - mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image'; + subtitle = mediaService!.mediaFile.type == MediaType.video + ? context.lang.video + : context.lang.image; } - var username = 'You'; + var username = context.lang.you; if (message!.senderId != null) { username = message!.senderId.toString(); } diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index 2373f9f..d05d174 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -51,13 +51,19 @@ class BetterText extends StatelessWidget { } if (lastMatchEnd < text.length) { - spans.add(TextSpan(text: text.substring(lastMatchEnd))); + spans.add( + TextSpan( + text: text.substring(lastMatchEnd), + ), + ); } return Text.rich( TextSpan( children: spans, ), + softWrap: true, + overflow: TextOverflow.visible, style: const TextStyle( color: Colors.white, fontSize: 17, From afbad41b274781735bffeb18e2cb696bcc373322 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 21:57:43 +0100 Subject: [PATCH 16/76] reactions works --- lib/main.dart | 6 +- lib/src/database/daos/reactions.dao.dart | 36 +++- lib/src/database/tables/reactions.table.dart | 2 +- lib/src/database/twonly.db.g.dart | 2 +- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + .../reaction.server_message.dart | 2 +- .../all_reactions.bottom_sheet.dart | 147 ++++++++++++++++ .../chat_list_entry.dart | 69 +++++--- .../chat_reaction_row.dart | 160 ++++++++++-------- .../chat_text_entry.dart | 32 ++-- .../message_context_menu.dart | 9 +- .../emoji_reactions_row.component.dart | 10 +- lib/src/views/components/animate_icon.dart | 2 +- lib/src/views/components/better_text.dart | 1 + 18 files changed, 371 insertions(+), 125 deletions(-) create mode 100644 lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart diff --git a/lib/main.dart b/lib/main.dart index 84a9378..717bd6c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,9 @@ -// import 'dart:io'; +import 'dart:io'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// import 'package:path/path.dart'; -// import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 93f02a4..e0fa70b 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -1,12 +1,13 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; part 'reactions.dao.g.dart'; -@DriftAccessor(tables: [Reactions]) +@DriftAccessor(tables: [Reactions, Contacts]) class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -51,7 +52,36 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { .watch(); } - Future insertReaction(ReactionsCompanion reaction) async { - await into(reactions).insert(reaction); + Stream> watchReactionWithContacts( + String messageId, + ) { + final query = (select(reactions)).join( + [leftOuterJoin(contacts, contacts.userId.equalsExp(reactions.senderId))], + )..where(reactions.messageId.equals(messageId)); + + return query + .map((row) => (row.readTable(reactions), row.readTableOrNull(contacts))) + .watch(); + } + + Future updateMyReaction(String messageId, String? emoji) async { + try { + await (delete(reactions) + ..where( + (t) => t.senderId.isNull() & t.messageId.equals(messageId), + )) + .go(); + if (emoji != null) { + await into(reactions).insert( + ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: const Value(null), + ), + ); + } + } catch (e) { + Log.error(e); + } } } diff --git a/lib/src/database/tables/reactions.table.dart b/lib/src/database/tables/reactions.table.dart index 1f29c06..5c7a671 100644 --- a/lib/src/database/tables/reactions.table.dart +++ b/lib/src/database/tables/reactions.table.dart @@ -17,5 +17,5 @@ class Reactions extends Table { DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @override - Set get primaryKey => {messageId, senderId, createdAt}; + Set get primaryKey => {messageId, senderId, emoji}; } diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index c88339a..98faa9a 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -3332,7 +3332,7 @@ class $ReactionsTable extends Reactions } @override - Set get $primaryKey => {messageId, senderId, createdAt}; + Set get $primaryKey => {messageId, senderId, emoji}; @override Reaction map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 6ca346c..42bfe7e 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -340,5 +340,6 @@ "reportUserTitle": "Melde {username}", "reportUserReason": "Meldegrund", "reportUser": "Benutzer melden", - "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet." + "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", + "tabToRemoveEmoji": "Tippen um zu entfernen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 0e6e0dc..68a6418 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -496,5 +496,6 @@ "reportUserTitle": "Report {username}", "reportUserReason": "Reporting reason", "reportUser": "Report user", - "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here." + "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", + "tabToRemoveEmoji": "Tab to remove" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 443e715..b1a8873 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2083,6 +2083,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'You have logged in on another device. You have therefore been logged out here.'** String get newDeviceRegistered; + + /// No description provided for @tabToRemoveEmoji. + /// + /// In en, this message translates to: + /// **'Tab to remove'** + String get tabToRemoveEmoji; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index da18ff0..9b14157 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1106,4 +1106,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get newDeviceRegistered => 'Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.'; + + @override + String get tabToRemoveEmoji => 'Tippen um zu entfernen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 32e6e2b..56a5f19 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1100,4 +1100,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get newDeviceRegistered => 'You have logged in on another device. You have therefore been logged out here.'; + + @override + String get tabToRemoveEmoji => 'Tab to remove'; } diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/server_messages/reaction.server_message.dart index 892a27d..daf4247 100644 --- a/lib/src/services/api/server_messages/reaction.server_message.dart +++ b/lib/src/services/api/server_messages/reaction.server_message.dart @@ -12,8 +12,8 @@ Future handleReaction( if (reaction.remove) { await twonlyDB.reactionsDao .updateReaction(fromUserId, reaction.targetMessageId, groupId, null); + return; } - return; } if (reaction.hasEmoji()) { await twonlyDB.reactionsDao.updateReaction( diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart new file mode 100644 index 0000000..65513b7 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -0,0 +1,147 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; + +class AllReactionsView extends StatefulWidget { + const AllReactionsView({required this.message, super.key}); + + final Message message; + + @override + State createState() => _AllReactionsViewState(); +} + +class _AllReactionsViewState extends State { + StreamSubscription>? reactionsSub; + List<(Reaction, Contact?)> reactionsUsers = []; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + reactionsSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + final stream = twonlyDB.reactionsDao + .watchReactionWithContacts(widget.message.messageId); + + reactionsSub = stream.listen((update) { + setState(() { + reactionsUsers = update; + }); + }); + setState(() {}); + } + + Future removeReaction() async { + await twonlyDB.reactionsDao + .updateMyReaction(widget.message.messageId, null); + await sendCipherTextToGroup( + widget.message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: widget.message.messageId, + remove: true, + ), + ), + null, + ); + if (mounted) Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 400, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + color: context.color.surface, + boxShadow: const [ + BoxShadow( + blurRadius: 10.9, + color: Color.fromRGBO(0, 0, 0, 0.1), + ), + ], + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: ListView( + children: reactionsUsers.map((entry) { + return GestureDetector( + onTap: (entry.$2 != null) ? null : removeReaction, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 5, horizontal: 30), + margin: const EdgeInsets.only(left: 4), + child: Row( + children: [ + AvatarIcon( + contact: entry.$2, + userData: (entry.$2 == null) ? gUser : null, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (entry.$2 == null) + ? context.lang.you + : getContactDisplayName(entry.$2!), + style: const TextStyle(fontSize: 17), + ), + if (entry.$2 == null) + Text( + context.lang.tabToRemoveEmoji, + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), + Text( + entry.$1.emoji, + style: const TextStyle(fontSize: 25), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index e7a84d5..9da67a7 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -40,6 +40,9 @@ class ChatListEntry extends StatefulWidget { class _ChatListEntryState extends State { MediaFileService? mediaService; + List reactions = []; + StreamSubscription>? reactionsSub; + StreamSubscription? mediaFileSub; @override @@ -51,6 +54,7 @@ class _ChatListEntryState extends State { @override void dispose() { mediaFileSub?.cancel(); + reactionsSub?.cancel(); super.dispose(); } @@ -65,6 +69,14 @@ class _ChatListEntryState extends State { } }); } + final stream = + twonlyDB.reactionsDao.watchReactions(widget.message.messageId); + + reactionsSub = stream.listen((update) { + setState(() { + reactions = update; + }); + }); setState(() {}); } @@ -76,8 +88,14 @@ class _ChatListEntryState extends State { widget.message, widget.prevMessage, widget.nextMessage, + reactions.isNotEmpty, ); + final seen = {}; + var reactionsForWidth = + reactions.where((t) => seen.add(t.emoji)).toList().length; + if (reactionsForWidth > 4) reactionsForWidth = 4; + return Align( alignment: right ? Alignment.centerRight : Alignment.centerLeft, child: Padding( @@ -95,36 +113,46 @@ class _ChatListEntryState extends State { message: widget.message, onResponseTriggered: widget.onResponseTriggered, child: Stack( + // overflow: Overflow.visible, + // clipBehavior: Clip.none, alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ - ResponseContainer( - msg: widget.message, - group: widget.group, - mediaService: mediaService, - borderRadius: borderRadius, - scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - ) - : (mediaService == null) - ? null - : ChatMediaEntry( + Column( + children: [ + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - ), + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), + ), + if (reactionsForWidth > 0) + const SizedBox(height: 20, width: 10), + ], ), Positioned( - bottom: 5, + bottom: -20, left: 5, right: 5, child: ReactionRow( message: widget.message, + reactions: reactions, ), ), ], @@ -142,6 +170,7 @@ class _ChatListEntryState extends State { Message message, Message? prevMessage, Message? nextMessage, + bool hasReactions, ) { var bottom = 30.0; var top = 0.0; diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index 8f95acb..be40794 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -1,73 +1,38 @@ -import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; -class ReactionRow extends StatefulWidget { +class ReactionRow extends StatelessWidget { const ReactionRow({ + required this.reactions, required this.message, super.key, }); + final List reactions; final Message message; - @override - State createState() => _ReactionRowState(); -} - -class _ReactionRowState extends State { - List reactions = []; - StreamSubscription>? reactionsSub; - - @override - void initState() { - initAsync(); - super.initState(); - } - - @override - void dispose() { - reactionsSub?.cancel(); - super.dispose(); - } - - Future initAsync() async { - final stream = - twonlyDB.reactionsDao.watchReactions(widget.message.messageId); - - reactionsSub = stream.listen((update) { - setState(() { - reactions = update; - }); - }); + Future _showReactionMenu(BuildContext context) async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return AllReactionsView( + message: message, + ); + }, + ); + // if (layer == null) return; } @override Widget build(BuildContext context) { - final children = []; + final emojis = {}; for (final reaction in reactions) { - // if (content is ReopenedMediaFileContent) { - // if (hasOneReopened) continue; - // hasOneReopened = true; - // children.add( - // Expanded( - // child: Align( - // alignment: Alignment.bottomRight, - // child: Padding( - // padding: const EdgeInsets.only(right: 3), - // child: FaIcon( - // FontAwesomeIcons.repeat, - // size: 12, - // color: isDarkMode(context) ? Colors.white : Colors.black, - // ), - // ), - // ), - // ), - // ); - // } - // only show one reaction - late Widget child; if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) { child = SizedBox( @@ -75,24 +40,83 @@ class _ReactionRowState extends State { child: EmojiAnimation(emoji: reaction.emoji), ); } else { - child = Text(reaction.emoji, style: const TextStyle(fontSize: 14)); + child = SizedBox( + height: 18, + child: Center( + child: Text( + reaction.emoji, + style: const TextStyle(fontSize: 18), + strutStyle: const StrutStyle( + forceStrutHeight: true, + height: 1.6, + ), + ), + ), + ); + } + if (emojis.containsKey(reaction.emoji)) { + emojis[reaction.emoji] = + (emojis[reaction.emoji]!.$1, emojis[reaction.emoji]!.$2 + 1); + } else { + emojis[reaction.emoji] = (child, 1); } - children.insert( - 0, - Padding( - padding: const EdgeInsets.only(left: 3), - child: child, - ), - ); } - if (children.isEmpty) return Container(); + if (emojis.isEmpty) return Container(); - return Row( - mainAxisAlignment: widget.message.senderId == null - ? MainAxisAlignment.end - : MainAxisAlignment.end, - children: children, + var emojisToShow = emojis.values.toList() + ..sort((a, b) => b.$2.compareTo(a.$2)); + + if (emojisToShow.length > 4) { + emojisToShow = emojisToShow.slice(0, 3).toList() + ..add( + ( + SizedBox( + height: 18, + child: Transform.translate( + offset: const Offset(0, -3), + child: const FaIcon(FontAwesomeIcons.ellipsis), + ), + ), + 1 + ), + ); + } + + return GestureDetector( + onTap: () => _showReactionMenu(context), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.only(bottom: 20, top: 5), + child: Row( + mainAxisAlignment: message.senderId == null + ? MainAxisAlignment.start + : MainAxisAlignment.end, + children: emojisToShow.map((entry) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), + margin: const EdgeInsets.only(left: 4), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 74, 74, 74), + ), + child: Row( + children: [ + entry.$1, + if (entry.$2 > 1) + SizedBox( + height: 19, + child: Text( + entry.$2.toString(), + ), + ), + ], + ), + ); + }).toList(), + ), + ), ); } } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 47fd528..a09450d 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -12,12 +12,14 @@ class ChatTextEntry extends StatelessWidget { required this.message, required this.nextMessage, required this.borderRadius, + required this.minWidth, super.key, }); final Message message; final Message? nextMessage; final BorderRadius borderRadius; + final double minWidth; @override Widget build(BuildContext context) { @@ -37,9 +39,13 @@ class ChatTextEntry extends StatelessWidget { final displayTime = !combineTextMessageWithNext(message, nextMessage); + var spacerWidth = minWidth - measureTextWidth(text) - 53; + if (spacerWidth < 0) spacerWidth = 0; + return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, + minWidth: minWidth, ), padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), decoration: BoxDecoration( @@ -49,26 +55,32 @@ class ChatTextEntry extends StatelessWidget { ), child: Row( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (measureTextWidth(text) > 270) Expanded( child: BetterText(text: text), ) - else + else ...[ BetterText(text: text), + SizedBox( + width: spacerWidth, + ), + ], if (displayTime) - Padding( - padding: const EdgeInsets.only(left: 6), - child: Text( - friendlyTime(context, message.createdAt), - style: TextStyle( - fontSize: 10, - color: Colors.white.withAlpha(150), + Align( + alignment: AlignmentGeometry.centerRight, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Text( + friendlyTime(context, message.createdAt), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + ), ), ), - ) + ), ], ), ); diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 349f349..7d6f592 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,6 +1,5 @@ // ignore_for_file: inference_failure_on_function_invocation -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -48,12 +47,8 @@ class MessageContextMenu extends StatelessWidget { ) as EmojiLayerData?; if (layer == null) return; - await twonlyDB.reactionsDao.insertReaction( - ReactionsCompanion( - messageId: Value(message.messageId), - emoji: Value(layer.text), - ), - ); + await twonlyDB.reactionsDao + .updateMyReaction(message.messageId, layer.text); await sendCipherTextToGroup( message.groupId, diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart index 524f8bd..210b6f1 100644 --- a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -1,9 +1,7 @@ // ignore_for_file: avoid_dynamic_calls -import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -37,12 +35,8 @@ class _EmojiReactionWidgetState extends State { curve: Curves.linearToEaseOut, child: GestureDetector( onTap: () async { - await twonlyDB.reactionsDao.insertReaction( - ReactionsCompanion( - messageId: Value(widget.messageId), - emoji: Value(widget.emoji), - ), - ); + await twonlyDB.reactionsDao + .updateMyReaction(widget.messageId, widget.emoji); await sendCipherTextToGroup( widget.groupId, diff --git a/lib/src/views/components/animate_icon.dart b/lib/src/views/components/animate_icon.dart index 267a400..981c315 100644 --- a/lib/src/views/components/animate_icon.dart +++ b/lib/src/views/components/animate_icon.dart @@ -33,7 +33,7 @@ class EmojiAnimation extends StatelessWidget { '😭': 'loudly-crying.json', '🤯': 'mind-blown.json', '❤️‍🔥': 'red_heart_fire.json', - '😁': 'grinning.json', + //'😁': 'grinning.json', '😆': 'laughing.json', '😅': 'grin-sweat.json', '🤣': 'rofl.json', diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index d05d174..d5f27b2 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -63,6 +63,7 @@ class BetterText extends StatelessWidget { children: spans, ), softWrap: true, + textAlign: TextAlign.start, overflow: TextOverflow.visible, style: const TextStyle( color: Colors.white, From e65cf99ea6ecc93acf2021ecdbc1774baff63fdb Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 21:58:23 +0100 Subject: [PATCH 17/76] add ignore --- lib/main.dart | 7 +++++-- .../bottom_sheets/all_reactions.bottom_sheet.dart | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 717bd6c..cb8c754 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,12 @@ +// ignore_for_file: unused_import + import 'dart:io'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; + import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 65513b7..7ec8b6d 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -100,7 +100,9 @@ class _AllReactionsViewState extends State { onTap: (entry.$2 != null) ? null : removeReaction, child: Container( padding: const EdgeInsets.symmetric( - vertical: 5, horizontal: 30), + vertical: 5, + horizontal: 30, + ), margin: const EdgeInsets.only(left: 4), child: Row( children: [ From 746887b8455a9ffb453b722b019ea07e51139023 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 22:14:12 +0100 Subject: [PATCH 18/76] fix thumbnail generation for videos --- lib/src/services/mediafiles/mediafile.service.dart | 2 +- lib/src/views/memories/memories.view.dart | 8 ++++++-- lib/src/views/updates/62_database_migration.view.dart | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 744c54d..a32dc62 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -199,7 +199,7 @@ class MediaFileService { File get thumbnailPath => _buildFilePath( 'stored', namePrefix: '.thumbnail', - extensionParam: 'webp', + extensionParam: 'png', ); File get encryptedPath => _buildFilePath( 'tmp', diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 9a2d263..e44f48d 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.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/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -54,6 +55,11 @@ class MemoriesViewState extends State { applicationSupportDirectory: applicationSupportDirectory, ); if (!mediaService.imagePreviewAvailable) continue; + if (mediaService.mediaFile.type == MediaType.video) { + if (!mediaService.thumbnailPath.existsSync()) { + await mediaService.createThumbnail(); + } + } galleryItems.add( MemoryItem( mediaService: mediaService, @@ -146,7 +152,5 @@ class MemoriesViewState extends State { ), ) as bool?; setState(() {}); - - await initAsync(); } } diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index 1b7bd14..a0e53a3 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -159,6 +159,7 @@ class _DatabaseMigrationViewState extends State { MediaFilesCompanion( type: Value(type), createdAt: Value(stat.modified), + stored: const Value(true), ), ); final mediaService = await MediaFileService.fromMedia(mediaFile!); From 7fe0f16a5cfde5de611c11c5826a4fba1382d025 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 22:49:36 +0100 Subject: [PATCH 19/76] updating profile and minor issues --- lib/src/database/daos/contacts.dao.dart | 10 ++ lib/src/database/tables/messages.table.dart | 3 +- lib/src/database/twonly.db.g.dart | 105 ++---------------- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 4 + .../generated/app_localizations_en.dart | 3 + lib/src/services/api/messages.dart | 2 + .../contact.server_messages.dart | 26 +++-- lib/src/views/chats/chat_messages.view.dart | 2 +- .../chat_text_entry.dart | 2 +- .../message_send_state_icon.dart | 12 +- .../response_container.dart | 91 ++++++++------- lib/src/views/chats/media_viewer.view.dart | 28 +---- 15 files changed, 124 insertions(+), 176 deletions(-) diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 898297c..126b03d 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly_database_old.dart' as old; @@ -45,6 +46,15 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { final contact = await getContactByUserId(userId).getSingleOrNull(); if (contact != null) { await updatePushUser(contact); + final group = await twonlyDB.groupsDao.getDirectChat(userId); + if (group != null) { + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); + } } } } diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index cf18f12..628a720 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -26,8 +26,7 @@ class Messages extends Table { BlobColumn get downloadToken => blob().nullable()(); - TextColumn get quotesMessageId => - text().nullable().references(Messages, #messageId)(); + TextColumn get quotesMessageId => text().nullable()(); BoolColumn get isDeletedFromSender => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 98faa9a..0a501bb 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2265,10 +2265,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { @override late final GeneratedColumn quotesMessageId = GeneratedColumn( 'quotes_message_id', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES messages (message_id)')); + type: DriftSqlType.string, requiredDuringInsert: false); static const VerificationMeta _isDeletedFromSenderMeta = const VerificationMeta('isDeletedFromSender'); @override @@ -8207,21 +8204,6 @@ final class $$MessagesTableReferences manager.$state.copyWith(prefetchedData: [item])); } - static $MessagesTable _quotesMessageIdTable(_$TwonlyDB db) => - db.messages.createAlias($_aliasNameGenerator( - db.messages.quotesMessageId, db.messages.messageId)); - - $$MessagesTableProcessedTableManager? get quotesMessageId { - final $_column = $_itemColumn('quotes_message_id'); - if ($_column == null) return null; - final manager = $$MessagesTableTableManager($_db, $_db.messages) - .filter((f) => f.messageId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_quotesMessageIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - static MultiTypedResultKey<$MessageHistoriesTable, List> _messageHistoriesRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable(db.messageHistories, @@ -8315,6 +8297,10 @@ class $$MessagesTableFilterComposer ColumnFilters get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + ColumnFilters get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, + builder: (column) => ColumnFilters(column)); + ColumnFilters get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnFilters(column)); @@ -8394,26 +8380,6 @@ class $$MessagesTableFilterComposer return composer; } - $$MessagesTableFilterComposer get quotesMessageId { - final $$MessagesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.quotesMessageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableFilterComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - Expression messageHistoriesRefs( Expression Function($$MessageHistoriesTableFilterComposer f) f) { final $$MessageHistoriesTableFilterComposer composer = $composerBuilder( @@ -8524,6 +8490,10 @@ class $$MessagesTableOrderingComposer column: $table.downloadToken, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnOrderings(column)); @@ -8602,26 +8572,6 @@ class $$MessagesTableOrderingComposer )); return composer; } - - $$MessagesTableOrderingComposer get quotesMessageId { - final $$MessagesTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.quotesMessageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableOrderingComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$MessagesTableAnnotationComposer @@ -8648,6 +8598,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => column); + GeneratedColumn get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, builder: (column) => column); + GeneratedColumn get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => column); @@ -8726,26 +8679,6 @@ class $$MessagesTableAnnotationComposer return composer; } - $$MessagesTableAnnotationComposer get quotesMessageId { - final $$MessagesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.quotesMessageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableAnnotationComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - Expression messageHistoriesRefs( Expression Function($$MessageHistoriesTableAnnotationComposer a) f) { final $$MessageHistoriesTableAnnotationComposer composer = $composerBuilder( @@ -8846,7 +8779,6 @@ class $$MessagesTableTableManager extends RootTableManager< {bool groupId, bool senderId, bool mediaId, - bool quotesMessageId, bool messageHistoriesRefs, bool reactionsRefs, bool receiptsRefs, @@ -8941,7 +8873,6 @@ class $$MessagesTableTableManager extends RootTableManager< {groupId = false, senderId = false, mediaId = false, - quotesMessageId = false, messageHistoriesRefs = false, reactionsRefs = false, receiptsRefs = false, @@ -8997,17 +8928,6 @@ class $$MessagesTableTableManager extends RootTableManager< $$MessagesTableReferences._mediaIdTable(db).mediaId, ) as T; } - if (quotesMessageId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.quotesMessageId, - referencedTable: - $$MessagesTableReferences._quotesMessageIdTable(db), - referencedColumn: $$MessagesTableReferences - ._quotesMessageIdTable(db) - .messageId, - ) as T; - } return state; }, @@ -9086,7 +9006,6 @@ typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< {bool groupId, bool senderId, bool mediaId, - bool quotesMessageId, bool messageHistoriesRefs, bool reactionsRefs, bool receiptsRefs, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 42bfe7e..f721f6a 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -341,5 +341,6 @@ "reportUserReason": "Meldegrund", "reportUser": "Benutzer melden", "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", - "tabToRemoveEmoji": "Tippen um zu entfernen" + "tabToRemoveEmoji": "Tippen um zu entfernen", + "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 68a6418..9d1c04d 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -497,5 +497,6 @@ "reportUserReason": "Reporting reason", "reportUser": "Report user", "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", - "tabToRemoveEmoji": "Tab to remove" + "tabToRemoveEmoji": "Tab to remove", + "quotedMessageWasDeleted": "The quoted message has been deleted." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b1a8873..9191830 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2089,6 +2089,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Tab to remove'** String get tabToRemoveEmoji; + + /// No description provided for @quotedMessageWasDeleted. + /// + /// In en, this message translates to: + /// **'The quoted message has been deleted.'** + String get quotedMessageWasDeleted; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 9b14157..614afb5 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1109,4 +1109,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tabToRemoveEmoji => 'Tippen um zu entfernen'; + + @override + String get quotedMessageWasDeleted => + 'Die zitierte Nachricht wurde gelöscht.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 56a5f19..523ae87 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1103,4 +1103,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tabToRemoveEmoji => 'Tab to remove'; + + @override + String get quotedMessageWasDeleted => 'The quoted message has been deleted.'; } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 8c3408c..31cc9ee 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -214,6 +214,8 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( bool onlyReturnEncryptedData = false, String? messageId, }) async { + encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); + final response = pb.Message() ..type = pb.Message_Type.CIPHERTEXT ..encryptedContent = encryptedContent.writeToBuffer(); diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index 2b3519e..fc96a85 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -106,19 +106,21 @@ Future checkForProfileUpdate( ) async { int? senderProfileCounter; - if (content.hasSenderProfileCounter() && !content.hasContactUpdate()) { + if (content.hasSenderProfileCounter()) { senderProfileCounter = content.senderProfileCounter.toInt(); - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact != null) { - if (contact.senderProfileCounter < senderProfileCounter) { - await sendCipherText( - fromUserId, - EncryptedContent() - ..contactUpdate = (EncryptedContent_ContactUpdate() - ..type = EncryptedContent_ContactUpdate_Type.REQUEST), - ); + if (!content.hasContactUpdate()) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.senderProfileCounter < senderProfileCounter) { + await sendCipherText( + fromUserId, + EncryptedContent() + ..contactUpdate = (EncryptedContent_ContactUpdate() + ..type = EncryptedContent_ContactUpdate_Type.REQUEST), + ); + } } } } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 63f236a..6d6ef42 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -277,7 +277,7 @@ class _ChatMessagesViewState extends State { return Transform.translate( offset: Offset( (focusedScrollItem == i) - ? (chatMessage.quotesMessageId == null) + ? (chatMessage.senderId == null) ? -8 : 8 : 0, diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index a09450d..3154ce2 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -57,7 +57,7 @@ class ChatTextEntry extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (measureTextWidth(text) > 270) + if (measureTextWidth(text) > 270 || message.quotesMessageId != null) Expanded( child: BetterText(text: text), ) diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 2354fd0..2cfe578 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -76,12 +76,14 @@ class _MessageSendStateIconState extends State { @override Widget build(BuildContext context) { - final icons = []; + var icons = []; var text = ''; Widget? textWidget; textWidget = null; final kindsAlreadyShown = HashSet(); + var hasLoader = false; + for (final message in widget.messages) { if (icons.length == 2) break; if (kindsAlreadyShown.contains(message.type)) continue; @@ -130,6 +132,7 @@ class _MessageSendStateIconState extends State { if (mediaFile.downloadState == DownloadState.downloading) { text = context.lang.messageSendState_Loading; icon = getLoaderIcon(color); + hasLoader = true; } } case MessageSendState.send: @@ -139,9 +142,11 @@ class _MessageSendStateIconState extends State { case MessageSendState.sending: icon = getLoaderIcon(color); text = context.lang.messageSendState_Sending; + hasLoader = true; case MessageSendState.receiving: icon = getLoaderIcon(color); text = context.lang.messageSendState_Received; + hasLoader = true; } if (message.mediaStored) { @@ -165,6 +170,11 @@ class _MessageSendStateIconState extends State { } } + if (hasLoader) { + icons = [icon]; + break; + } + if (message.type == MessageType.media) { icons.insert(0, icon); } else { diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index ca4b9bb..4752b89 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -150,53 +150,58 @@ class _ResponsePreviewState extends State { @override Widget build(BuildContext context) { - if (message == null) return Container(); String? subtitle; + var color = const Color.fromARGB(233, 68, 137, 255); + var username = ''; - if (message!.type == MessageType.text) { - if (message!.content != null) { - subtitle = truncateString(message!.content!); + if (message != null) { + if (message!.type == MessageType.text) { + if (message!.content != null) { + subtitle = truncateString(message!.content!); + } + } + if (message!.type == MessageType.media && mediaService != null) { + subtitle = mediaService!.mediaFile.type == MediaType.video + ? context.lang.video + : context.lang.image; } - } - if (message!.type == MessageType.media && mediaService != null) { - subtitle = mediaService!.mediaFile.type == MediaType.video - ? context.lang.video - : context.lang.image; - } - var username = context.lang.you; - if (message!.senderId != null) { - username = message!.senderId.toString(); - } + username = context.lang.you; + if (message!.senderId != null) { + username = message!.senderId.toString(); + } - final color = getMessageColor(message!); + color = getMessageColor(message!); - if (!message!.mediaStored) { - return Container( - padding: widget.showBorder - ? const EdgeInsets.only(left: 10, right: 10) - : const EdgeInsets.symmetric(horizontal: 5), - decoration: (widget.showBorder) - ? BoxDecoration( - border: Border( - left: BorderSide( - color: color, - width: 2, + if (!message!.mediaStored) { + return Container( + padding: widget.showBorder + ? const EdgeInsets.only(left: 10, right: 10) + : const EdgeInsets.symmetric(horizontal: 5), + decoration: (widget.showBorder) + ? BoxDecoration( + border: Border( + left: BorderSide( + color: color, + width: 2, + ), ), - ), - ) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - username, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - if (subtitle != null) Text(subtitle), - ], - ), - ); + ) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + username, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (subtitle != null) Text(subtitle), + ], + ), + ); + } + } else { + username = context.lang.quotedMessageWasDeleted; } return Container( @@ -227,7 +232,11 @@ class _ResponsePreviewState extends State { if (mediaService != null) SizedBox( height: widget.showBorder ? 100 : 210, - child: Image.file(mediaService!.thumbnailPath), + child: Image.file( + mediaService!.mediaFile.type == MediaType.video + ? mediaService!.thumbnailPath + : mediaService!.storedPath, + ), ), ], ), diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 626ea9b..e1ad7dc 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -469,27 +469,6 @@ class _MediaViewerViewState extends State { child: Image.file( currentMedia!.tempPath, fit: BoxFit.contain, - frameBuilder: ( - context, - child, - frame, - wasSynchronouslyLoaded, - ) { - if (wasSynchronouslyLoaded) return child; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: frame != null - ? child - : Container( - height: 60, - color: Colors.transparent, - width: 60, - child: const CircularProgressIndicator( - strokeWidth: 2, - ), - ), - ); - }, ), ), ], @@ -534,13 +513,16 @@ class _MediaViewerViewState extends State { ], ), ), - if (currentMedia?.mediaFile.downloadState != DownloadState.ready) + if (currentMedia != null && + currentMedia?.mediaFile.downloadState != DownloadState.ready) const Positioned.fill( child: Center( child: SizedBox( height: 60, width: 60, - child: CircularProgressIndicator(strokeWidth: 6), + child: CircularProgressIndicator( + strokeWidth: 6, + ), ), ), ), From c67bd6b464f9779fbeb38f96e2c25143256aa5aa Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 26 Oct 2025 23:06:49 +0100 Subject: [PATCH 20/76] fixing multiple bugs --- README.md | 6 +- lib/src/views/camera/share_image_view.dart | 2 - .../chat_media_entry.dart | 27 +++++++- .../response_container.dart | 62 +++++++++++-------- 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 33187be..09663de 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ This repository contains the complete source code of the [twonly](https://twonly - End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll) - No email or phone number required to register - Privacy friendly - Everything is stored on the device +- Open-Source ## In work - For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM -- For Android: Reproducible Builds + Publishing on Github/F-Droid +- For Android: Reproducible Builds + Publishing F-Droid - Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata -- Maybe: Switching from the Signal Protocol to [MLS](https://openmls.tech/). ## Security Issues If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send @@ -33,8 +33,6 @@ guarantee a bounty currently :/ Some dependencies are downloaded directly from the source as there are some new changes which are not yet published on pub.dev or because they require some special installation. -- `flutter_secure_storage`: We need the 10.0.0-beta version, but this version has some issues which are fixed but [not yet published](https://github.com/juliansteenbakker/flutter_secure_storage/issues/866): - ```bash git submodule update --init --recursive diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 92cb735..1db8c71 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -65,7 +64,6 @@ class _ShareImageView extends State { await widget.mediaStoreFuture; } mediaStoreFutureReady = true; - await widget.mediaFileService.setUploadState(UploadState.preprocessing); unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 8afe01a..0337ba2 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -36,7 +36,30 @@ class ChatMediaEntry extends StatefulWidget { class _ChatMediaEntryState extends State { GlobalKey reopenMediaFile = GlobalKey(); - bool canBeReopened = false; + bool _canBeReopened = false; + + @override + void initState() { + super.initState(); + unawaited(initAsync()); + } + + Future initAsync() async { + if (widget.message.senderId == null || widget.message.mediaStored) { + return; + } + if (widget.mediaService.mediaFile.requiresAuthentication || + widget.mediaService.mediaFile.displayLimitInMilliseconds != null) { + return; + } + if (widget.mediaService.tempPath.existsSync()) { + if (mounted) { + setState(() { + _canBeReopened = true; + }); + } + } + } Future onDoubleTap() async { if (widget.message.openedAt == null || widget.message.mediaStored) { @@ -109,7 +132,7 @@ class _ChatMediaEntryState extends State { mediaService: widget.mediaService, color: color, galleryItems: widget.galleryItems, - canBeReopened: canBeReopened, + canBeReopened: _canBeReopened, ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 4752b89..3db2601 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; 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'; @@ -128,22 +129,34 @@ class ResponsePreview extends StatefulWidget { } class _ResponsePreviewState extends State { - Message? message; - MediaFileService? mediaService; + Message? _message; + MediaFileService? _mediaService; + String _username = ''; @override void initState() { - message = widget.message; + _message = widget.message; initAsync(); super.initState(); } Future initAsync() async { - message ??= await twonlyDB.messagesDao + _message ??= await twonlyDB.messagesDao .getMessageById(widget.messageId!) .getSingleOrNull(); - if (message?.mediaId != null) { - mediaService = await MediaFileService.fromMediaId(message!.mediaId!); + if (_message?.mediaId != null) { + _mediaService = await MediaFileService.fromMediaId(_message!.mediaId!); + } + if (_message?.senderId != null) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(_message!.senderId!) + .getSingleOrNull(); + if (contact != null) { + _username = getContactDisplayName(contact); + } + } + if (_message == null && mounted) { + _username = context.lang.quotedMessageWasDeleted; } if (mounted) setState(() {}); } @@ -152,28 +165,27 @@ class _ResponsePreviewState extends State { Widget build(BuildContext context) { String? subtitle; var color = const Color.fromARGB(233, 68, 137, 255); - var username = ''; - if (message != null) { - if (message!.type == MessageType.text) { - if (message!.content != null) { - subtitle = truncateString(message!.content!); + if (_message != null) { + if (_message!.type == MessageType.text) { + if (_message!.content != null) { + subtitle = truncateString(_message!.content!); } } - if (message!.type == MessageType.media && mediaService != null) { - subtitle = mediaService!.mediaFile.type == MediaType.video + if (_message!.type == MessageType.media && _mediaService != null) { + subtitle = _mediaService!.mediaFile.type == MediaType.video ? context.lang.video : context.lang.image; } - username = context.lang.you; - if (message!.senderId != null) { - username = message!.senderId.toString(); + if (_message!.senderId == null) { + _username = context.lang.you; + // _username = _message!.senderId.toString(); } - color = getMessageColor(message!); + color = getMessageColor(_message!); - if (!message!.mediaStored) { + if (!_message!.mediaStored) { return Container( padding: widget.showBorder ? const EdgeInsets.only(left: 10, right: 10) @@ -192,7 +204,7 @@ class _ResponsePreviewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - username, + _username, style: const TextStyle(fontWeight: FontWeight.bold), ), if (subtitle != null) Text(subtitle), @@ -200,8 +212,6 @@ class _ResponsePreviewState extends State { ), ); } - } else { - username = context.lang.quotedMessageWasDeleted; } return Container( @@ -222,20 +232,20 @@ class _ResponsePreviewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - username, + _username, style: const TextStyle(fontWeight: FontWeight.bold), ), if (subtitle != null) Text(subtitle), ], ), ), - if (mediaService != null) + if (_mediaService != null) SizedBox( height: widget.showBorder ? 100 : 210, child: Image.file( - mediaService!.mediaFile.type == MediaType.video - ? mediaService!.thumbnailPath - : mediaService!.storedPath, + _mediaService!.mediaFile.type == MediaType.video + ? _mediaService!.thumbnailPath + : _mediaService!.storedPath, ), ), ], From 8f8f2cabe0eed9b8628c17ea70c8bd0ed1ad7b05 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 00:18:05 +0100 Subject: [PATCH 21/76] message deletion --- lib/main.dart | 3 +- lib/src/database/daos/messages.dao.dart | 9 ++- lib/src/database/tables/messages.table.dart | 2 +- lib/src/database/twonly.db.g.dart | 4 +- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 10 ++- .../generated/app_localizations_de.dart | 5 +- .../generated/app_localizations_en.dart | 5 +- .../chat_list_entry.dart | 79 +++++++++++-------- .../chat_text_entry.dart | 27 +++++-- .../message_context_menu.dart | 26 +++++- 12 files changed, 120 insertions(+), 56 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index cb8c754..a951611 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; @@ -19,8 +20,6 @@ import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'app.dart'; - void main() async { WidgetsFlutterBinding.ensureInitialized(); await initFCMService(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index e3e2d43..7425f5c 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -155,12 +155,15 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Future handleMessageDeletion( - int contactId, + int? contactId, String messageId, DateTime timestamp, ) async { final msg = await getMessageById(messageId).getSingleOrNull(); - if (msg == null || msg.senderId != contactId) return; + if (msg == null || msg.senderId != contactId) { + Log.error('Message does not exists or contact is not owner.'); + return; + } if (msg.mediaId != null) { await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) .go(); @@ -176,7 +179,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { await (update(messages) ..where( - (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), + (t) => t.messageId.equals(messageId), )) .write( const MessagesCompanion( diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 628a720..5a9aa3f 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -20,7 +20,7 @@ class Messages extends Table { TextColumn get content => text().nullable()(); TextColumn get mediaId => text() .nullable() - .references(MediaFiles, #mediaId, onDelete: KeyAction.cascade)(); + .references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)(); BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 0a501bb..4ec0de5 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2243,7 +2243,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES media_files (media_id) ON DELETE CASCADE')); + 'REFERENCES media_files (media_id) ON DELETE SET NULL')); static const VerificationMeta _mediaStoredMeta = const VerificationMeta('mediaStored'); @override @@ -6464,7 +6464,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { on: TableUpdateQuery.onTableName('media_files', limitUpdateKind: UpdateKind.delete), result: [ - TableUpdate('messages', kind: UpdateKind.delete), + TableUpdate('messages', kind: UpdateKind.update), ], ), WritePropagation( diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index f721f6a..25776e0 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -291,7 +291,8 @@ "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", "deleteTitle": "Bist du dir sicher?", - "deleteOkBtn": "Für mich löschen", + "deleteOkBtnForAll": "Für alle löschen", + "deleteOkBtnForMe": "Für mich löschen", "deleteImageTitle": "Bist du dir sicher?", "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", "backupNoticeTitle": "Kein Backup konfiguriert", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 9d1c04d..9f2cc07 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -443,7 +443,8 @@ "tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.", "memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.", "deleteTitle": "Are you sure?", - "deleteOkBtn": "Delete for me", + "deleteOkBtnForAll": "Delete for all", + "deleteOkBtnForMe": "Delete for me", "deleteImageTitle": "Are you sure?", "deleteImageBody": "The image will be irrevocably deleted.", "settingsBackup": "Backup", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 9191830..b8fd2e1 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1760,11 +1760,17 @@ abstract class AppLocalizations { /// **'Are you sure?'** String get deleteTitle; - /// No description provided for @deleteOkBtn. + /// No description provided for @deleteOkBtnForAll. + /// + /// In en, this message translates to: + /// **'Delete for all'** + String get deleteOkBtnForAll; + + /// No description provided for @deleteOkBtnForMe. /// /// In en, this message translates to: /// **'Delete for me'** - String get deleteOkBtn; + String get deleteOkBtnForMe; /// No description provided for @deleteImageTitle. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 614afb5..9aa1c2c 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -930,7 +930,10 @@ class AppLocalizationsDe extends AppLocalizations { String get deleteTitle => 'Bist du dir sicher?'; @override - String get deleteOkBtn => 'Für mich löschen'; + String get deleteOkBtnForAll => 'Für alle löschen'; + + @override + String get deleteOkBtnForMe => 'Für mich löschen'; @override String get deleteImageTitle => 'Bist du dir sicher?'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 523ae87..0394caa 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -924,7 +924,10 @@ class AppLocalizationsEn extends AppLocalizations { String get deleteTitle => 'Are you sure?'; @override - String get deleteOkBtn => 'Delete for me'; + String get deleteOkBtnForAll => 'Delete for all'; + + @override + String get deleteOkBtnForMe => 'Delete for me'; @override String get deleteImageTitle => 'Are you sure?'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 9da67a7..322c049 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -118,43 +118,52 @@ class _ChatListEntryState extends State { alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ - Column( - children: [ - ResponseContainer( - msg: widget.message, - group: widget.group, - mediaService: mediaService, - borderRadius: borderRadius, - scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - : (mediaService == null) - ? null - : ChatMediaEntry( - message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - ), - ), - if (reactionsForWidth > 0) - const SizedBox(height: 20, width: 10), - ], - ), - Positioned( - bottom: -20, - left: 5, - right: 5, - child: ReactionRow( + if (widget.message.isDeletedFromSender) + ChatTextEntry( message: widget.message, - reactions: reactions, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + else + Column( + children: [ + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), + ), + if (reactionsForWidth > 0) + const SizedBox(height: 20, width: 10), + ], + ), + if (!widget.message.isDeletedFromSender) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), ), - ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 3154ce2..1369ac3 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -23,7 +23,12 @@ class ChatTextEntry extends StatelessWidget { @override Widget build(BuildContext context) { - final text = message.content ?? ''; + var text = message.content ?? ''; + + if (message.isDeletedFromSender) { + text = 'Nachricht wurde gelöscht.'; + } + if (EmojiAnimation.supported(text)) { return Container( constraints: const BoxConstraints( @@ -37,11 +42,24 @@ class ChatTextEntry extends StatelessWidget { ); } - final displayTime = !combineTextMessageWithNext(message, nextMessage); + var displayTime = !combineTextMessageWithNext(message, nextMessage); var spacerWidth = minWidth - measureTextWidth(text) - 53; if (spacerWidth < 0) spacerWidth = 0; + Color? color; + var expanded = false; + if (message.quotesMessageId == null) { + color = getMessageColor(message); + } + if (message.isDeletedFromSender) { + color = context.color.surfaceBright; + displayTime = false; + } else if (measureTextWidth(text) > 270 || + message.quotesMessageId != null) { + expanded = true; + } + return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, @@ -49,15 +67,14 @@ class ChatTextEntry extends StatelessWidget { ), padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), decoration: BoxDecoration( - color: - message.quotesMessageId == null ? getMessageColor(message) : null, + color: color, borderRadius: borderRadius, ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (measureTextWidth(text) > 270 || message.quotesMessageId != null) + if (expanded) Expanded( child: BetterText(text: text), ) diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 7d6f592..1fab50d 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -85,10 +85,32 @@ class MessageContextMenu extends StatelessWidget { context, context.lang.deleteTitle, null, - customOk: context.lang.deleteOkBtn, + customOk: + (message.senderId == null && !message.isDeletedFromSender) + ? context.lang.deleteOkBtnForAll + : context.lang.deleteOkBtnForMe, ); if (delete) { - await twonlyDB.messagesDao.deleteMessagesById(message.messageId); + if (message.senderId == null && !message.isDeletedFromSender) { + await twonlyDB.messagesDao.handleMessageDeletion( + null, + message.messageId, + DateTime.now(), + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.DELETE, + senderMessageId: message.messageId, + ), + ), + null, + ); + } else { + await twonlyDB.messagesDao + .deleteMessagesById(message.messageId); + } } }, child: const FaIcon(FontAwesomeIcons.trash), From 4f68d22e07d6b504409fc46d795128b0fbd6acd9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 01:11:18 +0100 Subject: [PATCH 22/76] starting with message info page --- lib/src/database/daos/messages.dao.dart | 7 +- lib/src/localization/app_de.arb | 1 + lib/src/localization/app_en.arb | 1 + .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + .../chat_list_entry.dart | 137 ++++++------- .../chat_text_entry.dart | 29 ++- .../message_context_menu.dart | 183 ++++++++++++++---- lib/src/views/chats/message_info.view.dart | 83 ++++++++ 10 files changed, 342 insertions(+), 111 deletions(-) create mode 100644 lib/src/views/chats/message_info.view.dart diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7425f5c..d65c947 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -191,13 +191,13 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Future handleTextEdit( - int contactId, + int? contactId, String messageId, String text, DateTime timestamp, ) async { final msg = await getMessageById(messageId).getSingleOrNull(); - if (msg == null || msg.content == null || msg.senderId == contactId) { + if (msg == null || msg.content == null || msg.senderId != contactId) { return; } await into(messageHistories).insert( @@ -209,11 +209,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); await (update(messages) ..where( - (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), + (t) => t.messageId.equals(messageId), )) .write( MessagesCompanion( content: Value(text), + modifiedAt: Value(timestamp), ), ); } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 25776e0..c450638 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -174,6 +174,7 @@ "submit": "Abschicken", "close": "Schließen", "cancel": "Abbrechen", + "edit": "Bearbeiten", "ok": "Ok", "now": "Jetzt", "you": "Du", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 9f2cc07..039ffea 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -307,6 +307,7 @@ "react": "React", "reply": "Reply", "copy": "Copy", + "edit": "Edit", "delete": "Delete", "info": "Info", "ok": "Ok", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b8fd2e1..b65722b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1106,6 +1106,12 @@ abstract class AppLocalizations { /// **'Copy'** String get copy; + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + /// No description provided for @delete. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 9aa1c2c..4273b75 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -560,6 +560,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get copy => 'Kopieren'; + @override + String get edit => 'Bearbeiten'; + @override String get delete => 'Löschen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 0394caa..f58b9ed 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -555,6 +555,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copy => 'Copy'; + @override + String get edit => 'Edit'; + @override String get delete => 'Delete'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 322c049..4061a4a 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -23,6 +23,7 @@ class ChatListEntry extends StatefulWidget { required this.nextMessage, required this.onResponseTriggered, required this.scrollToMessage, + this.disableContextMenu = false, super.key, }); final Message? prevMessage; @@ -32,6 +33,7 @@ class ChatListEntry extends StatefulWidget { final List galleryItems; final void Function(String) scrollToMessage; final void Function() onResponseTriggered; + final bool disableContextMenu; @override State createState() => _ChatListEntryState(); @@ -96,81 +98,84 @@ class _ChatListEntryState extends State { reactions.where((t) => seen.add(t.emoji)).toList().length; if (reactionsForWidth > 4) reactionsForWidth = 4; - return Align( - alignment: right ? Alignment.centerRight : Alignment.centerLeft, - child: Padding( - padding: padding, - child: MessageContextMenu( + Widget child = Column( + mainAxisAlignment: + right ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: + right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + MessageActions( message: widget.message, onResponseTriggered: widget.onResponseTriggered, - child: Column( - mainAxisAlignment: - right ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: - right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + child: Stack( + // overflow: Overflow.visible, + // clipBehavior: Clip.none, + alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ - MessageActions( - message: widget.message, - onResponseTriggered: widget.onResponseTriggered, - child: Stack( - // overflow: Overflow.visible, - // clipBehavior: Clip.none, - alignment: - right ? Alignment.centerRight : Alignment.centerLeft, + if (widget.message.isDeletedFromSender) + ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + else + Column( children: [ - if (widget.message.isDeletedFromSender) - ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - else - Column( - children: [ - ResponseContainer( - msg: widget.message, - group: widget.group, - mediaService: mediaService, - borderRadius: borderRadius, - scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - : (mediaService == null) - ? null - : ChatMediaEntry( - message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - ), - ), - if (reactionsForWidth > 0) - const SizedBox(height: 20, width: 10), - ], - ), - if (!widget.message.isDeletedFromSender) - Positioned( - bottom: -20, - left: 5, - right: 5, - child: ReactionRow( - message: widget.message, - reactions: reactions, - ), - ), + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), + ), + if (reactionsForWidth > 0) + const SizedBox(height: 20, width: 10), ], ), - ), + if (!widget.message.isDeletedFromSender) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), + ), ], ), ), - ), + ], + ); + + if (!widget.disableContextMenu) { + child = MessageContextMenu( + message: widget.message, + group: widget.group, + onResponseTriggered: widget.onResponseTriggered, + child: child, + ); + } + + return Align( + alignment: right ? Alignment.centerRight : Alignment.centerLeft, + child: Padding(padding: padding, child: child), ); } } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 1369ac3..26c4c2f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart' hide TextDirection; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -89,12 +90,28 @@ class ChatTextEntry extends StatelessWidget { alignment: AlignmentGeometry.centerRight, child: Padding( padding: const EdgeInsets.only(left: 6), - child: Text( - friendlyTime(context, message.createdAt), - style: TextStyle( - fontSize: 10, - color: Colors.white.withAlpha(150), - ), + child: Row( + children: [ + if (message.modifiedAt != null) + Padding( + padding: const EdgeInsets.only(right: 5), + child: SizedBox( + height: 10, + child: FaIcon( + FontAwesomeIcons.pencil, + color: Colors.white.withAlpha(150), + size: 10, + ), + ), + ), + Text( + friendlyTime(context, message.createdAt), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + ), + ), + ], ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 1fab50d..54db4e4 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,10 +1,12 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' as pb; @@ -12,15 +14,18 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ required this.message, + required this.group, required this.child, required this.onResponseTriggered, super.key, }); + final Group group; final Widget child; final Message message; final VoidCallback onResponseTriggered; @@ -35,40 +40,52 @@ class MessageContextMenu extends StatelessWidget { } }, actions: [ - PieAction( - tooltip: Text(context.lang.react), - onSelect: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ) as EmojiLayerData?; - if (layer == null) return; + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.react), + onSelect: () async { + final layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const Emojis(); + }, + ) as EmojiLayerData?; + if (layer == null) return; - await twonlyDB.reactionsDao - .updateMyReaction(message.messageId, layer.text); + await twonlyDB.reactionsDao + .updateMyReaction(message.messageId, layer.text); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( - reaction: pb.EncryptedContent_Reaction( - targetMessageId: message.messageId, - emoji: layer.text, - remove: false, + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: message.messageId, + emoji: layer.text, + remove: false, + ), ), - ), - null, - ); - }, - child: const FaIcon(FontAwesomeIcons.faceLaugh), - ), - PieAction( - tooltip: Text(context.lang.reply), - onSelect: onResponseTriggered, - child: const FaIcon(FontAwesomeIcons.reply), - ), + null, + ); + }, + child: const FaIcon(FontAwesomeIcons.faceLaugh), + ), + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.reply), + onSelect: onResponseTriggered, + child: const FaIcon(FontAwesomeIcons.reply), + ), + if (!message.isDeletedFromSender && + message.senderId == null && + message.type == MessageType.text) + PieAction( + tooltip: Text(context.lang.edit), + onSelect: () async { + await editTextMessage(context, message); + }, + child: const FaIcon(FontAwesomeIcons.pencil), + ), if (message.content != null) PieAction( tooltip: Text(context.lang.copy), @@ -115,13 +132,107 @@ class MessageContextMenu extends StatelessWidget { }, child: const FaIcon(FontAwesomeIcons.trash), ), - // PieAction( - // tooltip: Text(context.lang.info), - // onSelect: () {}, - // child: const FaIcon(FontAwesomeIcons.circleInfo), - // ), + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.info), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MessageInfoView( + message: message, + group: group, + ); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.circleInfo), + ), ], child: child, ); } } + +Future editTextMessage(BuildContext context, Message message) async { + var newText = message.content; + final controller = TextEditingController(text: message.content); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: TextField( + controller: controller, + autofocus: true, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) => setState(() { + newText = value; + }), + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.characters, + ), + ), + ], + ), + ); + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.lang.cancel), + ), + TextButton( + onPressed: () async { + if (newText != null && + newText != message.content && + newText != '') { + final timestamp = DateTime.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, + ), + ), + ), + null, + ); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + child: Text(context.lang.ok), + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart new file mode 100644 index 0000000..3d198ec --- /dev/null +++ b/lib/src/views/chats/message_info.view.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; + +class MessageInfoView extends StatefulWidget { + const MessageInfoView({ + required this.message, + required this.group, + super.key, + }); + + final Message message; + final Group group; + + @override + State createState() => _MessageInfoViewState(); +} + +class _MessageInfoViewState extends State { + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + // watch message edit history + // watch message actions + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: ChatListEntry( + group: widget.group, + galleryItems: const [], + prevMessage: null, + message: widget.message, + disableContextMenu: true, + nextMessage: null, + onResponseTriggered: () {}, + scrollToMessage: (_) {}, + ), + ), + Row( + children: [ + const Text('Versendet'), + const SizedBox(width: 13), + Text(formatDateTime(context, widget.message.createdAt)), + ], + ), + // Row( + // children: [ + // Text("Empfangen"), + // SizedBox(width: 13), + // Text(formatDateTime(context, widget.message.ackByUser)), + // ], + // ) + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + const Text('Zugestelt an'), + ], + ), + ), + ); + } +} From 6bb18a5bd0aa0463feea2eace3b79fc4b8353508 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 01:26:07 +0100 Subject: [PATCH 23/76] use language file and do not capitalize input --- lib/src/localization/app_de.arb | 3 ++- lib/src/localization/app_en.arb | 3 ++- lib/src/localization/generated/app_localizations.dart | 6 ++++++ lib/src/localization/generated/app_localizations_de.dart | 3 +++ lib/src/localization/generated/app_localizations_en.dart | 3 +++ .../chats/chat_messages_components/chat_text_entry.dart | 2 +- .../chat_messages_components/message_context_menu.dart | 1 - 7 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index c450638..aa12dcc 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -344,5 +344,6 @@ "reportUser": "Benutzer melden", "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", "tabToRemoveEmoji": "Tippen um zu entfernen", - "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht." + "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", + "messageWasDeleted": "Nachricht wurde gelöscht." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 039ffea..6c8654d 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -500,5 +500,6 @@ "reportUser": "Report user", "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", "tabToRemoveEmoji": "Tab to remove", - "quotedMessageWasDeleted": "The quoted message has been deleted." + "quotedMessageWasDeleted": "The quoted message has been deleted.", + "messageWasDeleted": "Message has been deleted." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b65722b..4163bcf 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2107,6 +2107,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The quoted message has been deleted.'** String get quotedMessageWasDeleted; + + /// No description provided for @messageWasDeleted. + /// + /// In en, this message translates to: + /// **'Message has been deleted.'** + String get messageWasDeleted; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4273b75..5e94470 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1119,4 +1119,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get quotedMessageWasDeleted => 'Die zitierte Nachricht wurde gelöscht.'; + + @override + String get messageWasDeleted => 'Nachricht wurde gelöscht.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index f58b9ed..479cd81 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1112,4 +1112,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get quotedMessageWasDeleted => 'The quoted message has been deleted.'; + + @override + String get messageWasDeleted => 'Message has been deleted.'; } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 26c4c2f..233d884 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -27,7 +27,7 @@ class ChatTextEntry extends StatelessWidget { var text = message.content ?? ''; if (message.isDeletedFromSender) { - text = 'Nachricht wurde gelöscht.'; + text = context.lang.messageWasDeleted; } if (EmojiAnimation.supported(text)) { diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 54db4e4..765323e 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -183,7 +183,6 @@ Future editTextMessage(BuildContext context, Message message) async { decoration: const InputDecoration( border: OutlineInputBorder(), ), - textCapitalization: TextCapitalization.characters, ), ), ], From 3d5fc3e807a590265399ee75a7489befe8a2ffd3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 16:40:40 +0100 Subject: [PATCH 24/76] display message history --- lib/src/database/daos/messages.dao.dart | 38 ++++ lib/src/localization/app_de.arb | 8 +- lib/src/localization/app_en.arb | 8 +- .../generated/app_localizations.dart | 36 ++++ .../generated/app_localizations_de.dart | 18 ++ .../generated/app_localizations_en.dart | 18 ++ lib/src/services/api/messages.dart | 5 +- .../text_message.server_messages.dart | 1 + lib/src/utils/misc.dart | 24 +++ .../message_history.bottom_sheet.dart | 80 +++++++ .../chat_list_entry.dart | 133 ++++++------ .../chat_reaction_row.dart | 14 +- .../chat_text_entry.dart | 11 +- .../response_container.dart | 8 +- lib/src/views/chats/message_info.view.dart | 198 ++++++++++++++---- .../views/components/better_list_title.dart | 12 +- lib/src/views/components/better_text.dart | 2 + 17 files changed, 492 insertions(+), 122 deletions(-) create mode 100644 lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index d65c947..3189e66 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -68,6 +68,24 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .watch(); } + // Stream> watchMembersByGroupId(String groupId) { + // return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + // .watch(); + // } + + Stream> watchMembersByGroupId(String groupId) { + final query = (select(groupMembers).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(groupMembers.contactId), + ), + ]) + ..where(groupMembers.groupId.equals(groupId))); + return query + .map((row) => (row.readTable(groupMembers), row.readTable(contacts))) + .watch(); + } + Stream> watchMessageActionChanges(String messageId) { return (select(messageActions)..where((t) => t.messageId.equals(messageId))) .watch(); @@ -410,6 +428,26 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); } + Stream> watchMessageActions(String messageId) { + final query = (select(messageActions).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(messageActions.contactId), + ), + ]) + ..where(messageActions.messageId.equals(messageId))); + return query + .map((row) => (row.readTable(messageActions), row.readTable(contacts))) + .watch(); + } + + Stream> watchMessageHistory(String messageId) { + return (select(messageHistories) + ..where((t) => t.messageId.equals(messageId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + // Future> getMessagesByMediaUploadId(int mediaUploadId) async { // return (select(messages) // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index aa12dcc..b337e7f 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -345,5 +345,11 @@ "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", "tabToRemoveEmoji": "Tippen um zu entfernen", "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", - "messageWasDeleted": "Nachricht wurde gelöscht." + "messageWasDeleted": "Nachricht wurde gelöscht.", + "sent": "Versendet", + "sentTo": "Zugestellt an", + "received": "Empfangen", + "opened": "Geöffnet", + "waitingForInternet": "Warten auf Internet", + "editHistory": "Bearbeitungshistorie" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 6c8654d..3b91d4d 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -501,5 +501,11 @@ "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", "tabToRemoveEmoji": "Tab to remove", "quotedMessageWasDeleted": "The quoted message has been deleted.", - "messageWasDeleted": "Message has been deleted." + "messageWasDeleted": "Message has been deleted.", + "sent": "Delivered", + "sentTo": "Delivered to", + "received": "Received", + "opened": "Opened", + "waitingForInternet": "Waiting for internet", + "editHistory": "Edit history" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 4163bcf..fac03be 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2113,6 +2113,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Message has been deleted.'** String get messageWasDeleted; + + /// No description provided for @sent. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get sent; + + /// No description provided for @sentTo. + /// + /// In en, this message translates to: + /// **'Delivered to'** + String get sentTo; + + /// No description provided for @received. + /// + /// In en, this message translates to: + /// **'Received'** + String get received; + + /// No description provided for @opened. + /// + /// In en, this message translates to: + /// **'Opened'** + String get opened; + + /// No description provided for @waitingForInternet. + /// + /// In en, this message translates to: + /// **'Waiting for internet'** + String get waitingForInternet; + + /// No description provided for @editHistory. + /// + /// In en, this message translates to: + /// **'Edit history'** + String get editHistory; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 5e94470..653f33f 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1122,4 +1122,22 @@ class AppLocalizationsDe extends AppLocalizations { @override String get messageWasDeleted => 'Nachricht wurde gelöscht.'; + + @override + String get sent => 'Versendet'; + + @override + String get sentTo => 'Zugestellt an'; + + @override + String get received => 'Empfangen'; + + @override + String get opened => 'Geöffnet'; + + @override + String get waitingForInternet => 'Warten auf Internet'; + + @override + String get editHistory => 'Bearbeitungshistorie'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 479cd81..e797023 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1115,4 +1115,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get messageWasDeleted => 'Message has been deleted.'; + + @override + String get sent => 'Delivered'; + + @override + String get sentTo => 'Delivered to'; + + @override + String get received => 'Received'; + + @override + String get opened => 'Opened'; + + @override + String get waitingForInternet => 'Waiting for internet'; + + @override + String get editHistory => 'Edit history'; } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 31cc9ee..aa8a7ac 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -251,19 +251,22 @@ Future notifyContactAboutOpeningMessage( } Log.info('Opened messages: $messageOtherIds'); + final actionAt = DateTime.now(); + await sendCipherText( contactId, pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb.EncryptedContent_MessageUpdate_Type.OPENED, multipleTargetMessageIds: messageOtherIds, + timestamp: Int64(actionAt.millisecondsSinceEpoch), ), ), ); for (final messageId in messageOtherIds) { await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), + MessagesCompanion(openedAt: Value(actionAt)), ); } await updateLastMessageId(contactId, biggestMessageId); diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index c20d803..dfe6260 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -26,6 +26,7 @@ Future handleTextMessage( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), createdAt: Value(fromTimestamp(textMessage.timestamp)), + ackByServer: Value(DateTime.now()), ), ); if (message != null) { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index bd34ec1..b2adc93 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -348,3 +348,27 @@ String getUUIDforDirectChat(int a, int b) { ]; return parts.join('-'); } + +String friendlyDateTime( + BuildContext context, + DateTime dt, { + bool includeSeconds = false, + Locale? locale, +}) { + // Build date part + final datePart = + DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt); + + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + var timePart = ''; + if (use24Hour) { + timePart = + DateFormat.jm(Localizations.localeOf(context).toString()).format(dt); + } else { + timePart = + DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt); + } + + return '$timePart $datePart'; +} diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart new file mode 100644 index 0000000..7569305 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; + +class MessageHistoryView extends StatelessWidget { + const MessageHistoryView({ + required this.message, + required this.group, + required this.changes, + super.key, + }); + + final Message message; + final Group group; + final List changes; + + @override + Widget build(BuildContext context) { + final json = message.toJson(); + json['createdAt'] = message.modifiedAt; + final currentMessage = Message.fromJson(json); + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + color: context.color.surface, + boxShadow: const [ + BoxShadow( + blurRadius: 10.9, + color: Color.fromRGBO(0, 0, 0, 0.1), + ), + ], + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: ListView( + children: [ + ChatListEntry( + group: group, + message: currentMessage, + hideReactions: true, + ), + ...changes.map( + (change) { + final json = message.toJson(); + json['content'] = change.content; + json['createdAt'] = change.createdAt; + final msgChanged = Message.fromJson(json); + return ChatListEntry( + group: group, + message: msgChanged, + hideReactions: true, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 4061a4a..858a3cc 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -17,23 +17,23 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain class ChatListEntry extends StatefulWidget { const ChatListEntry({ required this.group, - required this.galleryItems, - required this.prevMessage, required this.message, - required this.nextMessage, - required this.onResponseTriggered, - required this.scrollToMessage, - this.disableContextMenu = false, + this.galleryItems = const [], + this.scrollToMessage, + this.onResponseTriggered, + this.prevMessage, + this.nextMessage, + this.hideReactions = false, super.key, }); final Message? prevMessage; final Message? nextMessage; final Message message; final Group group; + final bool hideReactions; final List galleryItems; - final void Function(String) scrollToMessage; - final void Function() onResponseTriggered; - final bool disableContextMenu; + final void Function(String)? scrollToMessage; + final void Function()? onResponseTriggered; @override State createState() => _ChatListEntryState(); @@ -98,77 +98,70 @@ class _ChatListEntryState extends State { reactions.where((t) => seen.add(t.emoji)).toList().length; if (reactionsForWidth > 4) reactionsForWidth = 4; - Widget child = Column( - mainAxisAlignment: - right ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: - right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + Widget child = Stack( + // overflow: Overflow.visible, + // clipBehavior: Clip.none, + alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ - MessageActions( - message: widget.message, - onResponseTriggered: widget.onResponseTriggered, - child: Stack( - // overflow: Overflow.visible, - // clipBehavior: Clip.none, - alignment: right ? Alignment.centerRight : Alignment.centerLeft, + if (widget.message.isDeletedFromSender) + ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + else + Column( children: [ - if (widget.message.isDeletedFromSender) - ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - else - Column( - children: [ - ResponseContainer( - msg: widget.message, - group: widget.group, - mediaService: mediaService, - borderRadius: borderRadius, - scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - : (mediaService == null) - ? null - : ChatMediaEntry( - message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - ), - ), - if (reactionsForWidth > 0) - const SizedBox(height: 20, width: 10), - ], - ), - if (!widget.message.isDeletedFromSender) - Positioned( - bottom: -20, - left: 5, - right: 5, - child: ReactionRow( - message: widget.message, - reactions: reactions, - ), - ), + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), + ), + if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), ], ), - ), + if (!widget.message.isDeletedFromSender && !widget.hideReactions) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), + ), ], ); - if (!widget.disableContextMenu) { + if (widget.onResponseTriggered != null) { + child = MessageActions( + message: widget.message, + onResponseTriggered: widget.onResponseTriggered!, + child: child, + ); + child = MessageContextMenu( message: widget.message, group: widget.group, - onResponseTriggered: widget.onResponseTriggered, + onResponseTriggered: widget.onResponseTriggered!, child: child, ); } diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index be40794..b0d6991 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -26,7 +27,6 @@ class ReactionRow extends StatelessWidget { ); }, ); - // if (layer == null) return; } @override @@ -99,7 +99,9 @@ class ReactionRow extends StatelessWidget { decoration: BoxDecoration( border: Border.all(), borderRadius: BorderRadius.circular(12), - color: const Color.fromARGB(255, 74, 74, 74), + color: isDarkMode(context) + ? const Color.fromARGB(255, 74, 74, 74) + : const Color.fromARGB(255, 197, 197, 197), ), child: Row( children: [ @@ -107,8 +109,16 @@ class ReactionRow extends StatelessWidget { if (entry.$2 > 1) SizedBox( height: 19, + width: 13, child: Text( entry.$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + color: Colors.black, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), ), ), ], diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 233d884..121fa94 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -26,10 +26,6 @@ class ChatTextEntry extends StatelessWidget { Widget build(BuildContext context) { var text = message.content ?? ''; - if (message.isDeletedFromSender) { - text = context.lang.messageWasDeleted; - } - if (EmojiAnimation.supported(text)) { return Container( constraints: const BoxConstraints( @@ -61,6 +57,11 @@ class ChatTextEntry extends StatelessWidget { expanded = true; } + if (message.isDeletedFromSender) { + text = context.lang.messageWasDeleted; + color = Colors.grey; + } + return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, @@ -109,6 +110,8 @@ class ChatTextEntry extends StatelessWidget { style: TextStyle( fontSize: 10, color: Colors.white.withAlpha(150), + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, ), ), ], diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 3db2601..404bb40 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -13,9 +13,9 @@ class ResponseContainer extends StatefulWidget { required this.msg, required this.group, required this.child, - required this.scrollToMessage, required this.mediaService, required this.borderRadius, + this.scrollToMessage, super.key, }); @@ -24,7 +24,7 @@ class ResponseContainer extends StatefulWidget { final Group group; final MediaFileService? mediaService; final BorderRadius borderRadius; - final void Function(String) scrollToMessage; + final void Function(String)? scrollToMessage; @override State createState() => _ResponseContainerState(); @@ -65,7 +65,9 @@ class _ResponseContainerState extends State { return widget.child!; } return GestureDetector( - onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!), + onTap: widget.scrollToMessage == null + ? null + : () => widget.scrollToMessage!(widget.msg.quotesMessageId!), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart index 3d198ec..36bb78f 100644 --- a/lib/src/views/chats/message_info.view.dart +++ b/lib/src/views/chats/message_info.view.dart @@ -1,7 +1,16 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; class MessageInfoView extends StatefulWidget { const MessageInfoView({ @@ -18,22 +27,132 @@ class MessageInfoView extends StatefulWidget { } class _MessageInfoViewState extends State { + StreamSubscription>? actionsStream; + StreamSubscription>? historyStream; + StreamSubscription>? groupMemberStream; + + List<(MessageAction, Contact)> messageActions = []; + List messageHistory = []; + List<(GroupMember, Contact)> groupMembers = []; + @override void initState() { initAsync(); super.initState(); } - Future initAsync() async { - // watch message edit history - // watch message actions - } - @override void dispose() { + actionsStream?.cancel(); + historyStream?.cancel(); + groupMemberStream?.cancel(); super.dispose(); } + Future initAsync() async { + final streamActions = + twonlyDB.messagesDao.watchMessageActions(widget.message.messageId); + actionsStream = streamActions.listen((update) { + setState(() { + messageActions = update; + }); + }); + + final streamGroup = + twonlyDB.messagesDao.watchMembersByGroupId(widget.message.groupId); + groupMemberStream = streamGroup.listen((update) { + setState(() { + groupMembers = update; + }); + }); + + final streamHistory = + twonlyDB.messagesDao.watchMessageHistory(widget.message.messageId); + historyStream = streamHistory.listen((update) { + setState(() { + messageHistory = update; + }); + }); + } + + List getReceivedColumns(BuildContext context) { + if (widget.message.senderId != null) return []; + + final columns = [ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Text(context.lang.sentTo), + const SizedBox(height: 10), + ]; + + for (final groupMember in groupMembers) { + final ackByServer = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByServerAt && + t.$2.userId == groupMember.$2.userId, + ); + final ackByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByUserAt && + t.$2.userId == groupMember.$2.userId, + ); + final openedByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.openedAt && + t.$2.userId == groupMember.$2.userId, + ); + + var actionTypeText = context.lang.waitingForInternet; + var actionAt = widget.message.createdAt; + if (ackByServer != null) { + actionTypeText = context.lang.sent; + actionAt = ackByServer.$1.actionAt; + } + if (ackByUser != null) { + actionTypeText = context.lang.received; + actionAt = ackByUser.$1.actionAt; + } + if (openedByUser != null) { + actionTypeText = context.lang.opened; + actionAt = openedByUser.$1.actionAt; + } + + columns.add( + Row( + children: [ + AvatarIcon( + contact: groupMember.$2, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getContactDisplayName(groupMember.$2), + style: const TextStyle(fontSize: 17), + ), + ], + ), + ), + Column( + children: [ + Text( + friendlyDateTime(context, actionAt), + style: const TextStyle(fontSize: 12), + ), + Text(actionTypeText), + ], + ), + ], + ), + ); + } + return columns; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -41,40 +160,47 @@ class _MessageInfoViewState extends State { title: const Text(''), ), body: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 24), child: ListView( children: [ - Padding( - padding: const EdgeInsets.all(8), - child: ChatListEntry( - group: widget.group, - galleryItems: const [], - prevMessage: null, - message: widget.message, - disableContextMenu: true, - nextMessage: null, - onResponseTriggered: () {}, - scrollToMessage: (_) {}, + const SizedBox(height: 20), + ChatListEntry( + group: widget.group, + message: widget.message, + ), + Text( + '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}', + ), + if (widget.message.senderId != null && + widget.message.ackByServer != null) + Text( + '${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}', ), - ), - Row( - children: [ - const Text('Versendet'), - const SizedBox(width: 13), - Text(formatDateTime(context, widget.message.createdAt)), - ], - ), - // Row( - // children: [ - // Text("Empfangen"), - // SizedBox(width: 13), - // Text(formatDateTime(context, widget.message.ackByUser)), - // ], - // ) - const SizedBox(height: 10), - const Divider(), - const SizedBox(height: 10), - const Text('Zugestelt an'), + if (messageHistory.isNotEmpty) ...[ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + BetterListTile( + icon: FontAwesomeIcons.pencil, + padding: EdgeInsets.zero, + text: context.lang.editHistory, + onTap: () async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return MessageHistoryView( + message: widget.message, + changes: messageHistory, + group: widget.group, + ); + }, + ); + }, + ), + ], + ...getReceivedColumns(context), ], ), ), diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index b7ad83c..c83eca2 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -10,6 +10,7 @@ class BetterListTile extends StatelessWidget { this.color, this.subtitle, this.iconSize = 20, + this.padding, }); final IconData icon; final String text; @@ -17,15 +18,18 @@ class BetterListTile extends StatelessWidget { final Color? color; final VoidCallback onTap; final double iconSize; + final EdgeInsets? padding; @override Widget build(BuildContext context) { return ListTile( leading: Padding( - padding: const EdgeInsets.only( - right: 10, - left: 19, - ), + padding: (padding == null) + ? const EdgeInsets.only( + right: 10, + left: 19, + ) + : padding!, child: FaIcon( icon, size: iconSize, diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index d5f27b2..97399a2 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -68,6 +68,8 @@ class BetterText extends StatelessWidget { style: const TextStyle( color: Colors.white, fontSize: 17, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, ), ); } From 350fb899e1a83e6aa19c075e4359fa3fe057cc71 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 20:14:07 +0100 Subject: [PATCH 25/76] display last reaction and compress avatarsvgs --- lib/src/database/daos/contacts.dao.dart | 27 +- lib/src/database/daos/groups.dao.dart | 39 ++- lib/src/database/daos/messages.dao.dart | 12 +- lib/src/database/daos/reactions.dao.dart | 18 ++ lib/src/database/tables/contacts.table.dart | 7 +- lib/src/database/twonly.db.g.dart | 264 ++++++++++++------ lib/src/localization/app_de.arb | 1 + lib/src/localization/app_en.arb | 1 + .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + .../client/generated/messages.pb.dart | 16 +- .../client/generated/messages.pbjson.dart | 33 +-- lib/src/model/protobuf/client/messages.proto | 2 +- lib/src/services/api.service.dart | 2 +- .../api/mediafiles/upload.service.dart | 2 + lib/src/services/api/messages.dart | 8 +- lib/src/services/api/server_messages.dart | 67 ++++- .../contact.server_messages.dart | 27 +- .../media.server_messages.dart | 2 + .../text_message.server_messages.dart | 4 + lib/src/services/api/utils.dart | 14 +- .../mediafiles/thumbnail.service.dart | 2 +- .../notifications/setup.notifications.dart | 8 +- lib/src/utils/misc.dart | 7 + lib/src/views/chats/add_new_user.view.dart | 26 +- lib/src/views/chats/chat_list.view.dart | 2 +- .../chat_list_components/group_list_item.dart | 24 +- .../chat_list_entry.dart | 6 +- .../message_send_state_icon.dart | 47 +++- .../components/avatar_icon.component.dart | 13 +- lib/src/views/contact/contact.view.dart | 2 +- .../updates/62_database_migration.view.dart | 206 +++++++------- 33 files changed, 604 insertions(+), 297 deletions(-) diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 126b03d..d1cff2e 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly_database_old.dart' as old; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; part 'contacts.dao.g.dart'; @@ -14,10 +15,20 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { // ignore: matching_super_parameters ContactsDao(super.db); - Future insertContact(ContactsCompanion contact) async { + Future insertContact(ContactsCompanion contact) async { try { return await into(contacts).insert(contact); } catch (e) { + Log.error(e); + return null; + } + } + + Future insertOnConflictUpdate(ContactsCompanion contact) async { + try { + return await into(contacts).insertOnConflictUpdate(contact); + } catch (e) { + Log.error(e); return 0; } } @@ -62,10 +73,12 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { Stream> watchNotAcceptedContacts() { return (select(contacts) ..where( - (t) => t.accepted.equals(false) & t.blocked.equals(false), + (t) => + t.accepted.equals(false) & + t.blocked.equals(false) & + t.deletedByUser.equals(false), )) .watch(); - // return (select(contacts)).watch(); } Stream watchContact(int userid) { @@ -74,7 +87,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { } Future> getAllNotBlockedContacts() { - return (select(contacts)..where((t) => t.blocked.equals(false))).get(); + return select(contacts).get(); } Stream watchContactsBlocked() { @@ -89,7 +102,9 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { final count = contacts.requested.count(distinct: true); final query = selectOnly(contacts) ..where( - contacts.requested.equals(true) & contacts.accepted.equals(true).not(), + contacts.requested.equals(true) & + contacts.accepted.equals(false) & + contacts.blocked.equals(false), ) ..addColumns([count]); return query.map((row) => row.read(count)).watchSingle(); @@ -113,7 +128,7 @@ String getContactDisplayName(Contact user) { } else if (user.displayName != null) { name = user.displayName!; } - if (user.deleted) { + if (user.accountDeleted) { name = applyStrikethrough(name); } if (name.length > 12) { diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 6c902e4..09f9fbc 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -81,13 +81,13 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future> getGroupContact(String groupId) async { - final query = select(contacts).join([ + final query = (select(contacts).join([ leftOuterJoin( groupMembers, - groupMembers.contactId.equalsExp(contacts.userId) & - groupMembers.groupId.equals(groupId), + groupMembers.contactId.equalsExp(contacts.userId), ), - ]); + ]) + ..where(groupMembers.groupId.equals(groupId))); return query.map((row) => row.readTable(contacts)).get(); } @@ -101,7 +101,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Stream> watchGroupsForChatList() { - return (select(groups)..where((t) => t.archived.equals(false))).watch(); + return (select(groups) + ..where((t) => t.archived.equals(false)) + ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + .watch(); } Future getGroup(String groupId) { @@ -117,7 +120,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { u.lastMessageReceived.isNotNull() & u.lastMessageSend.isNotNull(), )) - .watchSingle() + .watchSingleOrNull() .asyncMap(getFlameCounterFromGroup); } @@ -126,14 +129,14 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future getDirectChat(int userId) async { - final query = (select(groups).join([ + final query = + ((select(groups)..where((t) => t.isDirectChat.equals(true))).join([ leftOuterJoin( groupMembers, - groupMembers.groupId.equalsExp(groups.groupId) & - groupMembers.contactId.equals(userId), + groupMembers.groupId.equalsExp(groups.groupId), ), ]) - ..where(groups.isDirectChat.equals(true))); + ..where(groupMembers.contactId.equals(userId))); return query.map((row) => row.readTable(groups)).getSingleOrNull(); } @@ -208,9 +211,23 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ), ); } + + Future increaseLastMessageExchange( + String groupId, + DateTime newLastMessage, + ) async { + await (update(groups) + ..where( + (t) => + t.groupId.equals(groupId) & + (t.lastMessageExchange.isSmallerThanValue(newLastMessage)), + )) + .write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); + } } -int getFlameCounterFromGroup(Group group) { +int getFlameCounterFromGroup(Group? group) { + if (group == null) return 0; if (group.lastMessageSend == null || group.lastMessageReceived == null) { return 0; } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 3189e66..17fefb0 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -32,7 +32,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Stream> watchMessageNotOpened(String groupId) { return (select(messages) - ..where((t) => t.openedAt.isNull() & t.groupId.equals(groupId)) + ..where((t) => + t.openedAt.isNull() & + t.groupId.equals(groupId) & + t.isDeletedFromSender.equals(false)) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) .watch(); } @@ -182,7 +185,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Log.error('Message does not exists or contact is not owner.'); return; } - if (msg.mediaId != null) { + if (msg.mediaId != null && contactId != null) { + // contactId -> When a image is send to multiple and one message is delete the image should be still available... await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) .go(); @@ -326,8 +330,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Future updateMessageId( String messageId, MessagesCompanion updatedValues, - ) { - return (update(messages)..where((c) => c.messageId.equals(messageId))) + ) async { + await (update(messages)..where((c) => c.messageId.equals(messageId))) .write(updatedValues); } diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index e0fa70b..752d6a8 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -52,6 +52,24 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { .watch(); } + Stream watchLastReactions(String groupId) { + final query = (select(reactions) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .join( + [ + innerJoin( + messages, + messages.messageId.equalsExp(reactions.messageId), + useColumns: false, + ) + ], + ) + ..where(messages.groupId.equals(groupId)) + // ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + ..limit(1); + return query.map((row) => row.readTable(reactions)).watchSingleOrNull(); + } + Stream> watchReactionWithContacts( String messageId, ) { diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 2fa8096..a427fad 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -6,16 +6,19 @@ class Contacts extends Table { TextColumn get username => text()(); TextColumn get displayName => text().nullable()(); TextColumn get nickName => text().nullable()(); - TextColumn get avatarSvg => text().nullable()(); + BlobColumn get avatarSvgCompressed => blob().nullable()(); IntColumn get senderProfileCounter => integer().withDefault(const Constant(0))(); BoolColumn get accepted => boolean().withDefault(const Constant(false))(); + BoolColumn get deletedByUser => + boolean().withDefault(const Constant(false))(); BoolColumn get requested => boolean().withDefault(const Constant(false))(); BoolColumn get blocked => boolean().withDefault(const Constant(false))(); BoolColumn get verified => boolean().withDefault(const Constant(false))(); - BoolColumn get deleted => boolean().withDefault(const Constant(false))(); + BoolColumn get accountDeleted => + boolean().withDefault(const Constant(false))(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 4ec0de5..95b15a5 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -31,12 +31,12 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { late final GeneratedColumn nickName = GeneratedColumn( 'nick_name', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _avatarSvgMeta = - const VerificationMeta('avatarSvg'); + static const VerificationMeta _avatarSvgCompressedMeta = + const VerificationMeta('avatarSvgCompressed'); @override - late final GeneratedColumn avatarSvg = GeneratedColumn( - 'avatar_svg', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn avatarSvgCompressed = + GeneratedColumn('avatar_svg_compressed', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _senderProfileCounterMeta = const VerificationMeta('senderProfileCounter'); @override @@ -55,6 +55,16 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _deletedByUserMeta = + const VerificationMeta('deletedByUser'); + @override + late final GeneratedColumn deletedByUser = GeneratedColumn( + 'deleted_by_user', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_by_user" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _requestedMeta = const VerificationMeta('requested'); @override @@ -85,15 +95,15 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _deletedMeta = - const VerificationMeta('deleted'); + static const VerificationMeta _accountDeletedMeta = + const VerificationMeta('accountDeleted'); @override - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, false, + late final GeneratedColumn accountDeleted = GeneratedColumn( + 'account_deleted', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("account_deleted" IN (0, 1))'), defaultValue: const Constant(false)); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @@ -109,13 +119,14 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { username, displayName, nickName, - avatarSvg, + avatarSvgCompressed, senderProfileCounter, accepted, + deletedByUser, requested, blocked, verified, - deleted, + accountDeleted, createdAt ]; @override @@ -148,9 +159,11 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_nickNameMeta, nickName.isAcceptableOrUnknown(data['nick_name']!, _nickNameMeta)); } - if (data.containsKey('avatar_svg')) { - context.handle(_avatarSvgMeta, - avatarSvg.isAcceptableOrUnknown(data['avatar_svg']!, _avatarSvgMeta)); + if (data.containsKey('avatar_svg_compressed')) { + context.handle( + _avatarSvgCompressedMeta, + avatarSvgCompressed.isAcceptableOrUnknown( + data['avatar_svg_compressed']!, _avatarSvgCompressedMeta)); } if (data.containsKey('sender_profile_counter')) { context.handle( @@ -162,6 +175,12 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_acceptedMeta, accepted.isAcceptableOrUnknown(data['accepted']!, _acceptedMeta)); } + if (data.containsKey('deleted_by_user')) { + context.handle( + _deletedByUserMeta, + deletedByUser.isAcceptableOrUnknown( + data['deleted_by_user']!, _deletedByUserMeta)); + } if (data.containsKey('requested')) { context.handle(_requestedMeta, requested.isAcceptableOrUnknown(data['requested']!, _requestedMeta)); @@ -174,9 +193,11 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_verifiedMeta, verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta)); } - if (data.containsKey('deleted')) { - context.handle(_deletedMeta, - deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); + if (data.containsKey('account_deleted')) { + context.handle( + _accountDeletedMeta, + accountDeleted.isAcceptableOrUnknown( + data['account_deleted']!, _accountDeletedMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, @@ -199,20 +220,22 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { .read(DriftSqlType.string, data['${effectivePrefix}display_name']), nickName: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), - avatarSvg: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}avatar_svg']), + avatarSvgCompressed: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}avatar_svg_compressed']), senderProfileCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, accepted: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, + deletedByUser: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_by_user'])!, requested: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, blocked: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, verified: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, - deleted: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + accountDeleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}account_deleted'])!, createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); @@ -229,26 +252,28 @@ class Contact extends DataClass implements Insertable { final String username; final String? displayName; final String? nickName; - final String? avatarSvg; + final Uint8List? avatarSvgCompressed; final int senderProfileCounter; final bool accepted; + final bool deletedByUser; final bool requested; final bool blocked; final bool verified; - final bool deleted; + final bool accountDeleted; final DateTime createdAt; const Contact( {required this.userId, required this.username, this.displayName, this.nickName, - this.avatarSvg, + this.avatarSvgCompressed, required this.senderProfileCounter, required this.accepted, + required this.deletedByUser, required this.requested, required this.blocked, required this.verified, - required this.deleted, + required this.accountDeleted, required this.createdAt}); @override Map toColumns(bool nullToAbsent) { @@ -261,15 +286,16 @@ class Contact extends DataClass implements Insertable { if (!nullToAbsent || nickName != null) { map['nick_name'] = Variable(nickName); } - if (!nullToAbsent || avatarSvg != null) { - map['avatar_svg'] = Variable(avatarSvg); + if (!nullToAbsent || avatarSvgCompressed != null) { + map['avatar_svg_compressed'] = Variable(avatarSvgCompressed); } map['sender_profile_counter'] = Variable(senderProfileCounter); map['accepted'] = Variable(accepted); + map['deleted_by_user'] = Variable(deletedByUser); map['requested'] = Variable(requested); map['blocked'] = Variable(blocked); map['verified'] = Variable(verified); - map['deleted'] = Variable(deleted); + map['account_deleted'] = Variable(accountDeleted); map['created_at'] = Variable(createdAt); return map; } @@ -284,15 +310,16 @@ class Contact extends DataClass implements Insertable { nickName: nickName == null && nullToAbsent ? const Value.absent() : Value(nickName), - avatarSvg: avatarSvg == null && nullToAbsent + avatarSvgCompressed: avatarSvgCompressed == null && nullToAbsent ? const Value.absent() - : Value(avatarSvg), + : Value(avatarSvgCompressed), senderProfileCounter: Value(senderProfileCounter), accepted: Value(accepted), + deletedByUser: Value(deletedByUser), requested: Value(requested), blocked: Value(blocked), verified: Value(verified), - deleted: Value(deleted), + accountDeleted: Value(accountDeleted), createdAt: Value(createdAt), ); } @@ -305,14 +332,16 @@ class Contact extends DataClass implements Insertable { username: serializer.fromJson(json['username']), displayName: serializer.fromJson(json['displayName']), nickName: serializer.fromJson(json['nickName']), - avatarSvg: serializer.fromJson(json['avatarSvg']), + avatarSvgCompressed: + serializer.fromJson(json['avatarSvgCompressed']), senderProfileCounter: serializer.fromJson(json['senderProfileCounter']), accepted: serializer.fromJson(json['accepted']), + deletedByUser: serializer.fromJson(json['deletedByUser']), requested: serializer.fromJson(json['requested']), blocked: serializer.fromJson(json['blocked']), verified: serializer.fromJson(json['verified']), - deleted: serializer.fromJson(json['deleted']), + accountDeleted: serializer.fromJson(json['accountDeleted']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -324,13 +353,14 @@ class Contact extends DataClass implements Insertable { 'username': serializer.toJson(username), 'displayName': serializer.toJson(displayName), 'nickName': serializer.toJson(nickName), - 'avatarSvg': serializer.toJson(avatarSvg), + 'avatarSvgCompressed': serializer.toJson(avatarSvgCompressed), 'senderProfileCounter': serializer.toJson(senderProfileCounter), 'accepted': serializer.toJson(accepted), + 'deletedByUser': serializer.toJson(deletedByUser), 'requested': serializer.toJson(requested), 'blocked': serializer.toJson(blocked), 'verified': serializer.toJson(verified), - 'deleted': serializer.toJson(deleted), + 'accountDeleted': serializer.toJson(accountDeleted), 'createdAt': serializer.toJson(createdAt), }; } @@ -340,26 +370,30 @@ class Contact extends DataClass implements Insertable { String? username, Value displayName = const Value.absent(), Value nickName = const Value.absent(), - Value avatarSvg = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), int? senderProfileCounter, bool? accepted, + bool? deletedByUser, bool? requested, bool? blocked, bool? verified, - bool? deleted, + bool? accountDeleted, DateTime? createdAt}) => Contact( userId: userId ?? this.userId, username: username ?? this.username, displayName: displayName.present ? displayName.value : this.displayName, nickName: nickName.present ? nickName.value : this.nickName, - avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg, + avatarSvgCompressed: avatarSvgCompressed.present + ? avatarSvgCompressed.value + : this.avatarSvgCompressed, senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, requested: requested ?? this.requested, blocked: blocked ?? this.blocked, verified: verified ?? this.verified, - deleted: deleted ?? this.deleted, + accountDeleted: accountDeleted ?? this.accountDeleted, createdAt: createdAt ?? this.createdAt, ); Contact copyWithCompanion(ContactsCompanion data) { @@ -369,15 +403,22 @@ class Contact extends DataClass implements Insertable { displayName: data.displayName.present ? data.displayName.value : this.displayName, nickName: data.nickName.present ? data.nickName.value : this.nickName, - avatarSvg: data.avatarSvg.present ? data.avatarSvg.value : this.avatarSvg, + avatarSvgCompressed: data.avatarSvgCompressed.present + ? data.avatarSvgCompressed.value + : this.avatarSvgCompressed, senderProfileCounter: data.senderProfileCounter.present ? data.senderProfileCounter.value : this.senderProfileCounter, accepted: data.accepted.present ? data.accepted.value : this.accepted, + deletedByUser: data.deletedByUser.present + ? data.deletedByUser.value + : this.deletedByUser, requested: data.requested.present ? data.requested.value : this.requested, blocked: data.blocked.present ? data.blocked.value : this.blocked, verified: data.verified.present ? data.verified.value : this.verified, - deleted: data.deleted.present ? data.deleted.value : this.deleted, + accountDeleted: data.accountDeleted.present + ? data.accountDeleted.value + : this.accountDeleted, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -389,13 +430,14 @@ class Contact extends DataClass implements Insertable { ..write('username: $username, ') ..write('displayName: $displayName, ') ..write('nickName: $nickName, ') - ..write('avatarSvg: $avatarSvg, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') ..write('senderProfileCounter: $senderProfileCounter, ') ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') ..write('requested: $requested, ') ..write('blocked: $blocked, ') ..write('verified: $verified, ') - ..write('deleted: $deleted, ') + ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt') ..write(')')) .toString(); @@ -407,13 +449,14 @@ class Contact extends DataClass implements Insertable { username, displayName, nickName, - avatarSvg, + $driftBlobEquality.hash(avatarSvgCompressed), senderProfileCounter, accepted, + deletedByUser, requested, blocked, verified, - deleted, + accountDeleted, createdAt); @override bool operator ==(Object other) => @@ -423,13 +466,15 @@ class Contact extends DataClass implements Insertable { other.username == this.username && other.displayName == this.displayName && other.nickName == this.nickName && - other.avatarSvg == this.avatarSvg && + $driftBlobEquality.equals( + other.avatarSvgCompressed, this.avatarSvgCompressed) && other.senderProfileCounter == this.senderProfileCounter && other.accepted == this.accepted && + other.deletedByUser == this.deletedByUser && other.requested == this.requested && other.blocked == this.blocked && other.verified == this.verified && - other.deleted == this.deleted && + other.accountDeleted == this.accountDeleted && other.createdAt == this.createdAt); } @@ -438,26 +483,28 @@ class ContactsCompanion extends UpdateCompanion { final Value username; final Value displayName; final Value nickName; - final Value avatarSvg; + final Value avatarSvgCompressed; final Value senderProfileCounter; final Value accepted; + final Value deletedByUser; final Value requested; final Value blocked; final Value verified; - final Value deleted; + final Value accountDeleted; final Value createdAt; const ContactsCompanion({ this.userId = const Value.absent(), this.username = const Value.absent(), this.displayName = const Value.absent(), this.nickName = const Value.absent(), - this.avatarSvg = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), this.senderProfileCounter = const Value.absent(), this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), this.requested = const Value.absent(), this.blocked = const Value.absent(), this.verified = const Value.absent(), - this.deleted = const Value.absent(), + this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), }); ContactsCompanion.insert({ @@ -465,13 +512,14 @@ class ContactsCompanion extends UpdateCompanion { required String username, this.displayName = const Value.absent(), this.nickName = const Value.absent(), - this.avatarSvg = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), this.senderProfileCounter = const Value.absent(), this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), this.requested = const Value.absent(), this.blocked = const Value.absent(), this.verified = const Value.absent(), - this.deleted = const Value.absent(), + this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), }) : username = Value(username); static Insertable custom({ @@ -479,13 +527,14 @@ class ContactsCompanion extends UpdateCompanion { Expression? username, Expression? displayName, Expression? nickName, - Expression? avatarSvg, + Expression? avatarSvgCompressed, Expression? senderProfileCounter, Expression? accepted, + Expression? deletedByUser, Expression? requested, Expression? blocked, Expression? verified, - Expression? deleted, + Expression? accountDeleted, Expression? createdAt, }) { return RawValuesInsertable({ @@ -493,14 +542,16 @@ class ContactsCompanion extends UpdateCompanion { if (username != null) 'username': username, if (displayName != null) 'display_name': displayName, if (nickName != null) 'nick_name': nickName, - if (avatarSvg != null) 'avatar_svg': avatarSvg, + if (avatarSvgCompressed != null) + 'avatar_svg_compressed': avatarSvgCompressed, if (senderProfileCounter != null) 'sender_profile_counter': senderProfileCounter, if (accepted != null) 'accepted': accepted, + if (deletedByUser != null) 'deleted_by_user': deletedByUser, if (requested != null) 'requested': requested, if (blocked != null) 'blocked': blocked, if (verified != null) 'verified': verified, - if (deleted != null) 'deleted': deleted, + if (accountDeleted != null) 'account_deleted': accountDeleted, if (createdAt != null) 'created_at': createdAt, }); } @@ -510,26 +561,28 @@ class ContactsCompanion extends UpdateCompanion { Value? username, Value? displayName, Value? nickName, - Value? avatarSvg, + Value? avatarSvgCompressed, Value? senderProfileCounter, Value? accepted, + Value? deletedByUser, Value? requested, Value? blocked, Value? verified, - Value? deleted, + Value? accountDeleted, Value? createdAt}) { return ContactsCompanion( userId: userId ?? this.userId, username: username ?? this.username, displayName: displayName ?? this.displayName, nickName: nickName ?? this.nickName, - avatarSvg: avatarSvg ?? this.avatarSvg, + avatarSvgCompressed: avatarSvgCompressed ?? this.avatarSvgCompressed, senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, requested: requested ?? this.requested, blocked: blocked ?? this.blocked, verified: verified ?? this.verified, - deleted: deleted ?? this.deleted, + accountDeleted: accountDeleted ?? this.accountDeleted, createdAt: createdAt ?? this.createdAt, ); } @@ -549,8 +602,9 @@ class ContactsCompanion extends UpdateCompanion { if (nickName.present) { map['nick_name'] = Variable(nickName.value); } - if (avatarSvg.present) { - map['avatar_svg'] = Variable(avatarSvg.value); + if (avatarSvgCompressed.present) { + map['avatar_svg_compressed'] = + Variable(avatarSvgCompressed.value); } if (senderProfileCounter.present) { map['sender_profile_counter'] = Variable(senderProfileCounter.value); @@ -558,6 +612,9 @@ class ContactsCompanion extends UpdateCompanion { if (accepted.present) { map['accepted'] = Variable(accepted.value); } + if (deletedByUser.present) { + map['deleted_by_user'] = Variable(deletedByUser.value); + } if (requested.present) { map['requested'] = Variable(requested.value); } @@ -567,8 +624,8 @@ class ContactsCompanion extends UpdateCompanion { if (verified.present) { map['verified'] = Variable(verified.value); } - if (deleted.present) { - map['deleted'] = Variable(deleted.value); + if (accountDeleted.present) { + map['account_deleted'] = Variable(accountDeleted.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -583,13 +640,14 @@ class ContactsCompanion extends UpdateCompanion { ..write('username: $username, ') ..write('displayName: $displayName, ') ..write('nickName: $nickName, ') - ..write('avatarSvg: $avatarSvg, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') ..write('senderProfileCounter: $senderProfileCounter, ') ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') ..write('requested: $requested, ') ..write('blocked: $blocked, ') ..write('verified: $verified, ') - ..write('deleted: $deleted, ') + ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt') ..write(')')) .toString(); @@ -6533,13 +6591,14 @@ typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ required String username, Value displayName, Value nickName, - Value avatarSvg, + Value avatarSvgCompressed, Value senderProfileCounter, Value accepted, + Value deletedByUser, Value requested, Value blocked, Value verified, - Value deleted, + Value accountDeleted, Value createdAt, }); typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ @@ -6547,13 +6606,14 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ Value username, Value displayName, Value nickName, - Value avatarSvg, + Value avatarSvgCompressed, Value senderProfileCounter, Value accepted, + Value deletedByUser, Value requested, Value blocked, Value verified, - Value deleted, + Value accountDeleted, Value createdAt, }); @@ -6684,8 +6744,9 @@ class $$ContactsTableFilterComposer ColumnFilters get nickName => $composableBuilder( column: $table.nickName, builder: (column) => ColumnFilters(column)); - ColumnFilters get avatarSvg => $composableBuilder( - column: $table.avatarSvg, builder: (column) => ColumnFilters(column)); + ColumnFilters get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, + builder: (column) => ColumnFilters(column)); ColumnFilters get senderProfileCounter => $composableBuilder( column: $table.senderProfileCounter, @@ -6694,6 +6755,9 @@ class $$ContactsTableFilterComposer ColumnFilters get accepted => $composableBuilder( column: $table.accepted, builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedByUser => $composableBuilder( + column: $table.deletedByUser, builder: (column) => ColumnFilters(column)); + ColumnFilters get requested => $composableBuilder( column: $table.requested, builder: (column) => ColumnFilters(column)); @@ -6703,8 +6767,9 @@ class $$ContactsTableFilterComposer ColumnFilters get verified => $composableBuilder( column: $table.verified, builder: (column) => ColumnFilters(column)); - ColumnFilters get deleted => $composableBuilder( - column: $table.deleted, builder: (column) => ColumnFilters(column)); + ColumnFilters get accountDeleted => $composableBuilder( + column: $table.accountDeleted, + builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); @@ -6861,8 +6926,9 @@ class $$ContactsTableOrderingComposer ColumnOrderings get nickName => $composableBuilder( column: $table.nickName, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get avatarSvg => $composableBuilder( - column: $table.avatarSvg, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get senderProfileCounter => $composableBuilder( column: $table.senderProfileCounter, @@ -6871,6 +6937,10 @@ class $$ContactsTableOrderingComposer ColumnOrderings get accepted => $composableBuilder( column: $table.accepted, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedByUser => $composableBuilder( + column: $table.deletedByUser, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get requested => $composableBuilder( column: $table.requested, builder: (column) => ColumnOrderings(column)); @@ -6880,8 +6950,9 @@ class $$ContactsTableOrderingComposer ColumnOrderings get verified => $composableBuilder( column: $table.verified, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get deleted => $composableBuilder( - column: $table.deleted, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get accountDeleted => $composableBuilder( + column: $table.accountDeleted, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); @@ -6908,8 +6979,8 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get nickName => $composableBuilder(column: $table.nickName, builder: (column) => column); - GeneratedColumn get avatarSvg => - $composableBuilder(column: $table.avatarSvg, builder: (column) => column); + GeneratedColumn get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, builder: (column) => column); GeneratedColumn get senderProfileCounter => $composableBuilder( column: $table.senderProfileCounter, builder: (column) => column); @@ -6917,6 +6988,9 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get accepted => $composableBuilder(column: $table.accepted, builder: (column) => column); + GeneratedColumn get deletedByUser => $composableBuilder( + column: $table.deletedByUser, builder: (column) => column); + GeneratedColumn get requested => $composableBuilder(column: $table.requested, builder: (column) => column); @@ -6926,8 +7000,8 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get verified => $composableBuilder(column: $table.verified, builder: (column) => column); - GeneratedColumn get deleted => - $composableBuilder(column: $table.deleted, builder: (column) => column); + GeneratedColumn get accountDeleted => $composableBuilder( + column: $table.accountDeleted, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -7097,13 +7171,14 @@ class $$ContactsTableTableManager extends RootTableManager< Value username = const Value.absent(), Value displayName = const Value.absent(), Value nickName = const Value.absent(), - Value avatarSvg = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), Value senderProfileCounter = const Value.absent(), Value accepted = const Value.absent(), + Value deletedByUser = const Value.absent(), Value requested = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), - Value deleted = const Value.absent(), + Value accountDeleted = const Value.absent(), Value createdAt = const Value.absent(), }) => ContactsCompanion( @@ -7111,13 +7186,14 @@ class $$ContactsTableTableManager extends RootTableManager< username: username, displayName: displayName, nickName: nickName, - avatarSvg: avatarSvg, + avatarSvgCompressed: avatarSvgCompressed, senderProfileCounter: senderProfileCounter, accepted: accepted, + deletedByUser: deletedByUser, requested: requested, blocked: blocked, verified: verified, - deleted: deleted, + accountDeleted: accountDeleted, createdAt: createdAt, ), createCompanionCallback: ({ @@ -7125,13 +7201,14 @@ class $$ContactsTableTableManager extends RootTableManager< required String username, Value displayName = const Value.absent(), Value nickName = const Value.absent(), - Value avatarSvg = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), Value senderProfileCounter = const Value.absent(), Value accepted = const Value.absent(), + Value deletedByUser = const Value.absent(), Value requested = const Value.absent(), Value blocked = const Value.absent(), Value verified = const Value.absent(), - Value deleted = const Value.absent(), + Value accountDeleted = const Value.absent(), Value createdAt = const Value.absent(), }) => ContactsCompanion.insert( @@ -7139,13 +7216,14 @@ class $$ContactsTableTableManager extends RootTableManager< username: username, displayName: displayName, nickName: nickName, - avatarSvg: avatarSvg, + avatarSvgCompressed: avatarSvgCompressed, senderProfileCounter: senderProfileCounter, accepted: accepted, + deletedByUser: deletedByUser, requested: requested, blocked: blocked, verified: verified, - deleted: deleted, + accountDeleted: accountDeleted, createdAt: createdAt, ), withReferenceMapper: (p0) => p0 diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index b337e7f..375158c 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -346,6 +346,7 @@ "tabToRemoveEmoji": "Tippen um zu entfernen", "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", "messageWasDeleted": "Nachricht wurde gelöscht.", + "messageWasDeletedShort": "Gelöscht", "sent": "Versendet", "sentTo": "Zugestellt an", "received": "Empfangen", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 3b91d4d..d169e57 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -502,6 +502,7 @@ "tabToRemoveEmoji": "Tab to remove", "quotedMessageWasDeleted": "The quoted message has been deleted.", "messageWasDeleted": "Message has been deleted.", + "messageWasDeletedShort": "Deleted", "sent": "Delivered", "sentTo": "Delivered to", "received": "Received", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index fac03be..d2f423d 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2114,6 +2114,12 @@ abstract class AppLocalizations { /// **'Message has been deleted.'** String get messageWasDeleted; + /// No description provided for @messageWasDeletedShort. + /// + /// In en, this message translates to: + /// **'Deleted'** + String get messageWasDeletedShort; + /// No description provided for @sent. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 653f33f..b4ed1fe 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1123,6 +1123,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get messageWasDeleted => 'Nachricht wurde gelöscht.'; + @override + String get messageWasDeletedShort => 'Gelöscht'; + @override String get sent => 'Versendet'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index e797023..3337bed 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1116,6 +1116,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get messageWasDeleted => 'Message has been deleted.'; + @override + String get messageWasDeletedShort => 'Deleted'; + @override String get sent => 'Delivered'; diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index e777cc5..6a41b3f 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -777,15 +777,15 @@ class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { factory EncryptedContent_ContactUpdate({ EncryptedContent_ContactUpdate_Type? type, - $core.String? avatarSvg, + $core.List<$core.int>? avatarSvgCompressed, $core.String? displayName, }) { final $result = create(); if (type != null) { $result.type = type; } - if (avatarSvg != null) { - $result.avatarSvg = avatarSvg; + if (avatarSvgCompressed != null) { + $result.avatarSvgCompressed = avatarSvgCompressed; } if (displayName != null) { $result.displayName = displayName; @@ -798,7 +798,7 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactUpdate_Type.REQUEST, valueOf: EncryptedContent_ContactUpdate_Type.valueOf, enumValues: EncryptedContent_ContactUpdate_Type.values) - ..aOS(2, _omitFieldNames ? '' : 'avatarSvg', protoName: 'avatarSvg') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY, protoName: 'avatarSvgCompressed') ..aOS(3, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') ..hasRequiredFields = false ; @@ -834,13 +834,13 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { void clearType() => clearField(1); @$pb.TagNumber(2) - $core.String get avatarSvg => $_getSZ(1); + $core.List<$core.int> get avatarSvgCompressed => $_getN(1); @$pb.TagNumber(2) - set avatarSvg($core.String v) { $_setString(1, v); } + set avatarSvgCompressed($core.List<$core.int> v) { $_setBytes(1, v); } @$pb.TagNumber(2) - $core.bool hasAvatarSvg() => $_has(1); + $core.bool hasAvatarSvgCompressed() => $_has(1); @$pb.TagNumber(2) - void clearAvatarSvg() => clearField(2); + void clearAvatarSvgCompressed() => clearField(2); @$pb.TagNumber(3) $core.String get displayName => $_getSZ(2); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 483604a..43fb535 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -260,12 +260,12 @@ const EncryptedContent_ContactUpdate$json = { '1': 'ContactUpdate', '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactUpdate.Type', '10': 'type'}, - {'1': 'avatarSvg', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'avatarSvg', '17': true}, + {'1': 'avatarSvgCompressed', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'avatarSvgCompressed', '17': true}, {'1': 'displayName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'displayName', '17': true}, ], '4': [EncryptedContent_ContactUpdate_Type$json], '8': [ - {'1': '_avatarSvg'}, + {'1': '_avatarSvgCompressed'}, {'1': '_displayName'}, ], }; @@ -358,19 +358,20 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA' 'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK' 'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG' - 'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa0gEKDUNvbnRh' + 'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa8AEKDUNvbnRh' 'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS' - '5UeXBlUgR0eXBlEiEKCWF2YXRhclN2ZxgCIAEoCUgAUglhdmF0YXJTdmeIAQESJQoLZGlzcGxh' - 'eU5hbWUYAyABKAlIAVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVV' - 'BEQVRFEAFCDAoKX2F2YXRhclN2Z0IOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5' - 'cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SW' - 'QYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQY' - 'BCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQg' - 'gKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNv' - 'dW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgAS' - 'gDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmll' - 'bmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZX' - 'JCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFj' - 'dFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCw' - 'oJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZQ=='); + '5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29t' + 'cHJlc3NlZIgBARIlCgtkaXNwbGF5TmFtZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeX' + 'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEIOCgxf' + 'ZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW' + '50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5' + 'GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBF' + 'R5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVh' + 'dGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlch' + 'I2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdl' + 'Eh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZW' + 'N0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21l' + 'ZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3' + 'RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2Fn' + 'ZQ=='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 890ef28..64f947e 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -118,7 +118,7 @@ message EncryptedContent { } Type type = 1; - optional string avatarSvg = 2; + optional bytes avatarSvgCompressed = 2; optional string displayName = 3; } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 4d6ff9e..71781b7 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -339,7 +339,7 @@ class ApiService { await twonlyDB.contactsDao.updateContact( contactId, ContactsCompanion( - deleted: const Value(true), + accountDeleted: const Value(true), username: Value('${contact.username} (${contact.userId})'), ), ); diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 22326f0..5e97149 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -52,6 +52,8 @@ Future insertMediaFileInMessagesTable( type: const Value(MessageType.media), ), ); + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, DateTime.now()); if (message != null) { // de-archive contact when sending a new message await twonlyDB.groupsDao.updateGroup( diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index aa8a7ac..e748f54 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; @@ -116,7 +118,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ await twonlyDB.receiptsDao.deleteReceipt(receiptId); await twonlyDB.contactsDao.updateContact( receipt.contactId, - const ContactsCompanion(deleted: Value(true)), + const ContactsCompanion(accountDeleted: Value(true)), ); return null; } @@ -195,6 +197,8 @@ Future sendCipherTextToGroup( ) async { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); + await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); + encryptedContent.groupId = groupId; for (final groupMember in groupMembers) { @@ -280,7 +284,7 @@ Future notifyContactsAboutProfileChange({int? onlyToContact}) async { final encryptedContent = pb.EncryptedContent( contactUpdate: pb.EncryptedContent_ContactUpdate( type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, - avatarSvg: user.avatarSvg, + avatarSvgCompressed: gzip.encode(utf8.encode(user.avatarSvg!)), displayName: user.displayName, ), ); diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 9484785..e8e6ad6 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -3,6 +3,7 @@ import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; @@ -20,6 +21,7 @@ import 'package:twonly/src/services/api/server_messages/reaction.server_message. import 'package:twonly/src/services/api/server_messages/text_message.server_messages.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; final lockHandleServerMessage = Mutex(); @@ -28,19 +30,19 @@ Future handleServerMessage(server.ServerToClient msg) async { 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()) { - final body = Uint8List.fromList(msg.v0.newMessage.body); - final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - await handleNewMessage(fromUserId, body); - } else { - Log.error('Unknown server message: $msg'); - } - } catch (e) { - Log.error(e); + // try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + final body = Uint8List.fromList(msg.v0.newMessage.body); + final fromUserId = msg.v0.newMessage.fromUserId.toInt(); + await handleNewMessage(fromUserId, body); + } else { + Log.error('Unknown server message: $msg'); } + // } catch (e) { + // Log.error(e); + // } final v0 = client.V0() ..seq = msg.v0.seq @@ -93,6 +95,21 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { final encryptedContentRaw = Uint8List.fromList(message.encryptedContent); + if (await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull() == + null) { + /// In case the user does not exists, just create a dummy user which was deleted by the user, so the message + /// can be inserted into the receipts database + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + userId: Value(fromUserId), + deletedByUser: const Value(true), + username: const Value('[deleted]'), + ), + ); + } + final responsePlaintextContent = await handleEncryptedMessage( fromUserId, encryptedContentRaw, @@ -108,6 +125,7 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { } else { response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT; } + await twonlyDB.receiptsDao.insertReceipt( ReceiptsCompanion( receiptId: Value(receiptId), @@ -189,8 +207,29 @@ Future handleEncryptedMessage( /// Verify that the user is (still) in that group... if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { - Log.error('User $fromUserId tried to access group ${content.groupId}.'); - return null; + if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact == null || contact.deletedByUser) { + Log.error( + 'User tries to send message to direct chat while the user does not exists !', + ); + return null; + } + Log.info( + 'Creating new DirectChat between two users', + ); + await twonlyDB.groupsDao.createNewDirectChat( + fromUserId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); + } else { + Log.error('User $fromUserId tried to access group ${content.groupId}.'); + return null; + } } if (content.hasTextMessage()) { diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index fc96a85..c49ac82 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; @@ -25,11 +26,12 @@ Future handleContactRequest( if (username.isSuccess) { // ignore: avoid_dynamic_calls final name = username.value.userdata.username as Uint8List; - await twonlyDB.contactsDao.insertContact( + await twonlyDB.contactsDao.insertOnConflictUpdate( ContactsCompanion( username: Value(utf8.decode(name)), userId: Value(fromUserId), requested: const Value(true), + deletedByUser: const Value(false), ), ); } @@ -43,9 +45,25 @@ Future handleContactRequest( accepted: Value(true), ), ); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + await twonlyDB.groupsDao.createNewDirectChat( + fromUserId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact!)), + ), + ); case EncryptedContent_ContactRequest_Type.REJECT: Log.info('Got a contact reject from $fromUserId'); - await twonlyDB.contactsDao.deleteContactByUserId(fromUserId); + await twonlyDB.contactsDao.updateContact( + fromUserId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); } } @@ -61,13 +79,14 @@ Future handleContactUpdate( case EncryptedContent_ContactUpdate_Type.UPDATE: Log.info('Got a contact update $fromUserId'); - if (contactUpdate.hasAvatarSvg() && + if (contactUpdate.hasAvatarSvgCompressed() && contactUpdate.hasDisplayName() && senderProfileCounter != null) { await twonlyDB.contactsDao.updateContact( fromUserId, ContactsCompanion( - avatarSvg: Value(contactUpdate.avatarSvg), + avatarSvgCompressed: + Value(Uint8List.fromList(contactUpdate.avatarSvgCompressed)), displayName: Value(contactUpdate.displayName), senderProfileCounter: Value(senderProfileCounter), ), diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 29afe75..10108b6 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -101,6 +101,8 @@ Future handleMedia( ), ); if (message != null) { + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); Log.info('Inserted a new media message with ID: ${message.messageId}'); await twonlyDB.groupsDao.incFlameCounter( message.groupId, diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index dfe6260..f3444cd 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -29,6 +29,10 @@ Future handleTextMessage( ackByServer: Value(DateTime.now()), ), ); + await twonlyDB.groupsDao.increaseLastMessageExchange( + groupId, + fromTimestamp(textMessage.timestamp), + ); if (message != null) { Log.info('Inserted a new text message with ID: ${message.messageId}'); } diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index c7af9bc..81da8c9 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' hide Message; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/signal/session.signal.dart'; class Result { Result.error(this.error) : value = null; @@ -57,7 +56,7 @@ ClientToServer createClientToServerFromApplicationData( return ClientToServer()..v0 = v0; } -Future rejectAndDeleteContact(int contactId) async { +Future rejectAndHideContact(int contactId) async { await sendCipherText( contactId, EncryptedContent( @@ -66,9 +65,14 @@ Future rejectAndDeleteContact(int contactId) async { ), ), ); - await twonlyDB.signalDao.deleteAllByContactId(contactId); - await deleteSessionWithTarget(contactId); - await twonlyDB.contactsDao.deleteContactByUserId(contactId); + await twonlyDB.contactsDao.updateContact( + contactId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); } Future handleMediaError(MediaFile media) async { diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index ffaa999..5af964d 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -9,7 +9,7 @@ Future createThumbnailsForImage( ) async { final fileExtension = sourceFile.path.split('.').last.toLowerCase(); if (fileExtension != 'png') { - Log.error('Could not create thumbnail for image. $fileExtension != .png'); + Log.error('Could not create thumbnail for image. $fileExtension != png'); return; } diff --git a/lib/src/services/notifications/setup.notifications.dart b/lib/src/services/notifications/setup.notifications.dart index 03ff634..6d9b3b0 100644 --- a/lib/src/services/notifications/setup.notifications.dart +++ b/lib/src/services/notifications/setup.notifications.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_svg/svg.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/misc.dart'; final StreamController selectNotificationStream = StreamController.broadcast(); @@ -61,10 +62,11 @@ Future createPushAvatars() async { final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); for (final contact in contacts) { - if (contact.avatarSvg == null) return; + if (contact.avatarSvgCompressed == null) return; - final pictureInfo = - await vg.loadPicture(SvgStringLoader(contact.avatarSvg!), null); + final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!); + + final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null); final image = await pictureInfo.picture.toImage(300, 300); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b2adc93..d86d788 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -372,3 +374,8 @@ String friendlyDateTime( return '$timePart $datePart'; } + +String getAvatarSvg(Uint8List avatarSvgCompressed) { + final raw = gzip.decode(avatarSvgCompressed); + return utf8.decode(raw); +} diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index dedf439..5bdf57d 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/headline.dart'; @@ -49,8 +48,7 @@ class _SearchUsernameView extends State { } Future _addNewUser(BuildContext context) async { - final user = await getUser(); - if (user == null || user.username == searchUserName.text || !mounted) { + if (gUser.username == searchUserName.text) { return; } @@ -84,11 +82,13 @@ class _SearchUsernameView extends State { return; } - final added = await twonlyDB.contactsDao.insertContact( + final added = await twonlyDB.contactsDao.insertOnConflictUpdate( ContactsCompanion( username: Value(searchUserName.text), userId: Value(userdata.userId.toInt()), requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), ), ); @@ -223,18 +223,26 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () async { - await rejectAndDeleteContact(contact.userId); + await rejectAndHideContact(contact.userId); }, ), ), IconButton( icon: const Icon(Icons.check, color: Colors.green), onPressed: () async { - const update = ContactsCompanion( - accepted: Value(true), - requested: Value(false), + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + accepted: Value(true), + requested: Value(false), + ), + ); + await twonlyDB.groupsDao.createNewDirectChat( + contact.userId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), ); - await twonlyDB.contactsDao.updateContact(contact.userId, update); await sendCipherText( contact.userId, EncryptedContent( diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index f13fac1..e2491ae 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -47,7 +47,7 @@ class _ChatListViewState extends State { } Future initAsync() async { - final stream = twonlyDB.groupsDao.watchGroups(); + final stream = twonlyDB.groupsDao.watchGroupsForChatList(); _contactsSub = stream.listen((groups) { setState(() { _groupsNotPinned = groups.where((x) => !x.pinned).toList(); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index dd4bab5..03fba1b 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -38,7 +38,9 @@ class _UserListItem extends State { late StreamSubscription> messagesNotOpenedStream; Message? lastMessage; + Reaction? lastReaction; late StreamSubscription lastMessageStream; + late StreamSubscription lastReactionStream; late StreamSubscription> lastMediaFilesStream; List previewMessages = []; @@ -54,6 +56,7 @@ class _UserListItem extends State { @override void dispose() { messagesNotOpenedStream.cancel(); + lastReactionStream.cancel(); lastMessageStream.cancel(); lastMediaFilesStream.cancel(); super.dispose(); @@ -68,6 +71,17 @@ class _UserListItem extends State { }); }); + lastReactionStream = twonlyDB.reactionsDao + .watchLastReactions(widget.group.groupId) + .listen((update) { + setState(() { + lastReaction = update; + }); + // protectUpdateState.protect(() async { + // await updateState(lastMessage, update, messagesNotOpened); + // }); + }); + messagesNotOpenedStream = twonlyDB.messagesDao .watchMessageNotOpened(widget.group.groupId) .listen((update) { @@ -141,9 +155,7 @@ class _UserListItem extends State { lastMessage = newLastMessage; messagesNotOpened = newMessagesNotOpened; - setState(() { - // sets lastMessages, messagesNotOpened and currentMessage - }); + if (mounted) setState(() {}); } Future onTap() async { @@ -217,7 +229,11 @@ class _UserListItem extends State { ? Text(context.lang.chatsTapToSend) : Row( children: [ - MessageSendStateIcon(previewMessages, previewMediaFiles), + MessageSendStateIcon( + previewMessages, + previewMediaFiles, + lastReaction: lastReaction, + ), const Text('•'), const SizedBox(width: 5), if (currentMessage != null) diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 858a3cc..e4b9a19 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -179,7 +179,7 @@ class _ChatListEntryState extends State { Message? nextMessage, bool hasReactions, ) { - var bottom = 30.0; + var bottom = 20.0; var top = 0.0; var topLeft = 12.0; @@ -189,7 +189,7 @@ class _ChatListEntryState extends State { if (nextMessage != null) { if (message.senderId == nextMessage.senderId) { - bottom = 10; + bottom = 3; } } @@ -203,7 +203,7 @@ class _ChatListEntryState extends State { final combinesWidthNext = combineTextMessageWithNext(message, nextMessage); if (combinesWidthNext) { - bottom = 1; + bottom = 0; bottomLeft = 5.0; } diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 2cfe578..f8abbed 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 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/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -44,10 +45,12 @@ class MessageSendStateIcon extends StatefulWidget { this.mediaFiles, { super.key, this.canBeReopened = false, + this.lastReaction, this.mainAxisAlignment = MainAxisAlignment.end, }); final List messages; final List mediaFiles; + final Reaction? lastReaction; final MainAxisAlignment mainAxisAlignment; final bool canBeReopened; @@ -125,8 +128,8 @@ class _MessageSendStateIconState extends State { case MessageSendState.received: icon = Icon(Icons.square_rounded, size: 14, color: color); text = context.lang.messageSendState_Received; - if (message.type == MessageType.media) { - if (mediaFile!.downloadState == DownloadState.pending) { + if (message.type == MessageType.media && mediaFile != null) { + if (mediaFile.downloadState == DownloadState.pending) { text = context.lang.messageSendState_TapToLoad; } if (mediaFile.downloadState == DownloadState.downloading) { @@ -170,6 +173,11 @@ class _MessageSendStateIconState extends State { } } + if (message.isDeletedFromSender) { + icon = FaIcon(FontAwesomeIcons.trash, size: 10, color: color); + text = context.lang.messageWasDeletedShort; + } + if (hasLoader) { icons = [icon]; break; @@ -182,6 +190,41 @@ class _MessageSendStateIconState extends State { } } + if (widget.lastReaction != null && + !widget.messages.any((t) => t.openedAt == null)) { + /// No messages are still open, so check if the reaction is the last message received. + + if (!widget.messages + .any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) { + if (EmojiAnimation.animatedIcons + .containsKey(widget.lastReaction!.emoji)) { + icons = [ + SizedBox( + height: 18, + child: EmojiAnimation(emoji: widget.lastReaction!.emoji), + ) + ]; + } else { + icons = [ + SizedBox( + height: 18, + child: Center( + child: Text( + widget.lastReaction!.emoji, + style: const TextStyle(fontSize: 18), + strutStyle: const StrutStyle( + forceStrutHeight: true, + height: 1.6, + ), + ), + ), + ) + ]; + } + // Log.info("DISPLAY REACTION"); + } + } + if (icons.isEmpty) return Container(); var icon = icons[0]; diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index 8e9706e..c05a9f6 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -4,6 +4,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; class AvatarIcon extends StatefulWidget { const AvatarIcon({ @@ -38,21 +39,21 @@ class _AvatarIconState extends State { final contacts = await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); if (contacts.length == 1) { - if (contacts.first.avatarSvg != null) { - avatarSVGs.add(contacts.first.avatarSvg!); + if (contacts.first.avatarSvgCompressed != null) { + avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); } } else { for (final contact in contacts) { - if (contact.avatarSvg != null) { - avatarSVGs.add(contact.avatarSvg!); + if (contact.avatarSvgCompressed != null) { + avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); } } } // avatarSvg = group!.avatarSvg; } else if (widget.userData?.avatarSvg != null) { avatarSVGs.add(widget.userData!.avatarSvg!); - } else if (widget.contact?.avatarSvg != null) { - avatarSVGs.add(widget.contact!.avatarSvg!); + } else if (widget.contact?.avatarSvgCompressed != null) { + avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); } if (mounted) setState(() {}); } diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 7b51f36..5ff2d8e 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -30,7 +30,7 @@ class _ContactViewState extends State { ); if (remove) { // trigger deletion for the other user... - await rejectAndDeleteContact(contact.userId); + await rejectAndHideContact(contact.userId); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index a0e53a3..15821cb 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; @@ -39,134 +40,139 @@ class _DatabaseMigrationViewState extends State { final oldMessages = await oldDatabase.messages.select().get(); for (final oldContact in oldContacts) { + if (oldContact.deleted) continue; + Uint8List? avatarSvg; + if (oldContact.avatarSvg != null) { + avatarSvg = + Uint8List.fromList(gzip.encode(utf8.encode(oldContact.avatarSvg!))); + } await twonlyDB.contactsDao.insertContact( ContactsCompanion( userId: Value(oldContact.userId), username: Value(oldContact.username), displayName: Value(oldContact.displayName), nickName: Value(oldContact.nickName), - avatarSvg: Value(oldContact.avatarSvg), + avatarSvgCompressed: Value(avatarSvg), senderProfileCounter: const Value(0), accepted: Value(oldContact.accepted), requested: Value(oldContact.requested), blocked: Value(oldContact.blocked), verified: Value(oldContact.verified), - deleted: Value(oldContact.deleted), createdAt: Value(oldContact.createdAt), ), ); setState(() { _contactsMigrated += 1; }); - if (!oldContact.deleted) { - final group = await twonlyDB.groupsDao.createNewDirectChat( - oldContact.userId, - GroupsCompanion( - pinned: Value(oldContact.pinned), - archived: Value(oldContact.archived), - groupName: Value(getContactDisplayNameOld(oldContact)), - totalMediaCounter: Value(oldContact.totalMediaCounter), - alsoBestFriend: Value(oldContact.alsoBestFriend), - createdAt: Value(oldContact.createdAt), - lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), - lastFlameSync: Value(oldContact.lastFlameSync), - lastMessageExchange: Value(oldContact.lastMessageExchange), - lastMessageReceived: Value(oldContact.lastMessageReceived), - lastMessageSend: Value(oldContact.lastMessageSend), - flameCounter: Value(oldContact.flameCounter), + final group = await twonlyDB.groupsDao.createNewDirectChat( + oldContact.userId, + GroupsCompanion( + pinned: Value(oldContact.pinned), + archived: Value(oldContact.archived), + groupName: Value(getContactDisplayNameOld(oldContact)), + totalMediaCounter: Value(oldContact.totalMediaCounter), + alsoBestFriend: Value(oldContact.alsoBestFriend), + createdAt: Value(oldContact.createdAt), + lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), + lastFlameSync: Value(oldContact.lastFlameSync), + lastMessageExchange: Value(oldContact.lastMessageExchange), + lastMessageReceived: Value(oldContact.lastMessageReceived), + lastMessageSend: Value(oldContact.lastMessageSend), + flameCounter: Value(oldContact.flameCounter), + ), + ); + if (group == null) continue; + for (final oldMessage in oldMessages) { + if (oldMessage.mediaUploadId == null && + oldMessage.mediaDownloadId == null) { + /// only interested in media files... + continue; + } + if (oldMessage.contactId != oldContact.userId) continue; + if (!oldMessage.mediaStored) continue; + + var storedMediaPath = + join((await getApplicationSupportDirectory()).path, 'media'); + if (oldMessage.mediaDownloadId != null) { + storedMediaPath = + '${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}'; + } else { + storedMediaPath = + '${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}'; + } + + var type = MediaType.image; + if (File('$storedMediaPath.mp4').existsSync()) { + type = MediaType.video; + storedMediaPath = '$storedMediaPath.mp4'; + } else if (File('$storedMediaPath.png').existsSync()) { + type = MediaType.image; + storedMediaPath = '$storedMediaPath.png'; + } else if (File('$storedMediaPath.webp').existsSync()) { + type = MediaType.image; + storedMediaPath = '$storedMediaPath.webp'; + } else { + continue; + } + + final uniqueId = Value( + getUUIDforDirectChat( + oldMessage.messageOtherId ?? oldMessage.messageId, + oldMessage.contactId ^ gUser.userId, ), ); - if (group == null) continue; - for (final oldMessage in oldMessages) { - if (oldMessage.mediaUploadId == null && - oldMessage.mediaDownloadId == null) { - /// only interested in media files... - continue; - } - if (oldMessage.contactId != oldContact.userId) continue; - if (!oldMessage.mediaStored) continue; - var storedMediaPath = - join((await getApplicationSupportDirectory()).path, 'media'); - if (oldMessage.mediaDownloadId != null) { - storedMediaPath = - '${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}'; - } else { - storedMediaPath = - '${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}'; - } + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + mediaId: uniqueId, + stored: const Value(true), + type: Value(type), + createdAt: Value(oldMessage.sendAt), + ), + ); + if (mediaFile == null) continue; - var type = MediaType.image; - if (File('$storedMediaPath.mp4').existsSync()) { - type = MediaType.video; - storedMediaPath = '$storedMediaPath.mp4'; - } else if (File('$storedMediaPath.png').existsSync()) { - type = MediaType.image; - storedMediaPath = '$storedMediaPath.png'; - } else if (File('$storedMediaPath.webp').existsSync()) { - type = MediaType.image; - storedMediaPath = '$storedMediaPath.webp'; - } else { - continue; - } + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: uniqueId, + groupId: Value(group.groupId), + mediaId: uniqueId, + type: const Value(MessageType.media), + ), + ); + if (message == null) continue; - final uniqueId = Value( - getUUIDforDirectChat( - oldMessage.messageOtherId ?? oldMessage.messageId, - oldMessage.contactId ^ gUser.userId, - ), - ); - - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - mediaId: uniqueId, - stored: const Value(true), - type: Value(type), - createdAt: Value(oldMessage.sendAt), - ), - ); - if (mediaFile == null) continue; - - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - messageId: uniqueId, - groupId: Value(group.groupId), - mediaId: uniqueId, - type: const Value(MessageType.media), - ), - ); - if (message == null) continue; - - final mediaService = await MediaFileService.fromMedia(mediaFile); - File(storedMediaPath).copySync(mediaService.storedPath.path); - setState(() { - _storedMediaFiles += 1; - }); - } + final mediaService = await MediaFileService.fromMedia(mediaFile); + File(storedMediaPath).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); } } final memoriesPath = Directory( join((await getApplicationSupportDirectory()).path, 'media', 'memories'), ); - final files = memoriesPath.listSync(); - for (final file in files) { - if (file.path.contains('thumbnail')) continue; - final type = - file.path.contains('mp4') ? MediaType.video : MediaType.image; - final stat = FileStat.statSync(file.path); - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - type: Value(type), - createdAt: Value(stat.modified), - stored: const Value(true), - ), - ); - final mediaService = await MediaFileService.fromMedia(mediaFile!); - File(file.path).copySync(mediaService.storedPath.path); - setState(() { - _storedMediaFiles += 1; - }); + if (memoriesPath.existsSync()) { + final files = memoriesPath.listSync(); + for (final file in files) { + if (file.path.contains('thumbnail')) continue; + final type = + file.path.contains('mp4') ? MediaType.video : MediaType.image; + final stat = FileStat.statSync(file.path); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + type: Value(type), + createdAt: Value(stat.modified), + stored: const Value(true), + ), + ); + final mediaService = await MediaFileService.fromMedia(mediaFile!); + File(file.path).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); + } } final oldContactPreKeys = From df9b055ba76eb38b50169aa9592d756e06ccd905 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 20:44:14 +0100 Subject: [PATCH 26/76] fix verification shield and remove backup notice --- lib/src/database/daos/groups.dao.dart | 11 + lib/src/database/daos/messages.dao.dart | 10 +- lib/src/database/daos/reactions.dao.dart | 2 +- lib/src/services/api.service.dart | 1 - .../api/mediafiles/download.service.dart | 4 +- lib/src/services/api/messages.dart | 9 +- lib/src/services/signal/identity.signal.dart | 6 +- lib/src/services/signal/session.signal.dart | 7 +- .../twonly_safe/common.twonly_safe.dart | 7 +- .../create_backup.twonly_safe.dart | 19 +- .../camera_preview_controller_view.dart | 17 +- .../image_editor/modules/all_emojis.dart | 5 +- lib/src/views/chats/chat_list.view.dart | 8 +- .../backup_notice.card.dart | 96 ---- lib/src/views/chats/chat_messages.view.dart | 7 +- .../message_send_state_icon.dart | 5 +- .../reaction_buttons.component.dart | 7 +- lib/src/views/components/flame.dart | 1 + lib/src/views/components/verified_shield.dart | 84 +++- lib/src/views/contact/contact.view.dart | 2 +- .../views/settings/backup/backup.view.dart | 10 +- .../backup/twonly_safe_server.view.dart | 6 +- .../views/settings/settings_main.view.dart | 432 +++++++++--------- .../subscription/additional_users.view.dart | 5 +- 24 files changed, 340 insertions(+), 421 deletions(-) delete mode 100644 lib/src/views/chats/chat_list_components/backup_notice.card.dart diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 09f9fbc..27eec3f 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -91,6 +91,17 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return query.map((row) => row.readTable(contacts)).get(); } + Stream> watchGroupContact(String groupId) { + final query = (select(contacts).join([ + leftOuterJoin( + groupMembers, + groupMembers.contactId.equalsExp(contacts.userId), + ), + ]) + ..where(groupMembers.groupId.equals(groupId))); + return query.map((row) => row.readTable(contacts)).watch(); + } + Stream> watchGroups() { return select(groups).watch(); } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 17fefb0..02aac49 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -32,10 +32,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Stream> watchMessageNotOpened(String groupId) { return (select(messages) - ..where((t) => - t.openedAt.isNull() & - t.groupId.equals(groupId) & - t.isDeletedFromSender.equals(false)) + ..where( + (t) => + t.openedAt.isNull() & + t.groupId.equals(groupId) & + t.isDeletedFromSender.equals(false), + ) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) .watch(); } diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 752d6a8..997d3f8 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -61,7 +61,7 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { messages, messages.messageId.equalsExp(reactions.messageId), useColumns: false, - ) + ), ], ) ..where(messages.groupId.equals(groupId)) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 71781b7..3683f2d 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -94,7 +94,6 @@ class ApiService { unawaited(retransmitRawBytes()); unawaited(tryTransmitMessages()); unawaited(tryDownloadAllMediaFiles()); - unawaited(notifyContactsAboutProfileChange()); twonlyDB.markUpdated(); unawaited(syncFlameCounters()); unawaited(setupNotificationWithUsers()); diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 9f4e910..7c57699 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -16,7 +16,6 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; Future tryDownloadAllMediaFiles({bool force = false}) async { // This is called when WebSocket is newly connected, so allow all downloads to be restarted. @@ -44,8 +43,7 @@ Map> defaultAutoDownloadOptions = { Future isAllowedToDownload({required bool isVideo}) async { final connectivityResult = await Connectivity().checkConnectivity(); - final user = await getUser(); - final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions; + final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; if (connectivityResult.contains(ConnectivityResult.mobile)) { if (isVideo) { diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index e748f54..b6591f4 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -16,7 +16,6 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; final lockRetransmission = Mutex(); @@ -277,15 +276,13 @@ Future notifyContactAboutOpeningMessage( } Future notifyContactsAboutProfileChange({int? onlyToContact}) async { - final user = await getUser(); - if (user == null) return; - if (user.avatarSvg == null) return; + if (gUser.avatarSvg == null) return; final encryptedContent = pb.EncryptedContent( contactUpdate: pb.EncryptedContent_ContactUpdate( type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, - avatarSvgCompressed: gzip.encode(utf8.encode(user.avatarSvg!)), - displayName: user.displayName, + avatarSvgCompressed: gzip.encode(utf8.encode(gUser.avatarSvg!)), + displayName: gUser.displayName, ), ); diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index c4554d7..f2e3129 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -20,13 +20,11 @@ Future getSignalIdentityKeyPair() async { // This function runs after the clients authenticated with the server. // It then checks if it should update a new session key Future signalHandleNewServerConnection() async { - final user = await getUser(); - if (user == null) return; - if (user.signalLastSignedPreKeyUpdated != null) { + if (gUser.signalLastSignedPreKeyUpdated != null) { final fortyEightHoursAgo = DateTime.now().subtract(const Duration(hours: 48)); final isYoungerThan48Hours = - (user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); + (gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); if (isYoungerThan48Hours) { // The key does live for 48 hours then it expires and a new key is generated. return; diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 19ee1a0..48480c7 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -1,10 +1,10 @@ import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; 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'; -import 'package:twonly/src/utils/storage.dart'; Future createNewSignalSession(Response_UserData userData) async { final SignalProtocolStore? signalStore = await getSignalStore(); @@ -84,8 +84,7 @@ Future deleteSessionWithTarget(int target) async { Future generateSessionFingerPrint(int target) async { final signalStore = await getSignalStore(); - final user = await getUser(); - if (signalStore == null || user == null) return null; + if (signalStore == null) return null; try { final targetIdentity = await signalStore .getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId)); @@ -93,7 +92,7 @@ Future generateSessionFingerPrint(int target) async { final generator = NumericFingerprintGenerator(5200); final localFingerprint = generator.createFor( 1, - Uint8List.fromList([user.userId]), + Uint8List.fromList([gUser.userId]), (await signalStore.getIdentityKeyPair()).getPublicKey(), Uint8List.fromList([target]), targetIdentity, diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 630ef5d..5e022b4 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; @@ -10,10 +11,8 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future enableTwonlySafe(String password) async { - final user = await getUser(); - if (user == null) return; - - final (backupId, encryptionKey) = await getMasterKey(password, user.username); + final (backupId, encryptionKey) = + await getMasterKey(password, gUser.username); await updateUserdata((user) { user.twonlySafeBackup = TwonlySafeBackup( diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 42bc911..80dd6e3 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -10,6 +10,7 @@ import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; @@ -21,19 +22,17 @@ import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart'; Future performTwonlySafeBackup({bool force = false}) async { - final user = await getUser(); - - if (user == null || user.twonlySafeBackup == null) { + if (gUser.twonlySafeBackup == null) { return; } - if (user.twonlySafeBackup!.backupUploadState == + if (gUser.twonlySafeBackup!.backupUploadState == LastBackupUploadState.pending) { Log.warn('Backup upload is already pending.'); return; } - final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone; + final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone; if (!force && lastUpdateTime != null) { if (lastUpdateTime .isAfter(DateTime.now().subtract(const Duration(days: 1)))) { @@ -117,8 +116,8 @@ Future performTwonlySafeBackup({bool force = false}) async { final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); - if (user.twonlySafeBackup!.lastBackupDone == null || - user.twonlySafeBackup!.lastBackupDone! + if (gUser.twonlySafeBackup!.lastBackupDone == null || + gUser.twonlySafeBackup!.lastBackupDone! .isAfter(DateTime.now().subtract(const Duration(days: 90)))) { force = true; } @@ -144,7 +143,7 @@ Future performTwonlySafeBackup({bool force = false}) async { final secretBox = await chacha20.encrypt( backupBytes, - secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey), + secretKey: SecretKey(gUser.twonlySafeBackup!.encryptionKey), nonce: nonce, ); @@ -165,8 +164,8 @@ Future performTwonlySafeBackup({bool force = false}) async { 'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.', ); - if (user.backupServer != null) { - if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) { + if (gUser.backupServer != null) { + if (encryptedBackupBytes.length > gUser.backupServer!.maxBackupBytes) { Log.error('Backup is to big for the alternative backup server.'); await updateUserdata((user) { user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index b4bb4e4..edea3bc 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -181,17 +181,12 @@ class _CameraPreviewViewState extends State { Future initAsync() async { hasAudioPermission = await Permission.microphone.isGranted; - if (!hasAudioPermission) { - final user = await getUser(); - if (user != null) { - if (!user.requestedAudioPermission) { - await updateUserdata((u) { - u.requestedAudioPermission = true; - return u; - }); - await requestMicrophonePermission(); - } - } + if (!hasAudioPermission && !gUser.requestedAudioPermission) { + await updateUserdata((u) { + u.requestedAudioPermission = true; + return u; + }); + await requestMicrophonePermission(); } if (!mounted) return; setState(() {}); diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index 18775ea..423342a 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/image_editor/data/data.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; @@ -22,10 +23,8 @@ class _EmojisState extends State { } Future initAsync() async { - final user = await getUser(); - if (user == null) return; setState(() { - lastUsed = user.lastUsedEditorEmojis ?? []; + lastUsed = gUser.lastUsedEditorEmojis ?? []; lastUsed.addAll(emojis); }); } diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index e2491ae..ae9e4e1 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -11,7 +11,6 @@ import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; -import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; @@ -237,13 +236,8 @@ class _ChatListViewState extends State { : ListView.builder( itemCount: _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0) + - _groupsNotPinned.length + - 1, + _groupsNotPinned.length, itemBuilder: (context, index) { - if (index == 0) { - return const BackupNoticeCard(); - } - index -= 1; // Check if the index is for the pinned users if (index < _groupsPinned.length) { final group = _groupsPinned[index]; diff --git a/lib/src/views/chats/chat_list_components/backup_notice.card.dart b/lib/src/views/chats/chat_list_components/backup_notice.card.dart deleted file mode 100644 index d5b9a36..0000000 --- a/lib/src/views/chats/chat_list_components/backup_notice.card.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/settings/backup/backup.view.dart'; - -class BackupNoticeCard extends StatefulWidget { - const BackupNoticeCard({super.key}); - - @override - State createState() => _BackupNoticeCardState(); -} - -class _BackupNoticeCardState extends State { - bool showBackupNotice = false; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - final user = await getUser(); - showBackupNotice = false; - if (user != null && - (user.nextTimeToShowBackupNotice == null || - DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) { - if (user.twonlySafeBackup == null) { - showBackupNotice = true; - } - } - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - if (!showBackupNotice) return Container(); - - return Card( - elevation: 4, - margin: const EdgeInsets.all(10), - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.lang.backupNoticeTitle, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 5), - Text( - context.lang.backupNoticeDesc, - style: const TextStyle(fontSize: 14), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () async { - await updateUserdata((user) { - user.nextTimeToShowBackupNotice = - DateTime.now().add(const Duration(days: 7)); - return user; - }); - await initAsync(); - }, - child: Text(context.lang.backupNoticeLater), - ), - const SizedBox(width: 10), - FilledButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BackupView(), - ), - ); - }, - child: Text(context.lang.backupNoticeOpenBackup), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 6d6ef42..c29b4c6 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -17,6 +17,8 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.d import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; @@ -242,8 +244,9 @@ class _ChatMessagesViewState extends State { children: [ Text(group.groupName), const SizedBox(width: 10), - // if (group.verified) - // VerifiedShield(key: verifyShieldKey, group), + VerifiedShield(key: verifyShieldKey, group: group), + const SizedBox(width: 10), + FlameCounterWidget(groupId: group.groupId), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index f8abbed..9e50231 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -5,7 +5,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 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/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -202,7 +201,7 @@ class _MessageSendStateIconState extends State { SizedBox( height: 18, child: EmojiAnimation(emoji: widget.lastReaction!.emoji), - ) + ), ]; } else { icons = [ @@ -218,7 +217,7 @@ class _MessageSendStateIconState extends State { ), ), ), - ) + ), ]; } // Log.info("DISPLAY REACTION"); diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart index 75146c4..d5678fa 100644 --- a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -39,9 +39,8 @@ class _ReactionButtonsState extends State { } Future initAsync() async { - final user = await getUser(); - if (user != null && user.preSelectedEmojies != null) { - selectedEmojis = user.preSelectedEmojies!; + if (gUser.preSelectedEmojies != null) { + selectedEmojis = gUser.preSelectedEmojies!; } setState(() {}); } diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 7753e55..7512489 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -55,6 +55,7 @@ class _FlameCounterWidgetState extends State { @override Widget build(BuildContext context) { + if (flameCounter < 1) return Container(); return Row( children: [ if (widget.prefix) const SizedBox(width: 5), diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 4c3523d..6c8b2d8 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -1,38 +1,82 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; -class VerifiedShield extends StatelessWidget { - const VerifiedShield(this.contact, {super.key, this.size = 18}); - final Contact contact; +class VerifiedShield extends StatefulWidget { + const VerifiedShield({ + this.contact, + this.group, + super.key, + this.size = 18, + }); + final Group? group; + final Contact? contact; final double size; + @override + State createState() => _VerifiedShieldState(); +} + +class _VerifiedShieldState extends State { + bool isVerified = false; + Contact? contact; + + StreamSubscription>? stream; + + @override + void initState() { + if (widget.group != null) { + stream = twonlyDB.groupsDao + .watchGroupContact(widget.group!.groupId) + .listen((contacts) { + if (contacts.length == 1) { + contact = contacts.first; + } + setState(() { + isVerified = contacts.any((t) => t.verified); + }); + }); + } else if (widget.contact != null) { + isVerified = widget.contact!.verified; + contact = widget.contact; + } + + super.initState(); + } + + @override + void dispose() { + stream?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactVerifyView(contact); + onTap: (contact == null) + ? null + : () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactVerifyView(contact!); + }, + ), + ); }, - ), - ); - }, child: Tooltip( - message: contact.verified + message: isVerified ? 'You verified this contact' : 'You have not verifies this contact.', child: FaIcon( - contact.verified - ? FontAwesomeIcons.shieldHeart - : Icons.gpp_maybe_rounded, - color: contact.verified - ? Theme.of(context).colorScheme.primary - : Colors.red, - size: size, + isVerified ? FontAwesomeIcons.shieldHeart : Icons.gpp_maybe_rounded, + color: + isVerified ? Theme.of(context).colorScheme.primary : Colors.red, + size: widget.size, ), ), ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 5ff2d8e..c38d53e 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -116,7 +116,7 @@ class _ContactViewState extends State { children: [ Padding( padding: const EdgeInsets.only(right: 10), - child: VerifiedShield(contact), + child: VerifiedShield(key: GlobalKey(), contact: contact), ), Text( getContactDisplayName(contact), diff --git a/lib/src/views/settings/backup/backup.view.dart b/lib/src/views/settings/backup/backup.view.dart index 87daaab..92a42eb 100644 --- a/lib/src/views/settings/backup/backup.view.dart +++ b/lib/src/views/settings/backup/backup.view.dart @@ -1,12 +1,11 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; @@ -48,11 +47,10 @@ class _BackupViewState extends State { } Future initAsync() async { - final user = await getUser(); - twonlySafeBackup = user?.twonlySafeBackup; + twonlySafeBackup = gUser.twonlySafeBackup; backupServer = defaultBackupServer; - if (user?.backupServer != null) { - backupServer = user!.backupServer!; + if (gUser.backupServer != null) { + backupServer = gUser.backupServer!; } setState(() {}); } diff --git a/lib/src/views/settings/backup/twonly_safe_server.view.dart b/lib/src/views/settings/backup/twonly_safe_server.view.dart index 973f87a..a7c2cf8 100644 --- a/lib/src/views/settings/backup/twonly_safe_server.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_server.view.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -30,9 +31,8 @@ class _TwonlySafeServerViewState extends State { } Future initAsync() async { - final user = await getUser(); - if (user?.backupServer != null) { - final uri = Uri.parse(user!.backupServer!.serverUrl); + if (gUser.backupServer != null) { + final uri = Uri.parse(gUser.backupServer!.serverUrl); // remove user auth data final serverUrl = Uri( scheme: uri.scheme, diff --git a/lib/src/views/settings/settings_main.view.dart b/lib/src/views/settings/settings_main.view.dart index c27b5a4..b58efcd 100644 --- a/lib/src/views/settings/settings_main.view.dart +++ b/lib/src/views/settings/settings_main.view.dart @@ -1,10 +1,7 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/settings/account.view.dart'; @@ -28,247 +25,232 @@ class SettingsMainView extends StatefulWidget { } class _SettingsMainViewState extends State { - UserData? userData; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - userData = await getUser(); - setState(() {}); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(context.lang.settingsTitle), ), - body: (userData == null) - ? null - : ListView( + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(30), + child: Row( children: [ - Padding( - padding: const EdgeInsets.all(30), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ProfileView(); - }, - ), - ); - await initAsync(); - }, - child: ColoredBox( - color: context.color.surface.withAlpha(0), - child: Row( - children: [ - AvatarIcon( - userData: userData, - fontSize: 30, - ), - Container(width: 20, color: Colors.transparent), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - userData!.displayName, - style: const TextStyle(fontSize: 20), - textAlign: TextAlign.left, - ), - Text( - userData!.username, - style: const TextStyle( - fontSize: 14, - ), - textAlign: TextAlign.left, - ), - ], - ), - ], - ), - ), - ), - ), - // Align( - // alignment: Alignment.centerRight, - // child: IconButton( - // onPressed: () {}, - // icon: FaIcon(FontAwesomeIcons.qrcode), - // ), - // ) - ], - ), - ), - BetterListTile( - icon: FontAwesomeIcons.user, - text: context.lang.settingsAccount, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const AccountView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.shieldHeart, - text: context.lang.settingsSubscription, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const SubscriptionView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: Icons.lock_clock_rounded, - text: context.lang.settingsBackup, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const BackupView(); - }, - ), - ); - }, - ), - const Divider(), - BetterListTile( - icon: FontAwesomeIcons.sun, - text: context.lang.settingsAppearance, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const AppearanceView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.comment, - text: context.lang.settingsChats, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ChatSettingsView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.lock, - text: context.lang.settingsPrivacy, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PrivacyView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.bell, - text: context.lang.settingsNotification, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const NotificationView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.chartPie, - iconSize: 15, - text: context.lang.settingsStorageData, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const DataAndStorageView(); - }, - ), - ); - }, - ), - const Divider(), - BetterListTile( - icon: FontAwesomeIcons.circleQuestion, - text: context.lang.settingsHelp, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const HelpView(); - }, - ), - ); - }, - ), - if (userData != null && userData!.isDeveloper) - BetterListTile( - icon: FontAwesomeIcons.code, - text: 'Developer Settings', + Expanded( + child: GestureDetector( onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { - return const DeveloperSettingsView(); + return const ProfileView(); }, ), ); + setState(() {}); }, - ), - BetterListTile( - icon: FontAwesomeIcons.shareFromSquare, - text: context.lang.inviteFriends, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ShareWithFriendsView(); - }, + child: ColoredBox( + color: context.color.surface.withAlpha(0), + child: Row( + children: [ + AvatarIcon( + userData: gUser, + fontSize: 30, + ), + Container(width: 20, color: Colors.transparent), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + gUser.displayName, + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.left, + ), + Text( + gUser.username, + style: const TextStyle( + fontSize: 14, + ), + textAlign: TextAlign.left, + ), + ], + ), + ], ), - ); - }, + ), + ), ), + // Align( + // alignment: Alignment.centerRight, + // child: IconButton( + // onPressed: () {}, + // icon: FaIcon(FontAwesomeIcons.qrcode), + // ), + // ) ], ), + ), + BetterListTile( + icon: FontAwesomeIcons.user, + text: context.lang.settingsAccount, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AccountView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.shieldHeart, + text: context.lang.settingsSubscription, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: Icons.lock_clock_rounded, + text: context.lang.settingsBackup, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const BackupView(); + }, + ), + ); + }, + ), + const Divider(), + BetterListTile( + icon: FontAwesomeIcons.sun, + text: context.lang.settingsAppearance, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AppearanceView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.comment, + text: context.lang.settingsChats, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ChatSettingsView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.lock, + text: context.lang.settingsPrivacy, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PrivacyView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.bell, + text: context.lang.settingsNotification, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const NotificationView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.chartPie, + iconSize: 15, + text: context.lang.settingsStorageData, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DataAndStorageView(); + }, + ), + ); + }, + ), + const Divider(), + BetterListTile( + icon: FontAwesomeIcons.circleQuestion, + text: context.lang.settingsHelp, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const HelpView(); + }, + ), + ); + }, + ), + if (gUser.isDeveloper) + BetterListTile( + icon: FontAwesomeIcons.code, + text: 'Developer Settings', + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DeveloperSettingsView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.shareFromSquare, + text: context.lang.inviteFriends, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ShareWithFriendsView(); + }, + ), + ); + }, + ), + ], + ), ); } } diff --git a/lib/src/views/settings/subscription/additional_users.view.dart b/lib/src/views/settings/subscription/additional_users.view.dart index 5a76e88..1bb21dd 100644 --- a/lib/src/views/settings/subscription/additional_users.view.dart +++ b/lib/src/views/settings/subscription/additional_users.view.dart @@ -23,10 +23,9 @@ Future?> loadAdditionalUserInvites() async { }); return ballance; } - final user = await getUser(); - if (user != null && user.lastPlanBallance != null) { + if (gUser.lastPlanBallance != null) { try { - final decoded = jsonDecode(user.additionalUserInvites!) as List; + final decoded = jsonDecode(gUser.additionalUserInvites!) as List; return decoded.map(Response_AddAccountsInvite.fromJson).toList(); } catch (e) { Log.error('could not parse additional user json: $e'); From 9d563793c7a4624dbd88ea16cde0785c3d9f409a Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 23:53:48 +0100 Subject: [PATCH 27/76] crafting a custom context-menu --- CHANGELOG.md | 13 + lib/main.dart | 4 + lib/src/database/daos/contacts.dao.dart | 4 +- lib/src/database/daos/groups.dao.dart | 1 - .../reaction.server_message.dart | 3 +- lib/src/services/api/utils.dart | 19 - .../mediafiles/mediafile.service.dart | 77 +++- .../mediafiles/thumbnail.service.dart | 30 -- lib/src/utils/misc.dart | 26 -- lib/src/views/chats/add_new_user.view.dart | 18 +- lib/src/views/chats/archived_chats.view.dart | 54 +++ lib/src/views/chats/chat_list.view.dart | 375 ++++++++++-------- .../chat_list_components/group_list_item.dart | 194 +++++---- lib/src/views/chats/chat_messages.view.dart | 246 ++++++------ .../chat_list_entry.dart | 5 +- .../message_context_menu.dart | 66 +-- lib/src/views/chats/message_info.view.dart | 24 +- lib/src/views/chats/start_new_chat.view.dart | 48 +-- .../components/avatar_icon.component.dart | 14 +- .../components/context_menu.component.dart | 99 +++++ .../group_context_menu.component.dart | 84 ++-- .../user_context_menu.component.dart | 26 +- lib/src/views/contact/contact.view.dart | 25 +- lib/src/views/home.view.dart | 156 ++++---- .../settings/privacy_view_block.users.dart | 82 ++-- pubspec.lock | 13 +- pubspec.yaml | 3 - 27 files changed, 935 insertions(+), 774 deletions(-) create mode 100644 lib/src/views/chats/archived_chats.view.dart create mode 100644 lib/src/views/components/context_menu.component.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5121631..e04885f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.0.62 + +- Support for Groups +- Editing of text messages +- Deletion of messages +- Various UI improvements like a new context-menu +- Client-to-client (C2C) protocol converted to ProtoBuf +- Use of UUIDs in the database +- Completely new database schema +- Improved reliability of C2C messages +- Improved video handling +- Various bug fixes + ## 0.0.61 - Improving image editor when changing colors diff --git a/lib/main.dart b/lib/main.dart index a951611..1004f5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ // ignore_for_file: unused_import +import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; @@ -17,6 +18,7 @@ import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -54,6 +56,8 @@ void main() async { await initFileDownloader(); + unawaited(MediaFileService.purgeTempFolder()); + // await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.signalDao.purgeOutDatedPreKeys(); diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index d1cff2e..6792ad0 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -131,8 +131,8 @@ String getContactDisplayName(Contact user) { if (user.accountDeleted) { name = applyStrikethrough(name); } - if (name.length > 12) { - return '${name.substring(0, 12)}...'; + if (name.length > 27) { + return '${name.substring(0, 27 - 3)}...'; } return name; } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 27eec3f..b5c741d 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -113,7 +113,6 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { Stream> watchGroupsForChatList() { return (select(groups) - ..where((t) => t.archived.equals(false)) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) .watch(); } diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/server_messages/reaction.server_message.dart index daf4247..1eaf4db 100644 --- a/lib/src/services/api/server_messages/reaction.server_message.dart +++ b/lib/src/services/api/server_messages/reaction.server_message.dart @@ -22,6 +22,7 @@ Future handleReaction( groupId, reaction.emoji, ); - return; + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, DateTime.now()); } } diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 81da8c9..7357d06 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -56,25 +56,6 @@ ClientToServer createClientToServerFromApplicationData( return ClientToServer()..v0 = v0; } -Future rejectAndHideContact(int contactId) async { - await sendCipherText( - contactId, - EncryptedContent( - contactRequest: EncryptedContent_ContactRequest( - type: EncryptedContent_ContactRequest_Type.REJECT, - ), - ), - ); - await twonlyDB.contactsDao.updateContact( - contactId, - const ContactsCompanion( - accepted: Value(false), - requested: Value(false), - deletedByUser: Value(true), - ), - ); -} - Future handleMediaError(MediaFile media) async { await twonlyDB.mediaFilesDao.updateMedia( media.mediaId, diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index a32dc62..e0fcb7e 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -32,6 +32,61 @@ class MediaFileService { ); } + static Future purgeTempFolder() async { + final tempDirectory = MediaFileService._buildDirectoryPath( + 'tmp', + await getApplicationSupportDirectory(), + ); + + final files = tempDirectory.listSync(); + for (final file in files) { + final mediaId = basename(file.path).split('.').first; + + var delete = true; + + final service = await MediaFileService.fromMediaId(mediaId); + if (service == null) { + Log.error( + 'Purging media file, as it is not in the database $mediaId.', + ); + } else { + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + + for (final message in messages) { + if (message.senderId == null) { + // Media was send by me + if (message.openedAt == null) { + // Message was not yet opened from all persons, so wait... + delete = false; + } else if (service.mediaFile.requiresAuthentication || + service.mediaFile.displayLimitInMilliseconds != null) { + // Message was opened by all persons, and they can not reopen the image. + // delete = true; // do not overwrite a previous delete = false + // this is just to make it easier to understand :) + } else if (message.openedAt! + .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + // Message was opened by all persons, as it can be reopened and then stored by a other person keep it for + // two day just to be sure. + delete = false; + } + } else { + // this media was received from another person + if (message.openedAt == null) { + // Message was not yet opened, so do not remove it. + delete = false; + } + } + } + } + + if (delete) { + Log.info('Purging media file $mediaId'); + file.deleteSync(); + } + } + } + Future updateFromDB() async { final updated = await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); @@ -89,7 +144,8 @@ class MediaFileService { } switch (mediaFile.type) { case MediaType.image: - await createThumbnailsForImage(storedPath, thumbnailPath); + // all images are already compress.. + break; case MediaType.video: await createThumbnailsForVideo(storedPath, thumbnailPath); case MediaType.gif: @@ -163,11 +219,10 @@ class MediaFileService { await updateFromDB(); } - File _buildFilePath( - String directory, { - String namePrefix = '', - String extensionParam = '', - }) { + static Directory _buildDirectoryPath( + String directory, + Directory applicationSupportDirectory, + ) { final mediaBaseDir = Directory( join( applicationSupportDirectory.path, @@ -178,6 +233,14 @@ class MediaFileService { if (!mediaBaseDir.existsSync()) { mediaBaseDir.createSync(recursive: true); } + return mediaBaseDir; + } + + File _buildFilePath( + String directory, { + String namePrefix = '', + String extensionParam = '', + }) { var extension = extensionParam; if (extension == '') { switch (mediaFile.type) { @@ -189,6 +252,8 @@ class MediaFileService { extension = 'gif'; } } + final mediaBaseDir = + _buildDirectoryPath(directory, applicationSupportDirectory); return File( join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), ); diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index 5af964d..e73c8e4 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -1,37 +1,7 @@ import 'dart:io'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; -Future createThumbnailsForImage( - File sourceFile, - File destinationFile, -) async { - final fileExtension = sourceFile.path.split('.').last.toLowerCase(); - if (fileExtension != 'png') { - Log.error('Could not create thumbnail for image. $fileExtension != png'); - return; - } - - try { - final imageBytesCompressed = await FlutterImageCompress.compressWithFile( - minHeight: 800, - minWidth: 450, - sourceFile.path, - format: CompressFormat.webp, - quality: 50, - ); - - if (imageBytesCompressed == null) { - Log.error('Could not compress the image'); - return; - } - await destinationFile.writeAsBytes(imageBytesCompressed); - } catch (e) { - Log.error('Could not compress the image got :$e'); - } -} - Future createThumbnailsForVideo( File sourceFile, File destinationFile, diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index d86d788..b488fdb 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -8,7 +8,6 @@ import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @@ -270,31 +269,6 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList( ), ); -PieTheme getPieCanvasTheme(BuildContext context) { - return PieTheme( - brightness: Theme.of(context).brightness, - rightClickShowsMenu: true, - radius: 70, - buttonTheme: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.tertiary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - buttonThemeHovered: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - tooltipPadding: const EdgeInsets.all(20), - overlayColor: isDarkMode(context) - ? const Color.fromARGB(69, 0, 0, 0) - : const Color.fromARGB(40, 0, 0, 0), - // spacing: 0, - tooltipTextStyle: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), - ); -} - Color getMessageColorFromType( Message message, MediaFile? mediaFile, diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index 5bdf57d..7ab04b5 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -8,7 +8,6 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -223,7 +222,22 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () async { - await rejectAndHideContact(contact.userId); + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REJECT, + ), + ), + ); + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); }, ), ), diff --git a/lib/src/views/chats/archived_chats.view.dart b/lib/src/views/chats/archived_chats.view.dart new file mode 100644 index 0000000..e769afa --- /dev/null +++ b/lib/src/views/chats/archived_chats.view.dart @@ -0,0 +1,54 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; + +class ArchivedChatsView extends StatefulWidget { + const ArchivedChatsView({super.key}); + + @override + State createState() => _ArchivedChatsViewState(); +} + +class _ArchivedChatsViewState extends State { + List _groupsArchived = []; + late StreamSubscription> _contactsSub; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + final stream = twonlyDB.groupsDao.watchGroupsForChatList(); + _contactsSub = stream.listen((groups) { + setState(() { + _groupsArchived = groups.where((x) => x.archived).toList(); + }); + }); + } + + @override + void dispose() { + _contactsSub.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Archivierte Chats"), + ), + body: ListView( + children: _groupsArchived.map((group) { + return GroupListItem( + group: group, + ); + }).toList(), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index ae9e4e1..2bbf609 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; +import 'package:twonly/src/views/chats/archived_chats.view.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; @@ -33,8 +34,8 @@ class _ChatListViewState extends State { late StreamSubscription> _contactsSub; List _groupsNotPinned = []; List _groupsPinned = []; + List _groupsArchived = []; - GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey(); Timer? tutorial; bool showFeedbackShortcut = false; @@ -49,8 +50,10 @@ class _ChatListViewState extends State { final stream = twonlyDB.groupsDao.watchGroupsForChatList(); _contactsSub = stream.listen((groups) { setState(() { - _groupsNotPinned = groups.where((x) => !x.pinned).toList(); - _groupsPinned = groups.where((x) => x.pinned).toList(); + _groupsNotPinned = + groups.where((x) => !x.pinned && !x.archived).toList(); + _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); + _groupsArchived = groups.where((x) => x.archived).toList(); }); }); @@ -59,9 +62,9 @@ class _ChatListViewState extends State { if (!mounted) return; await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); if (!mounted) return; - if (_groupsNotPinned.isNotEmpty) { - await showChatListTutorialContextMenu(context, firstUserListItemKey); - } + // if (_groupsNotPinned.isNotEmpty) { + // await showChatListTutorialContextMenu(context, firstUserListItemKey); + // } }); final changeLog = await rootBundle.loadString('CHANGELOG.md'); @@ -102,193 +105,219 @@ class _ChatListViewState extends State { Widget build(BuildContext context) { final isConnected = context.watch().isConnected; final planId = context.watch().plan; - return Scaffold( - appBar: AppBar( - title: Row( - children: [ - GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ProfileView(); - }, - ), - ); - if (!mounted) return; - setState(() {}); // gUser has updated - }, - child: AvatarIcon( - userData: gUser, - fontSize: 14, - color: context.color.onSurface.withAlpha(20), - ), - ), - const SizedBox(width: 10), - const Text('twonly '), - if (planId != 'Free') + return Container( + child: Scaffold( + appBar: AppBar( + title: Row( + children: [ GestureDetector( - onTap: () { - Navigator.push( + onTap: () async { + await Navigator.push( context, MaterialPageRoute( builder: (context) { - return const SubscriptionView(); + return const ProfileView(); }, ), ); + if (!mounted) return; + setState(() {}); // gUser has updated }, - child: Container( - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(15), - ), - padding: - const EdgeInsets.symmetric(horizontal: 5, vertical: 3), - child: Text( - planId, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: isDarkMode(context) ? Colors.black : Colors.white, - ), - ), + child: AvatarIcon( + userData: gUser, + fontSize: 14, + color: context.color.onSurface.withAlpha(20), ), ), - ], - ), - actions: [ - const FeedbackIconButton(), - StreamBuilder( - stream: twonlyDB.contactsDao.watchContactsRequested(), - builder: (context, snapshot) { - var count = 0; - if (snapshot.hasData && snapshot.data != null) { - count = snapshot.data!; - } - return NotificationBadge( - count: count.toString(), - child: IconButton( - key: searchForOtherUsers, - icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), - onPressed: () { + const SizedBox(width: 10), + const Text('twonly '), + if (planId != 'Free') + GestureDetector( + onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => const AddNewUserView(), + builder: (context) { + return const SubscriptionView(); + }, ), ); }, + child: Container( + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(15), + ), + padding: + const EdgeInsets.symmetric(horizontal: 5, vertical: 3), + child: Text( + planId, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: + isDarkMode(context) ? Colors.black : Colors.white, + ), + ), + ), + ), + ], + ), + actions: [ + const FeedbackIconButton(), + StreamBuilder( + stream: twonlyDB.contactsDao.watchContactsRequested(), + builder: (context, snapshot) { + var count = 0; + if (snapshot.hasData && snapshot.data != null) { + count = snapshot.data!; + } + return NotificationBadge( + count: count.toString(), + child: IconButton( + key: searchForOtherUsers, + icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + ), + ); + }, + ), + IconButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsMainView(), + ), + ); + if (!mounted) return; + setState(() {}); // gUser may has changed... + }, + icon: const FaIcon(FontAwesomeIcons.gear, size: 19), + ), + ], + ), + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: isConnected ? Container() : const ConnectionInfo(), + ), + Positioned.fill( + child: Container( + child: RefreshIndicator( + onRefresh: () async { + await apiService.close(() {}); + await apiService.connect(force: true); + await Future.delayed(const Duration(seconds: 1)); + }, + child: (_groupsNotPinned.isEmpty && + _groupsPinned.isEmpty && + _groupsArchived.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const AddNewUserView(), + ), + ); + }, + label: Text( + context.lang.chatListViewSearchUserNameBtn), + ), + ), + ) + : ListView.builder( + itemCount: _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + + _groupsNotPinned.length + + (_groupsArchived.isNotEmpty ? 1 : 0), + itemBuilder: (context, index) { + if (index >= + _groupsNotPinned.length + + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (_groupsArchived.isEmpty) return Container(); + return ListTile( + title: Text( + "Archivierte Chats (${_groupsArchived.length})", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ArchivedChatsView(); + }, + ), + ); + }, + ); + } + // Check if the index is for the pinned users + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItem( + key: ValueKey(group.groupId), + group: group, + ); + } + + // If there are pinned users, account for the Divider + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && + adjustedIndex == 0) { + return const Divider(); + } + + // Adjust the index for the contacts list + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); + + // Get the contacts that are not pinned + final group = _groupsNotPinned.elementAt( + adjustedIndex, + ); + return GroupListItem( + key: ValueKey(group.groupId), group: group); + }, + ), + ), + ), + ), + ], + ), + floatingActionButton: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const StartNewChatView(); + }, ), ); }, + child: const FaIcon(FontAwesomeIcons.penToSquare), ), - IconButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsMainView(), - ), - ); - if (!mounted) return; - setState(() {}); // gUser may has changed... - }, - icon: const FaIcon(FontAwesomeIcons.gear, size: 19), - ), - ], - ), - body: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: isConnected ? Container() : const ConnectionInfo(), - ), - Positioned.fill( - child: RefreshIndicator( - onRefresh: () async { - await apiService.close(() {}); - await apiService.connect(force: true); - await Future.delayed(const Duration(seconds: 1)); - }, - child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - label: - Text(context.lang.chatListViewSearchUserNameBtn), - ), - ), - ) - : ListView.builder( - itemCount: _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0) + - _groupsNotPinned.length, - itemBuilder: (context, index) { - // Check if the index is for the pinned users - if (index < _groupsPinned.length) { - final group = _groupsPinned[index]; - return GroupListItem( - key: ValueKey(group.groupId), - group: group, - firstUserListItemKey: (index == 0 || index == 1) - ? firstUserListItemKey - : null, - ); - } - - // If there are pinned users, account for the Divider - var adjustedIndex = index - _groupsPinned.length; - if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { - return const Divider(); - } - - // Adjust the index for the contacts list - adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); - - // Get the contacts that are not pinned - final group = _groupsNotPinned.elementAt( - adjustedIndex, - ); - return GroupListItem( - key: ValueKey(group.groupId), - group: group, - firstUserListItemKey: - (index == 0) ? firstUserListItemKey : null, - ); - }, - ), - ), - ), - ], - ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const StartNewChatView(); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.penToSquare), ), ), ); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 03fba1b..417cf8d 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; @@ -20,32 +21,29 @@ import 'package:twonly/src/views/components/group_context_menu.component.dart'; class GroupListItem extends StatefulWidget { const GroupListItem({ required this.group, - required this.firstUserListItemKey, super.key, }); final Group group; - final GlobalKey? firstUserListItemKey; @override State createState() => _UserListItem(); } class _UserListItem extends State { - MessageSendState state = MessageSendState.send; - Message? currentMessage; + Message? _currentMessage; - List messagesNotOpened = []; - late StreamSubscription> messagesNotOpenedStream; + List _messagesNotOpened = []; + late StreamSubscription> _messagesNotOpenedStream; - Message? lastMessage; - Reaction? lastReaction; - late StreamSubscription lastMessageStream; - late StreamSubscription lastReactionStream; - late StreamSubscription> lastMediaFilesStream; + Message? _lastMessage; + Reaction? _lastReaction; + late StreamSubscription _lastMessageStream; + late StreamSubscription _lastReactionStream; + late StreamSubscription> _lastMediaFilesStream; - List previewMessages = []; - List previewMediaFiles = []; - bool hasNonOpenedMediaFile = false; + List _previewMessages = []; + final List _previewMediaFiles = []; + bool _hasNonOpenedMediaFile = false; @override void initState() { @@ -55,48 +53,48 @@ class _UserListItem extends State { @override void dispose() { - messagesNotOpenedStream.cancel(); - lastReactionStream.cancel(); - lastMessageStream.cancel(); - lastMediaFilesStream.cancel(); + _messagesNotOpenedStream.cancel(); + _lastReactionStream.cancel(); + _lastMessageStream.cancel(); + _lastMediaFilesStream.cancel(); super.dispose(); } void initStreams() { - lastMessageStream = twonlyDB.messagesDao + _lastMessageStream = twonlyDB.messagesDao .watchLastMessage(widget.group.groupId) .listen((update) { protectUpdateState.protect(() async { - await updateState(update, messagesNotOpened); + await updateState(update, _messagesNotOpened); }); }); - lastReactionStream = twonlyDB.reactionsDao + _lastReactionStream = twonlyDB.reactionsDao .watchLastReactions(widget.group.groupId) .listen((update) { setState(() { - lastReaction = update; + _lastReaction = update; }); // protectUpdateState.protect(() async { // await updateState(lastMessage, update, messagesNotOpened); // }); }); - messagesNotOpenedStream = twonlyDB.messagesDao + _messagesNotOpenedStream = twonlyDB.messagesDao .watchMessageNotOpened(widget.group.groupId) .listen((update) { protectUpdateState.protect(() async { - await updateState(lastMessage, update); + await updateState(_lastMessage, update); }); }); - lastMediaFilesStream = + _lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { for (final mediaFile in mediaFiles) { - final index = - previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId); + final index = _previewMediaFiles + .indexWhere((t) => t.mediaId == mediaFile.mediaId); if (index >= 0) { - previewMediaFiles[index] = mediaFile; + _previewMediaFiles[index] = mediaFile; } } setState(() {}); @@ -111,55 +109,55 @@ class _UserListItem extends State { ) async { if (newLastMessage == null) { // there are no messages at all - currentMessage = null; - previewMessages = []; + _currentMessage = null; + _previewMessages = []; } else if (newMessagesNotOpened.isNotEmpty) { // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); if (receivedMessages.isNotEmpty) { - previewMessages = receivedMessages; - currentMessage = receivedMessages.first; + _previewMessages = receivedMessages; + _currentMessage = receivedMessages.first; } else { - previewMessages = newMessagesNotOpened; - currentMessage = newMessagesNotOpened.first; + _previewMessages = newMessagesNotOpened; + _currentMessage = newMessagesNotOpened.first; } } else { // there are no not opened messages show just the last message in the table - currentMessage = newLastMessage; - previewMessages = [newLastMessage]; + _currentMessage = newLastMessage; + _previewMessages = [newLastMessage]; } final msgs = - previewMessages.where((x) => x.type == MessageType.media).toList(); + _previewMessages.where((x) => x.type == MessageType.media).toList(); if (msgs.isNotEmpty && msgs.first.type == MessageType.media && msgs.first.senderId != null && msgs.first.openedAt == null) { - hasNonOpenedMediaFile = true; + _hasNonOpenedMediaFile = true; } else { - hasNonOpenedMediaFile = false; + _hasNonOpenedMediaFile = false; } - for (final message in previewMessages) { + for (final message in _previewMessages) { if (message.mediaId != null && - !previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); if (mediaFile != null) { - previewMediaFiles.add(mediaFile); + _previewMediaFiles.add(mediaFile); } } } - lastMessage = newLastMessage; - messagesNotOpened = newMessagesNotOpened; + _lastMessage = newLastMessage; + _messagesNotOpened = newMessagesNotOpened; if (mounted) setState(() {}); } Future onTap() async { - if (currentMessage == null) { + if (_currentMessage == null) { await Navigator.push( context, MaterialPageRoute( @@ -171,9 +169,9 @@ class _UserListItem extends State { return; } - if (hasNonOpenedMediaFile) { + if (_hasNonOpenedMediaFile) { final msgs = - previewMessages.where((x) => x.type == MessageType.media).toList(); + _previewMessages.where((x) => x.type == MessageType.media).toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); if (mediaFile?.downloadState == null) return; @@ -207,70 +205,56 @@ class _UserListItem extends State { @override Widget build(BuildContext context) { - return Stack( - children: [ - Positioned( - top: 0, - bottom: 0, - left: 50, - child: SizedBox( - key: widget.firstUserListItemKey, - height: 20, - width: 20, - ), + return GroupContextMenu( + group: widget.group, + child: ListTile( + title: Text( + widget.group.groupName, ), - GroupContextMenu( - group: widget.group, - child: ListTile( - title: Text( - widget.group.groupName, - ), - subtitle: (currentMessage == null) - ? Text(context.lang.chatsTapToSend) - : Row( - children: [ - MessageSendStateIcon( - previewMessages, - previewMediaFiles, - lastReaction: lastReaction, - ), - const Text('•'), - const SizedBox(width: 5), - if (currentMessage != null) - LastMessageTime(message: currentMessage!), - FlameCounterWidget( - groupId: widget.group.groupId, - prefix: true, - ), - ], + subtitle: (_currentMessage == null) + ? Text(context.lang.chatsTapToSend) + : Row( + children: [ + MessageSendStateIcon( + _previewMessages, + _previewMediaFiles, + lastReaction: _lastReaction, ), - leading: AvatarIcon(group: widget.group), - trailing: IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - if (hasNonOpenedMediaFile) { - return ChatMessagesView(widget.group); - } else { - return CameraSendToView(widget.group); - } - }, + const Text('•'), + const SizedBox(width: 5), + if (_currentMessage != null) + LastMessageTime(message: _currentMessage!), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, ), - ); - }, - icon: FaIcon( - hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), + ], ), - ), - onTap: onTap, + leading: AvatarIcon(group: widget.group), + trailing: IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + if (_hasNonOpenedMediaFile) { + return ChatMessagesView(widget.group); + } else { + return CameraSendToView(widget.group); + } + }, + ), + ); + }, + icon: FaIcon( + _hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), ), ), - ], + onTap: onTap, + ), ); } } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index c29b4c6..5ed959f 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @@ -255,96 +254,64 @@ class _ChatMessagesViewState extends State { ), ), ), - body: PieCanvas( - theme: getPieCanvasTheme(context), - child: SafeArea( - child: Column( - children: [ - Expanded( - child: ScrollablePositionedList.builder( - reverse: true, - itemCount: messages.length + 1, - itemScrollController: itemScrollController, - itemBuilder: (context, i) { - if (i == messages.length) { - return const Padding( - padding: EdgeInsetsGeometry.only(top: 10), - ); - } - if (messages[i].isDate) { - return ChatDateChip( - item: messages[i], - ); - } else { - final chatMessage = messages[i].message!; - return Transform.translate( - offset: Offset( - (focusedScrollItem == i) - ? (chatMessage.senderId == null) - ? -8 - : 8 - : 0, - 0, - ), - child: Transform.scale( - scale: (focusedScrollItem == i) ? 1.05 : 1, - child: ChatListEntry( - key: Key(chatMessage.messageId), - message: messages[i].message!, - nextMessage: - (i > 0) ? messages[i - 1].message : null, - prevMessage: ((i + 1) < messages.length) - ? messages[i + 1].message - : null, - group: group, - galleryItems: galleryItems, - scrollToMessage: scrollToMessage, - onResponseTriggered: () { - setState(() { - quotesMessage = chatMessage; - }); - textFieldFocus.requestFocus(); - }, - ), - ), - ); - } - }, - ), - ), - if (quotesMessage != null) - Container( - padding: const EdgeInsets.only( - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: ResponsePreview( - message: quotesMessage, - showBorder: true, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ScrollablePositionedList.builder( + reverse: true, + itemCount: messages.length + 1, + itemScrollController: itemScrollController, + itemBuilder: (context, i) { + if (i == messages.length) { + return const Padding( + padding: EdgeInsetsGeometry.only(top: 10), + ); + } + if (messages[i].isDate) { + return ChatDateChip( + item: messages[i], + ); + } else { + final chatMessage = messages[i].message!; + return Transform.translate( + offset: Offset( + (focusedScrollItem == i) + ? (chatMessage.senderId == null) + ? -8 + : 8 + : 0, + 0, + ), + child: Transform.scale( + scale: (focusedScrollItem == i) ? 1.05 : 1, + child: ChatListEntry( + key: Key(chatMessage.messageId), + message: messages[i].message!, + nextMessage: + (i > 0) ? messages[i - 1].message : null, + prevMessage: ((i + 1) < messages.length) + ? messages[i + 1].message + : null, group: group, + galleryItems: galleryItems, + scrollToMessage: scrollToMessage, + onResponseTriggered: () { + setState(() { + quotesMessage = chatMessage; + }); + textFieldFocus.requestFocus(); + }, ), ), - IconButton( - onPressed: () { - setState(() { - quotesMessage = null; - }); - }, - icon: const FaIcon( - FontAwesomeIcons.xmark, - size: 16, - ), - ), - ], - ), - ), - Padding( + ); + } + }, + ), + ), + if (quotesMessage != null) + Container( padding: const EdgeInsets.only( - bottom: 30, left: 20, right: 20, top: 10, @@ -352,50 +319,79 @@ class _ChatMessagesViewState extends State { child: Row( children: [ Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), + child: ResponsePreview( + message: quotesMessage, + showBorder: true, + group: group, ), ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, + IconButton( + onPressed: () { + setState(() { + quotesMessage = null; + }); + }, + icon: const FaIcon( + FontAwesomeIcons.xmark, + size: 16, ), + ), ], ), ), - ], - ), + Padding( + padding: const EdgeInsets.only( + bottom: 30, + left: 20, + right: 20, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: inputTextMessageDeco(context), + ), + ), + if (currentInputText != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), + ), + ], ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index e4b9a19..97d6933 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -162,7 +162,10 @@ class _ChatListEntryState extends State { message: widget.message, group: widget.group, onResponseTriggered: widget.onResponseTriggered!, - child: child, + galleryItems: widget.galleryItems, + child: Container( + child: child, + ), ); } diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 765323e..4d6a33f 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -4,10 +4,10 @@ import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.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/messages.pbserver.dart' as pb; import 'package:twonly/src/services/api/messages.dart'; @@ -16,6 +16,7 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ @@ -23,27 +24,23 @@ class MessageContextMenu extends StatelessWidget { required this.group, required this.child, required this.onResponseTriggered, + required this.galleryItems, super.key, }); final Group group; final Widget child; final Message message; + final List galleryItems; final VoidCallback onResponseTriggered; @override Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ + return ContextMenu( + items: [ if (!message.isDeletedFromSender) - PieAction( - tooltip: Text(context.lang.react), - onSelect: () async { + ContextMenuItem( + title: context.lang.react, + onTap: () async { final layer = await showModalBottomSheet( context: context, backgroundColor: Colors.black, @@ -68,36 +65,38 @@ class MessageContextMenu extends StatelessWidget { null, ); }, - child: const FaIcon(FontAwesomeIcons.faceLaugh), + icon: FontAwesomeIcons.faceLaugh, ), if (!message.isDeletedFromSender) - PieAction( - tooltip: Text(context.lang.reply), - onSelect: onResponseTriggered, - child: const FaIcon(FontAwesomeIcons.reply), + ContextMenuItem( + title: context.lang.reply, + onTap: () async { + onResponseTriggered(); + }, + icon: FontAwesomeIcons.reply, ), if (!message.isDeletedFromSender && message.senderId == null && message.type == MessageType.text) - PieAction( - tooltip: Text(context.lang.edit), - onSelect: () async { + ContextMenuItem( + title: context.lang.edit, + onTap: () async { await editTextMessage(context, message); }, - child: const FaIcon(FontAwesomeIcons.pencil), + icon: FontAwesomeIcons.pencil, ), if (message.content != null) - PieAction( - tooltip: Text(context.lang.copy), - onSelect: () async { + ContextMenuItem( + title: context.lang.copy, + onTap: () async { await Clipboard.setData(ClipboardData(text: message.content!)); await HapticFeedback.heavyImpact(); }, - child: const FaIcon(FontAwesomeIcons.solidCopy), + icon: FontAwesomeIcons.solidCopy, ), - PieAction( - tooltip: Text(context.lang.delete), - onSelect: () async { + ContextMenuItem( + title: context.lang.delete, + onTap: () async { final delete = await showAlertDialog( context, context.lang.deleteTitle, @@ -130,12 +129,12 @@ class MessageContextMenu extends StatelessWidget { } } }, - child: const FaIcon(FontAwesomeIcons.trash), + icon: FontAwesomeIcons.trash, ), if (!message.isDeletedFromSender) - PieAction( - tooltip: Text(context.lang.info), - onSelect: () async { + ContextMenuItem( + title: context.lang.info, + onTap: () async { await Navigator.push( context, MaterialPageRoute( @@ -143,12 +142,13 @@ class MessageContextMenu extends StatelessWidget { return MessageInfoView( message: message, group: group, + galleryItems: galleryItems, ); }, ), ); }, - child: const FaIcon(FontAwesomeIcons.circleInfo), + icon: FontAwesomeIcons.circleInfo, ), ], child: child, diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart index 36bb78f..e2b5acd 100644 --- a/lib/src/views/chats/message_info.view.dart +++ b/lib/src/views/chats/message_info.view.dart @@ -6,6 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.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/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; @@ -16,11 +17,13 @@ class MessageInfoView extends StatefulWidget { const MessageInfoView({ required this.message, required this.group, + required this.galleryItems, super.key, }); final Message message; final Group group; + final List galleryItems; @override State createState() => _MessageInfoViewState(); @@ -164,9 +167,24 @@ class _MessageInfoViewState extends State { child: ListView( children: [ const SizedBox(height: 20), - ChatListEntry( - group: widget.group, - message: widget.message, + Stack( + children: [ + ChatListEntry( + group: widget.group, + message: widget.message, + galleryItems: widget.galleryItems, + ), + Positioned.fill( + child: GestureDetector( + onTap: () { + // In case in ChatListEntry is a image, this prevents to open the image preview. + }, + child: Container( + color: Colors.transparent, + ), + ), + ), + ], ), Text( '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}', diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 0aad997..c1670a6 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -74,34 +73,31 @@ class _StartNewChatView extends State { title: Text(context.lang.startNewChatTitle), ), body: SafeArea( - child: PieCanvas( - theme: getPieCanvasTheme(context), - child: Padding( - padding: - const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: (_) async { - await filterUsers(); - }, - controller: searchUserName, - decoration: getInputDecoration( - context, - context.lang.shareImageSearchAllContacts, - ), + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + await filterUsers(); + }, + controller: searchUserName, + decoration: getInputDecoration( + context, + context.lang.shareImageSearchAllContacts, ), ), - const SizedBox(height: 10), - Expanded( - child: UserList( - contacts, - ), + ), + const SizedBox(height: 10), + Expanded( + child: UserList( + contacts, ), - ], - ), + ), + ], ), ), ), diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index c05a9f6..8b9924a 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -26,7 +26,7 @@ class AvatarIcon extends StatefulWidget { } class _AvatarIconState extends State { - List avatarSVGs = []; + final List _avatarSVGs = []; @override void initState() { @@ -40,20 +40,20 @@ class _AvatarIconState extends State { await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); if (contacts.length == 1) { if (contacts.first.avatarSvgCompressed != null) { - avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); + _avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); } } else { for (final contact in contacts) { if (contact.avatarSvgCompressed != null) { - avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); + _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); } } } // avatarSvg = group!.avatarSvg; } else if (widget.userData?.avatarSvg != null) { - avatarSVGs.add(widget.userData!.avatarSvg!); + _avatarSVGs.add(widget.userData!.avatarSvg!); } else if (widget.contact?.avatarSvgCompressed != null) { - avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); + _avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); } if (mounted) setState(() {}); } @@ -77,10 +77,10 @@ class _AvatarIconState extends State { width: proSize, color: widget.color, child: Center( - child: avatarSVGs.isEmpty + child: _avatarSVGs.isEmpty ? SvgPicture.asset('assets/images/default_avatar.svg') : SvgPicture.string( - avatarSVGs.first, + _avatarSVGs.first, errorBuilder: (context, error, stackTrace) { Log.error('$error'); return Container(); diff --git a/lib/src/views/components/context_menu.component.dart b/lib/src/views/components/context_menu.component.dart new file mode 100644 index 0000000..0cba731 --- /dev/null +++ b/lib/src/views/components/context_menu.component.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class ContextMenu extends StatefulWidget { + const ContextMenu({ + required this.child, + required this.items, + super.key, + }); + + final List items; + final Widget child; + + @override + State createState() => _ContextMenuState(); +} + +class _ContextMenuState extends State { + Offset? _tapPosition; + + Widget _getIcon(IconData icon) { + return Padding( + padding: const EdgeInsets.only(left: 12), + child: FaIcon( + icon, + size: 20, + ), + ); + } + + Future _showCustomMenu() async { + if (_tapPosition == null) { + return; + } + final overlay = Overlay.of(context).context.findRenderObject(); + if (overlay == null) { + return; + } + unawaited(HapticFeedback.heavyImpact()); + + await showMenu( + context: context, + menuPadding: EdgeInsetsGeometry.zero, + elevation: 1, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), // corner radius + ), + popUpAnimationStyle: const AnimationStyle( + duration: Duration.zero, + curve: Curves.fastOutSlowIn, + ), + items: >[ + ...widget.items.map( + (item) => PopupMenuItem( + padding: EdgeInsets.zero, + child: ListTile( + title: Text(item.title), + onTap: () async { + if (mounted) Navigator.pop(context); + await item.onTap(); + }, + leading: _getIcon(item.icon), + ), + ), + ) + ], + position: RelativeRect.fromRect( + _tapPosition! & const Size(40, 40), + Offset.zero & overlay.semanticBounds.size, + ), + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: _showCustomMenu, + onTapDown: (TapDownDetails details) { + _tapPosition = details.globalPosition; + }, + child: widget.child, + ); + } +} + +class ContextMenuItem { + ContextMenuItem({ + required this.title, + required this.onTap, + required this.icon, + }); + final String title; + final Future Function() onTap; + final IconData icon; +} diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index 67605f7..b4d87f0 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -1,14 +1,13 @@ -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; -class GroupContextMenu extends StatefulWidget { +class GroupContextMenu extends StatelessWidget { const GroupContextMenu({ required this.group, required this.child, @@ -17,80 +16,63 @@ class GroupContextMenu extends StatefulWidget { final Widget child; final Group group; - @override - State createState() => _GroupContextMenuState(); -} - -class _GroupContextMenuState extends State { @override Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ - if (!widget.group.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { + return ContextMenu( + items: [ + if (!group.archived) + ContextMenuItem( + title: context.lang.contextMenuArchiveUser, + onTap: () async { const update = GroupsCompanion(archived: Value(true)); if (context.mounted) { - await twonlyDB.groupsDao - .updateGroup(widget.group.groupId, update); + await twonlyDB.groupsDao.updateGroup(group.groupId, update); } }, - child: const FaIcon(FontAwesomeIcons.boxArchive), + icon: FontAwesomeIcons.boxArchive, ), - if (widget.group.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { + if (group.archived) + ContextMenuItem( + title: context.lang.contextMenuUndoArchiveUser, + onTap: () async { const update = GroupsCompanion(archived: Value(false)); if (context.mounted) { - await twonlyDB.groupsDao - .updateGroup(widget.group.groupId, update); + await twonlyDB.groupsDao.updateGroup(group.groupId, update); } }, - child: const FaIcon(FontAwesomeIcons.boxOpen), + icon: FontAwesomeIcons.boxOpen, ), - PieAction( - tooltip: Text(context.lang.contextMenuOpenChat), - onSelect: () async { + ContextMenuItem( + title: context.lang.contextMenuOpenChat, + onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { - return ChatMessagesView(widget.group); + return ChatMessagesView(group); }, ), ); }, - child: const FaIcon(FontAwesomeIcons.solidComments), + icon: FontAwesomeIcons.comments, ), - PieAction( - tooltip: Text( - widget.group.pinned + if (!group.archived) + ContextMenuItem( + title: group.pinned ? context.lang.contextMenuUnpin : context.lang.contextMenuPin, - ), - onSelect: () async { - final update = GroupsCompanion(pinned: Value(!widget.group.pinned)); - if (context.mounted) { - await twonlyDB.groupsDao - .updateGroup(widget.group.groupId, update); - } - }, - child: FaIcon( - widget.group.pinned + onTap: () async { + final update = GroupsCompanion(pinned: Value(!group.pinned)); + if (context.mounted) { + await twonlyDB.groupsDao.updateGroup(group.groupId, update); + } + }, + icon: group.pinned ? FontAwesomeIcons.thumbtackSlash : FontAwesomeIcons.thumbtack, ), - ), ], - child: widget.child, + child: child, ); } } diff --git a/lib/src/views/components/user_context_menu.component.dart b/lib/src/views/components/user_context_menu.component.dart index 2d0f004..7151848 100644 --- a/lib/src/views/components/user_context_menu.component.dart +++ b/lib/src/views/components/user_context_menu.component.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; -class UserContextMenu extends StatefulWidget { +class UserContextMenu extends StatelessWidget { const UserContextMenu({ required this.contact, required this.child, @@ -14,32 +14,26 @@ class UserContextMenu extends StatefulWidget { final Widget child; final Contact contact; - @override - State createState() => _UserContextMenuBlocked(); -} - -class _UserContextMenuBlocked extends State { @override Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - actions: [ - PieAction( - tooltip: Text(context.lang.contextMenuUserProfile), - onSelect: () async { + return ContextMenu( + items: [ + ContextMenuItem( + title: context.lang.contextMenuUserProfile, + onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { - return ContactView(widget.contact.userId); + return ContactView(contact.userId); }, ), ); }, - child: const FaIcon(FontAwesomeIcons.user), + icon: FontAwesomeIcons.user, ), ], - child: widget.child, + child: child, ); } } diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index c38d53e..2df4ce8 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -4,7 +4,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; @@ -29,8 +28,14 @@ class _ContactViewState extends State { context.lang.contactRemoveBody, ); if (remove) { - // trigger deletion for the other user... - await rejectAndHideContact(contact.userId); + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } @@ -192,13 +197,13 @@ class _ContactViewState extends State { text: context.lang.contactBlock, onTap: () => handleUserBlockRequest(contact), ), - BetterListTile( - icon: FontAwesomeIcons.userMinus, - iconSize: 16, - color: Colors.red, - text: context.lang.contactRemove, - onTap: () => handleUserRemoveRequest(contact), - ), + // BetterListTile( + // icon: FontAwesomeIcons.userMinus, + // iconSize: 16, + // color: Colors.red, + // text: context.lang.contactRemove, + // onTap: () => handleUserRemoveRequest(contact), + // ), ], ); }, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 0c24c4a..d7e3bd8 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -3,7 +3,6 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -155,93 +154,90 @@ class HomeViewState extends State { @override Widget build(BuildContext context) { - return PieCanvas( - theme: getPieCanvasTheme(context), - child: Scaffold( - body: GestureDetector( - onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, - child: Stack( - children: [ - HomeViewCameraPreview( - controller: cameraController, - screenshotController: screenshotController, - ), - Shade( - opacity: offsetRatio, - ), - NotificationListener( - onNotification: onPageView, - child: Positioned.fill( - child: PageView( - controller: homeViewPageController, - onPageChanged: (index) { - setState(() { - activePageIdx = index; - }); - }, - children: [ - const ChatListView(), - Container(), - const MemoriesView(), - ], - ), + return Scaffold( + body: GestureDetector( + onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, + child: Stack( + children: [ + HomeViewCameraPreview( + controller: cameraController, + screenshotController: screenshotController, + ), + Shade( + opacity: offsetRatio, + ), + NotificationListener( + onNotification: onPageView, + child: Positioned.fill( + child: PageView( + controller: homeViewPageController, + onPageChanged: (index) { + setState(() { + activePageIdx = index; + }); + }, + children: [ + const ChatListView(), + Container(), + const MemoriesView(), + ], ), ), - Positioned( - left: 0, - top: 0, - right: 0, - bottom: (offsetRatio > 0.25) - ? MediaQuery.sizeOf(context).height * 2 - : 0, - child: Opacity( - opacity: 1 - (offsetRatio * 4) % 1, - child: CameraPreviewControllerView( - cameraController: cameraController, - screenshotController: screenshotController, - selectedCameraDetails: selectedCameraDetails, - selectCamera: selectCamera, - ), + ), + Positioned( + left: 0, + top: 0, + right: 0, + bottom: (offsetRatio > 0.25) + ? MediaQuery.sizeOf(context).height * 2 + : 0, + child: Opacity( + opacity: 1 - (offsetRatio * 4) % 1, + child: CameraPreviewControllerView( + cameraController: cameraController, + screenshotController: screenshotController, + selectedCameraDetails: selectedCameraDetails, + selectCamera: selectCamera, ), ), - ], - ), - ), - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - unselectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), - ), - selectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface, - ), - items: const [ - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.solidComments), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.camera), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.photoFilm), - label: '', ), ], - onTap: (int index) async { - activePageIdx = index; - await homeViewPageController.animateToPage( - index, - duration: const Duration(milliseconds: 100), - curve: Curves.bounceIn, - ); - if (mounted) setState(() {}); - }, - currentIndex: activePageIdx, ), ), + bottomNavigationBar: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + unselectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), + ), + selectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.inverseSurface, + ), + items: const [ + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.solidComments), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.camera), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.photoFilm), + label: '', + ), + ], + onTap: (int index) async { + activePageIdx = index; + await homeViewPageController.animateToPage( + index, + duration: const Duration(milliseconds: 100), + curve: Curves.bounceIn, + ); + if (mounted) setState(() {}); + }, + currentIndex: activePageIdx, + ), ); } } diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 8b383f2..92066d2 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -1,6 +1,5 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -32,53 +31,50 @@ class _PrivacyViewBlockUsers extends State { appBar: AppBar( title: Text(context.lang.settingsPrivacyBlockUsers), ), - body: PieCanvas( - theme: getPieCanvasTheme(context), - child: Padding( - padding: - const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: (value) => setState(() { - filter = value; - }), - decoration: getInputDecoration( - context, - context.lang.searchUsernameInput, - ), + body: Padding( + padding: + const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (value) => setState(() { + filter = value; + }), + decoration: getInputDecoration( + context, + context.lang.searchUsernameInput, ), ), - const SizedBox(height: 20), - Text( - context.lang.settingsPrivacyBlockUsersDesc, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Expanded( - child: StreamBuilder( - stream: allUsers, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Container(); - } + ), + const SizedBox(height: 20), + Text( + context.lang.settingsPrivacyBlockUsersDesc, + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Expanded( + child: StreamBuilder( + stream: allUsers, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } - final filteredContacts = snapshot.data!.where((contact) { - return getContactDisplayName(contact) - .toLowerCase() - .contains(filter.toLowerCase()); - }).toList(); + final filteredContacts = snapshot.data!.where((contact) { + return getContactDisplayName(contact) + .toLowerCase() + .contains(filter.toLowerCase()); + }).toList(); - return UserList( - List.from(filteredContacts), - ); - }, - ), + return UserList( + List.from(filteredContacts), + ); + }, ), - ], - ), + ), + ], ), ), ); diff --git a/pubspec.lock b/pubspec.lock index 99827f9..a1584b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1278,15 +1278,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.0" - pie_menu: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: e1ae0b2dabdfa9ad204b2cf93c48a5962e243c6c - url: "https://github.com/otsmr/flutter-pie-menu.git" - source: git - version: "3.3.2" platform: dependency: transitive description: @@ -1816,10 +1807,10 @@ packages: dependency: transitive description: name: video_player_platform_interface - sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b" + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.0" video_player_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d01d5d0..f9a646d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,9 +59,6 @@ dependencies: path_provider: ^2.1.5 permission_handler: ^12.0.0+1 photo_view: ^0.15.0 - pie_menu: - git: - url: https://github.com/otsmr/flutter-pie-menu.git protobuf: ^4.0.0 provider: ^6.1.2 restart_app: ^1.3.2 From 4914df561029036e34664a7979515f84882f9020 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Oct 2025 23:55:02 +0100 Subject: [PATCH 28/76] linter issues --- lib/src/views/chats/archived_chats.view.dart | 2 +- lib/src/views/chats/chat_list.view.dart | 390 +++++++++--------- .../chat_list_components/group_list_item.dart | 1 - .../components/context_menu.component.dart | 2 +- 4 files changed, 195 insertions(+), 200 deletions(-) diff --git a/lib/src/views/chats/archived_chats.view.dart b/lib/src/views/chats/archived_chats.view.dart index e769afa..a785c0b 100644 --- a/lib/src/views/chats/archived_chats.view.dart +++ b/lib/src/views/chats/archived_chats.view.dart @@ -40,7 +40,7 @@ class _ArchivedChatsViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Archivierte Chats"), + title: const Text('Archivierte Chats'), ), body: ListView( children: _groupsArchived.map((group) { diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 2bbf609..ca1acca 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -105,219 +105,215 @@ class _ChatListViewState extends State { Widget build(BuildContext context) { final isConnected = context.watch().isConnected; final planId = context.watch().plan; - return Container( - child: Scaffold( - appBar: AppBar( - title: Row( - children: [ - GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ProfileView(); - }, - ), - ); - if (!mounted) return; - setState(() {}); // gUser has updated - }, - child: AvatarIcon( - userData: gUser, - fontSize: 14, - color: context.color.onSurface.withAlpha(20), - ), - ), - const SizedBox(width: 10), - const Text('twonly '), - if (planId != 'Free') - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const SubscriptionView(); - }, - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(15), - ), - padding: - const EdgeInsets.symmetric(horizontal: 5, vertical: 3), - child: Text( - planId, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: - isDarkMode(context) ? Colors.black : Colors.white, - ), - ), - ), - ), - ], - ), - actions: [ - const FeedbackIconButton(), - StreamBuilder( - stream: twonlyDB.contactsDao.watchContactsRequested(), - builder: (context, snapshot) { - var count = 0; - if (snapshot.hasData && snapshot.data != null) { - count = snapshot.data!; - } - return NotificationBadge( - count: count.toString(), - child: IconButton( - key: searchForOtherUsers, - icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - ), - ); - }, - ), - IconButton( - onPressed: () async { + return Scaffold( + appBar: AppBar( + title: Row( + children: [ + GestureDetector( + onTap: () async { await Navigator.push( context, MaterialPageRoute( - builder: (context) => const SettingsMainView(), + builder: (context) { + return const ProfileView(); + }, ), ); if (!mounted) return; - setState(() {}); // gUser may has changed... + setState(() {}); // gUser has updated }, - icon: const FaIcon(FontAwesomeIcons.gear, size: 19), - ), - ], - ), - body: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: isConnected ? Container() : const ConnectionInfo(), - ), - Positioned.fill( - child: Container( - child: RefreshIndicator( - onRefresh: () async { - await apiService.close(() {}); - await apiService.connect(force: true); - await Future.delayed(const Duration(seconds: 1)); - }, - child: (_groupsNotPinned.isEmpty && - _groupsPinned.isEmpty && - _groupsArchived.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const AddNewUserView(), - ), - ); - }, - label: Text( - context.lang.chatListViewSearchUserNameBtn), - ), - ), - ) - : ListView.builder( - itemCount: _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0) + - _groupsNotPinned.length + - (_groupsArchived.isNotEmpty ? 1 : 0), - itemBuilder: (context, index) { - if (index >= - _groupsNotPinned.length + - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0)) { - if (_groupsArchived.isEmpty) return Container(); - return ListTile( - title: Text( - "Archivierte Chats (${_groupsArchived.length})", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 13), - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ArchivedChatsView(); - }, - ), - ); - }, - ); - } - // Check if the index is for the pinned users - if (index < _groupsPinned.length) { - final group = _groupsPinned[index]; - return GroupListItem( - key: ValueKey(group.groupId), - group: group, - ); - } - - // If there are pinned users, account for the Divider - var adjustedIndex = index - _groupsPinned.length; - if (_groupsPinned.isNotEmpty && - adjustedIndex == 0) { - return const Divider(); - } - - // Adjust the index for the contacts list - adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); - - // Get the contacts that are not pinned - final group = _groupsNotPinned.elementAt( - adjustedIndex, - ); - return GroupListItem( - key: ValueKey(group.groupId), group: group); - }, - ), - ), + child: AvatarIcon( + userData: gUser, + fontSize: 14, + color: context.color.onSurface.withAlpha(20), ), ), + const SizedBox(width: 10), + const Text('twonly '), + if (planId != 'Free') + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(15), + ), + padding: + const EdgeInsets.symmetric(horizontal: 5, vertical: 3), + child: Text( + planId, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), + ), + ), + ), ], ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const StartNewChatView(); + actions: [ + const FeedbackIconButton(), + StreamBuilder( + stream: twonlyDB.contactsDao.watchContactsRequested(), + builder: (context, snapshot) { + var count = 0; + if (snapshot.hasData && snapshot.data != null) { + count = snapshot.data!; + } + return NotificationBadge( + count: count.toString(), + child: IconButton( + key: searchForOtherUsers, + icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); }, ), ); }, - child: const FaIcon(FontAwesomeIcons.penToSquare), ), + IconButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsMainView(), + ), + ); + if (!mounted) return; + setState(() {}); // gUser may has changed... + }, + icon: const FaIcon(FontAwesomeIcons.gear, size: 19), + ), + ], + ), + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: isConnected ? Container() : const ConnectionInfo(), + ), + Positioned.fill( + child: RefreshIndicator( + onRefresh: () async { + await apiService.close(() {}); + await apiService.connect(force: true); + await Future.delayed(const Duration(seconds: 1)); + }, + child: (_groupsNotPinned.isEmpty && + _groupsPinned.isEmpty && + _groupsArchived.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + label: Text( + context.lang.chatListViewSearchUserNameBtn, + ), + ), + ), + ) + : ListView.builder( + itemCount: _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + + _groupsNotPinned.length + + (_groupsArchived.isNotEmpty ? 1 : 0), + itemBuilder: (context, index) { + if (index >= + _groupsNotPinned.length + + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (_groupsArchived.isEmpty) return Container(); + return ListTile( + title: Text( + 'Archivierte Chats (${_groupsArchived.length})', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ArchivedChatsView(); + }, + ), + ); + }, + ); + } + // Check if the index is for the pinned users + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItem( + key: ValueKey(group.groupId), + group: group, + ); + } + + // If there are pinned users, account for the Divider + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { + return const Divider(); + } + + // Adjust the index for the contacts list + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); + + // Get the contacts that are not pinned + final group = _groupsNotPinned.elementAt( + adjustedIndex, + ); + return GroupListItem( + key: ValueKey(group.groupId), + group: group, + ); + }, + ), + ), + ), + ], + ), + floatingActionButton: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const StartNewChatView(); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.penToSquare), ), ), ); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 417cf8d..597f622 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; diff --git a/lib/src/views/components/context_menu.component.dart b/lib/src/views/components/context_menu.component.dart index 0cba731..c2bd12e 100644 --- a/lib/src/views/components/context_menu.component.dart +++ b/lib/src/views/components/context_menu.component.dart @@ -66,7 +66,7 @@ class _ContextMenuState extends State { leading: _getIcon(item.icon), ), ), - ) + ), ], position: RelativeRect.fromRect( _tapPosition! & const Size(40, 40), From 071c5c2a0d53b6fbd4eca81839c3e4184baafc54 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Oct 2025 13:23:35 +0100 Subject: [PATCH 29/76] purge text messages fix multiple bugs --- lib/main.dart | 3 +- lib/src/database/daos/contacts.dao.dart | 13 ++++- lib/src/database/daos/messages.dao.dart | 56 ++++++++++++------- lib/src/database/tables/groups.table.dart | 2 +- lib/src/database/twonly.db.g.dart | 2 +- lib/src/localization/app_de.arb | 7 ++- lib/src/localization/app_en.arb | 7 ++- .../generated/app_localizations.dart | 30 ++++++++++ .../generated/app_localizations_de.dart | 15 +++++ .../generated/app_localizations_en.dart | 15 +++++ .../api/mediafiles/upload.service.dart | 14 +++-- .../mediafiles/mediafile.service.dart | 2 + lib/src/utils/misc.dart | 15 +++-- .../camera_preview_components/send_to.dart | 8 +-- .../views/camera/share_image_editor_view.dart | 7 ++- lib/src/views/chats/archived_chats.view.dart | 3 +- lib/src/views/chats/chat_list.view.dart | 2 +- .../chat_list_components/group_list_item.dart | 9 ++- .../last_message_time.dart | 20 ++++--- lib/src/views/chats/chat_messages.view.dart | 5 +- .../message_send_state_icon.dart | 10 ++++ lib/src/views/contact/contact.view.dart | 5 +- .../memories/memories_photo_slider.view.dart | 22 +++++++- .../views/settings/profile/profile.view.dart | 1 + .../views/settings/settings_main.view.dart | 3 +- 25 files changed, 207 insertions(+), 69 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1004f5c..ec86621 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,13 +57,12 @@ void main() async { await initFileDownloader(); unawaited(MediaFileService.purgeTempFolder()); + await twonlyDB.messagesDao.purgeMessageTable(); // await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.signalDao.purgeOutDatedPreKeys(); - // Purge media files in the background - // unawaited(purgeReceivedMediaFiles()); // unawaited(purgeSendMediaFiles()); // unawaited(performTwonlySafeBackup()); diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 6792ad0..07a730e 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -121,7 +121,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { } } -String getContactDisplayName(Contact user) { +String getContactDisplayName(Contact user, {int? maxLength}) { var name = user.username; if (user.nickName != null && user.nickName != '') { name = user.nickName!; @@ -131,12 +131,19 @@ String getContactDisplayName(Contact user) { if (user.accountDeleted) { name = applyStrikethrough(name); } - if (name.length > 27) { - return '${name.substring(0, 27 - 3)}...'; + if (maxLength != null) { + name = substringBy(name, maxLength); } return name; } +String substringBy(String string, int maxLength) { + if (string.length > maxLength) { + return '${string.substring(0, maxLength - 3)}...'; + } + return string; +} + String getContactDisplayNameOld(old.Contact user) { var name = user.username; if (user.nickName != null && user.nickName != '') { diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 02aac49..bc6a26e 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -68,16 +68,20 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Stream> watchByGroupId(String groupId) { - return ((select(messages)..where((t) => t.groupId.equals(groupId))) + return ((select(messages) + ..where( + (t) => + t.groupId.equals(groupId) & + (t.isDeletedFromSender.equals(true) | + ((t.type.equals(MessageType.text.name) & + t.content.isNotNull()) | + (t.type.equals(MessageType.media.name) & + t.mediaId.isNotNull()))), + )) ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) .watch(); } - // Stream> watchMembersByGroupId(String groupId) { - // return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) - // .watch(); - // } - Stream> watchMembersByGroupId(String groupId) { final query = (select(groupMembers).join([ leftOuterJoin( @@ -101,21 +105,31 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .watchSingleOrNull(); } - // Future removeOldMessages() { - // return (update(messages) - // ..where( - // (t) => - // (t.openedAt.isSmallerThanValue( - // DateTime.now().subtract(const Duration(days: 1)), - // ) | - // (t.sendAt.isSmallerThanValue( - // DateTime.now().subtract(const Duration(days: 3)), - // ) & - // t.errorWhileSending.equals(true))) & - // t.kind.equals(MessageKind.textMessage.name), - // )) - // .write(const MessagesCompanion(contentJson: Value(null))); - // } + Future purgeMessageTable() async { + final allGroups = await select(groups).get(); + + for (final group in allGroups) { + final deletionTime = DateTime.now().subtract( + Duration( + milliseconds: group.deleteMessagesAfterMilliseconds, + ), + ); + final affected = await (delete(messages) + ..where( + (m) => + m.groupId.equals(group.groupId) & + // m.messageId.equals(lastMessage.messageId).not() & + (m.mediaStored.equals(true) & + m.isDeletedFromSender.equals(true) | + m.mediaStored.equals(false)) & + (m.openedAt.isSmallerThanValue(deletionTime) | + (m.isDeletedFromSender.equals(true) & + m.createdAt.isSmallerThanValue(deletionTime))), + )) + .go(); + Log.info('Deleted $affected messages.'); + } + } // Future> getAllMessagesPendingDownloading() { // return (select(messages) diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index bd1ad71..9bbac87 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -18,7 +18,7 @@ class Groups extends Table { boolean().withDefault(const Constant(false))(); IntColumn get deleteMessagesAfterMilliseconds => - integer().withDefault(const Constant(1000 * 60 * 24))(); + integer().withDefault(const Constant(1000 * 60 * 60 * 24))(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 95b15a5..9c63e40 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -734,7 +734,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { 'delete_messages_after_milliseconds', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(1000 * 60 * 24)); + defaultValue: const Constant(1000 * 60 * 60 * 24)); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 375158c..9b1026f 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -352,5 +352,10 @@ "received": "Empfangen", "opened": "Geöffnet", "waitingForInternet": "Warten auf Internet", - "editHistory": "Bearbeitungshistorie" + "editHistory": "Bearbeitungshistorie", + "archivedChats": "Archivierte Chats", + "durationShortSecond": "Sek.", + "durationShortMinute": "Min.", + "durationShortHour": "Std", + "durationShortDays": "Tagen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index d169e57..795dfb6 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -508,5 +508,10 @@ "received": "Received", "opened": "Opened", "waitingForInternet": "Waiting for internet", - "editHistory": "Edit history" + "editHistory": "Edit history", + "archivedChats": "Archived chats", + "durationShortSecond": "Sec.", + "durationShortMinute": "Min.", + "durationShortHour": "Hrs.", + "durationShortDays": "Days" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d2f423d..90fd5c2 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2155,6 +2155,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Edit history'** String get editHistory; + + /// No description provided for @archivedChats. + /// + /// In en, this message translates to: + /// **'Archived chats'** + String get archivedChats; + + /// No description provided for @durationShortSecond. + /// + /// In en, this message translates to: + /// **'Sec.'** + String get durationShortSecond; + + /// No description provided for @durationShortMinute. + /// + /// In en, this message translates to: + /// **'Min.'** + String get durationShortMinute; + + /// No description provided for @durationShortHour. + /// + /// In en, this message translates to: + /// **'Hrs.'** + String get durationShortHour; + + /// No description provided for @durationShortDays. + /// + /// In en, this message translates to: + /// **'Days'** + String get durationShortDays; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index b4ed1fe..e41102a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1143,4 +1143,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get editHistory => 'Bearbeitungshistorie'; + + @override + String get archivedChats => 'Archivierte Chats'; + + @override + String get durationShortSecond => 'Sek.'; + + @override + String get durationShortMinute => 'Min.'; + + @override + String get durationShortHour => 'Std'; + + @override + String get durationShortDays => 'Tagen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 3337bed..71ffab8 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1136,4 +1136,19 @@ class AppLocalizationsEn extends AppLocalizations { @override String get editHistory => 'Edit history'; + + @override + String get archivedChats => 'Archived chats'; + + @override + String get durationShortSecond => 'Sec.'; + + @override + String get durationShortMinute => 'Min.'; + + @override + String get durationShortHour => 'Hrs.'; + + @override + String get durationShortDays => 'Days'; } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 5e97149..55a7ba0 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -71,7 +71,8 @@ Future insertMediaFileInMessagesTable( } Future startBackgroundMediaUpload(MediaFileService mediaService) async { - if (mediaService.mediaFile.uploadState == UploadState.initialized) { + if (mediaService.mediaFile.uploadState == UploadState.initialized || + mediaService.mediaFile.uploadState == UploadState.preprocessing) { await mediaService.setUploadState(UploadState.preprocessing); if (!mediaService.tempPath.existsSync()) { await mediaService.compressMedia(); @@ -84,7 +85,9 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { if (!mediaService.uploadRequestPath.existsSync()) { await _createUploadRequest(mediaService); } - await mediaService.setUploadState(UploadState.uploading); + if (mediaService.uploadRequestPath.existsSync()) { + await mediaService.setUploadState(UploadState.uploading); + } } if (mediaService.mediaFile.uploadState == UploadState.uploading) { @@ -109,8 +112,6 @@ Future _encryptMediaFiles(MediaFileService mediaService) async { mediaService.encryptedPath .writeAsBytesSync(Uint8List.fromList(secretBox.cipherText)); - - await mediaService.setUploadState(UploadState.uploading); } Future _createUploadRequest(MediaFileService media) async { @@ -121,6 +122,11 @@ Future _createUploadRequest(MediaFileService media) async { final messages = await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId); + if (messages.isEmpty) { + // There where no user selected who should receive the image, so waiting with this step... + return; + } + for (final message in messages) { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(message.groupId); diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index e0fcb7e..e2852d6 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -53,6 +53,8 @@ class MediaFileService { final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + // in case messages in empty the file will be deleted, as delete is true by default + for (final message in messages) { if (message.senderId == null) { // Media was send by me diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b488fdb..4517571 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -89,27 +89,26 @@ String errorCodeToText(BuildContext context, ErrorCode code) { case ErrorCode.PlanUpgradeNotYearly: return context.lang.errorPlanUpgradeNotYearly; } - return code.toString(); // Fallback for unrecognized keys + return code.toString(); } -String formatDuration(int seconds) { +String formatDuration(BuildContext context, int seconds) { if (seconds < 60) { - return '$seconds Sec.'; + return '$seconds ${context.lang.durationShortSecond}'; } else if (seconds < 3600) { final minutes = seconds ~/ 60; - return '$minutes Min.'; + return '$minutes ${context.lang.durationShortMinute}'; } else if (seconds < 86400) { final hours = seconds ~/ 3600; - return '$hours Hrs.'; // Assuming "Stu." is for hours + return '$hours ${context.lang.durationShortHour}'; } else { final days = seconds ~/ 86400; - return '$days Days'; + return '$days ${context.lang.durationShortDays}'; } } InputDecoration getInputDecoration(BuildContext context, String hintText) { - final primaryColor = - Theme.of(context).colorScheme.primary; // Get the primary color + final primaryColor = Theme.of(context).colorScheme.primary; return InputDecoration( hintText: hintText, focusedBorder: OutlineInputBorder( diff --git a/lib/src/views/camera/camera_preview_components/send_to.dart b/lib/src/views/camera/camera_preview_components/send_to.dart index b061a9b..76e7a7c 100644 --- a/lib/src/views/camera/camera_preview_components/send_to.dart +++ b/lib/src/views/camera/camera_preview_components/send_to.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; class SendToWidget extends StatelessWidget { @@ -40,7 +41,7 @@ class SendToWidget extends StatelessWidget { style: textStyle, ), Text( - sendTo, + substringBy(sendTo, 20), textAlign: TextAlign.center, style: boldTextStyle, // Use the bold text style here ), @@ -48,9 +49,4 @@ class SendToWidget extends StatelessWidget { ), ); } - - String getContactDisplayName(String contact) { - // Replace this with your actual logic to get the contact display name - return contact; // Placeholder implementation - } } diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 6cf4910..d84708d 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; @@ -75,8 +76,8 @@ class _ShareImageEditorView extends State { if (widget.imageBytesFuture != null) { loadImage(widget.imageBytesFuture!); } else { - if (widget.mediaFileService.storedPath.existsSync()) { - loadImage(widget.mediaFileService.storedPath.readAsBytes()); + if (widget.mediaFileService.tempPath.existsSync()) { + loadImage(widget.mediaFileService.tempPath.readAsBytes()); } } } @@ -482,7 +483,7 @@ class _ShareImageEditorView extends State { label: Text( (widget.sendToGroup == null) ? context.lang.shareImagedEditorShareWith - : widget.sendToGroup!.groupName, + : substringBy(widget.sendToGroup!.groupName, 15), style: const TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/chats/archived_chats.view.dart b/lib/src/views/chats/archived_chats.view.dart index a785c0b..0f0847a 100644 --- a/lib/src/views/chats/archived_chats.view.dart +++ b/lib/src/views/chats/archived_chats.view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; class ArchivedChatsView extends StatefulWidget { @@ -40,7 +41,7 @@ class _ArchivedChatsViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Archivierte Chats'), + title: Text(context.lang.archivedChats), ), body: ListView( children: _groupsArchived.map((group) { diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index ca1acca..575300d 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -252,7 +252,7 @@ class _ChatListViewState extends State { if (_groupsArchived.isEmpty) return Container(); return ListTile( title: Text( - 'Archivierte Chats (${_groupsArchived.length})', + '${context.lang.archivedChats} (${_groupsArchived.length})', textAlign: TextAlign.center, style: const TextStyle(fontSize: 13), ), diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 597f622..729ba02 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; 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'; @@ -208,10 +209,12 @@ class _UserListItem extends State { group: widget.group, child: ListTile( title: Text( - widget.group.groupName, + substringBy(widget.group.groupName, 30), ), subtitle: (_currentMessage == null) - ? Text(context.lang.chatsTapToSend) + ? (widget.group.totalMediaCounter == 0) + ? Text(context.lang.chatsTapToSend) + : LastMessageTime(dateTime: widget.group.lastMessageExchange) : Row( children: [ MessageSendStateIcon( @@ -222,7 +225,7 @@ class _UserListItem extends State { const Text('•'), const SizedBox(width: 5), if (_currentMessage != null) - LastMessageTime(message: _currentMessage!), + LastMessageTime(message: _currentMessage), FlameCounterWidget( groupId: widget.group.groupId, prefix: true, diff --git a/lib/src/views/chats/chat_list_components/last_message_time.dart b/lib/src/views/chats/chat_list_components/last_message_time.dart index b81981a..6b82d30 100644 --- a/lib/src/views/chats/chat_list_components/last_message_time.dart +++ b/lib/src/views/chats/chat_list_components/last_message_time.dart @@ -6,9 +6,10 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; class LastMessageTime extends StatefulWidget { - const LastMessageTime({required this.message, super.key}); + const LastMessageTime({this.message, this.dateTime, super.key}); - final Message message; + final Message? message; + final DateTime? dateTime; @override State createState() => _LastMessageTimeState(); @@ -24,12 +25,17 @@ class _LastMessageTimeState extends State { // Change the color every 200 milliseconds updateTime = Timer.periodic(const Duration(milliseconds: 500), (timer) async { - final lastAction = await twonlyDB.messagesDao - .getLastMessageAction(widget.message.messageId); - setState(() { + if (widget.message != null) { + final lastAction = await twonlyDB.messagesDao + .getLastMessageAction(widget.message!.messageId); lastMessageInSeconds = DateTime.now() - .difference(lastAction?.actionAt ?? widget.message.createdAt) + .difference(lastAction?.actionAt ?? widget.message!.createdAt) .inSeconds; + } else if (widget.dateTime != null) { + lastMessageInSeconds = + DateTime.now().difference(widget.dateTime!).inSeconds; + } + setState(() { if (lastMessageInSeconds < 0) { lastMessageInSeconds = 0; } @@ -46,7 +52,7 @@ class _LastMessageTimeState extends State { @override Widget build(BuildContext context) { return Text( - formatDuration(lastMessageInSeconds), + formatDuration(context, lastMessageInSeconds), style: const TextStyle(fontSize: 12), ); } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 5ed959f..e7191dd 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.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'; @@ -241,7 +242,9 @@ class _ChatMessagesViewState extends State { color: Colors.transparent, child: Row( children: [ - Text(group.groupName), + Text( + substringBy(widget.group.groupName, 20), + ), const SizedBox(width: 10), VerifiedShield(key: verifyShieldKey, group: group), const SizedBox(width: 10), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 9e50231..cb378a9 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -144,6 +144,16 @@ class _MessageSendStateIconState extends State { case MessageSendState.sending: icon = getLoaderIcon(color); text = context.lang.messageSendState_Sending; + + if (mediaFile != null) { + if (mediaFile.uploadState == UploadState.uploadLimitReached) { + text = 'Upload Limit erreicht'; + } + if (mediaFile.uploadState == UploadState.preprocessing) { + text = 'Wird verarbeitet'; + } + } + hasLoader = true; case MessageSendState.receiving: icon = getLoaderIcon(color); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 2df4ce8..b615549 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -24,7 +24,8 @@ class _ContactViewState extends State { Future handleUserRemoveRequest(Contact contact) async { final remove = await showAlertDialog( context, - context.lang.contactRemoveTitle(getContactDisplayName(contact)), + context.lang + .contactRemoveTitle(getContactDisplayName(contact, maxLength: 20)), context.lang.contactRemoveBody, ); if (remove) { @@ -124,7 +125,7 @@ class _ContactViewState extends State { child: VerifiedShield(key: GlobalKey(), contact: contact), ), Text( - getContactDisplayName(contact), + getContactDisplayName(contact, maxLength: 20), style: const TextStyle(fontSize: 20), ), // if (flameCounter > 0) diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 55e298b..99b1aa2 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -5,6 +5,8 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; @@ -117,12 +119,28 @@ class _MemoriesPhotoSliderViewState extends State { FilledButton.icon( icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { + final orgMediaService = + widget.galleryItems[currentIndex].mediaService; + + final newMediaService = await initializeMediaUpload( + orgMediaService.mediaFile.type, + gUser.defaultShowTime, + ); + if (newMediaService == null) { + Log.error('Could not create new mediaFIle'); + return; + } + + orgMediaService.storedPath + .copySync(newMediaService.tempPath.path); + + if (!context.mounted) return; + await Navigator.push( context, MaterialPageRoute( builder: (context) => ShareImageEditorView( - mediaFileService: widget - .galleryItems[currentIndex].mediaService, + mediaFileService: newMediaService, sharedFromGallery: true, ), ), diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 104bde0..8659f75 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -114,6 +114,7 @@ Future showDisplayNameChangeDialog( content: TextField( controller: controller, autofocus: true, + maxLength: 30, decoration: InputDecoration( hintText: context.lang.settingsProfileEditDisplayNameNew, ), diff --git a/lib/src/views/settings/settings_main.view.dart b/lib/src/views/settings/settings_main.view.dart index b58efcd..6c818b5 100644 --- a/lib/src/views/settings/settings_main.view.dart +++ b/lib/src/views/settings/settings_main.view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; @@ -63,7 +64,7 @@ class _SettingsMainViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - gUser.displayName, + substringBy(gUser.displayName, 27), style: const TextStyle(fontSize: 20), textAlign: TextAlign.left, ), From bd4c30ed4d1b3e6c092ee454b62b4869e4983697 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Oct 2025 14:28:44 +0100 Subject: [PATCH 30/76] fixing ios push notifications --- .../NotificationService.swift | 18 +++++++++++++++--- lib/src/services/api/messages.dart | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 57de2d5..c2c6048 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -35,6 +35,7 @@ class NotificationService: UNNotificationServiceExtension { bestAttemptContent.body = data!.body bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) } else { + NSLog("Could not decrypt message. Show default.") bestAttemptContent.title = "\(bestAttemptContent.title)" } @@ -81,7 +82,8 @@ func getPushNotificationData(pushData: String) -> ( key: pushKey.key, pushData: pushData) if pushNotification != nil { pushUser = tryPushUser - if pushNotification!.messageID <= pushUser!.lastMessageID { + if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID) + { return ("blocked", "blocked", 0) } break @@ -125,6 +127,16 @@ func getPushNotificationData(pushData: String) -> ( } } +func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool { + guard uuid1.count >= 8, uuid2.count >= 8 else { return true } + let hex1 = String(uuid1.prefix(8)) + let hex2 = String(uuid2.prefix(8)) + guard let timestamp1 = UInt32(hex1, radix: 16), + let timestamp2 = UInt32(hex2, radix: 16) + else { return true } + return timestamp1 > timestamp2 +} + func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNotification? { do { @@ -152,8 +164,8 @@ func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNo func getPushUsers() -> [PushUser]? { // Retrieve the data from secure storage (Keychain) - guard let pushUsersB64 = readFromKeychain(key: "receiving_push_keys") else { - NSLog("No data found for key: receiving_push_keys") + guard let pushUsersB64 = readFromKeychain(key: "push_keys_receiving") else { + NSLog("No data found for key: push_keys_receiving") return nil } guard let pushUsersBytes = Data(base64Encoded: pushUsersB64) else { diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index b6591f4..2345e02 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -70,7 +70,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ ); Uint8List? pushData; - if (pushNotification != null) { + if (pushNotification != null && receipt.retryCount <= 3) { + /// In case the message has to be resend more than three times, do not show a notification again... pushData = await encryptPushNotification(receipt.contactId, pushNotification); } From 35152cce23957630ba60b005b316ed0ae906c6a9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Oct 2025 23:26:24 +0100 Subject: [PATCH 31/76] integrating ffmpeg --- CHANGELOG.md | 22 +-- ios/Podfile.lock | 15 +- lib/src/database/tables/mediafiles.table.dart | 1 + lib/src/database/twonly.db.g.dart | 61 +++++++ .../api/mediafiles/upload.service.dart | 3 + .../contact.server_messages.dart | 17 ++ .../mediafiles/compression.service.dart | 76 ++++---- .../mediafiles/mediafile.service.dart | 26 ++- .../mediafiles/thumbnail.service.dart | 32 ++-- .../zoom_selector.dart | 7 +- .../camera_preview_controller_view.dart | 164 +++++++----------- lib/src/views/camera/camera_send_to_view.dart | 6 +- .../views/camera/image_editor/data/layer.dart | 4 +- .../image_editor/layers/filter_layer.dart | 1 + .../best_friends_selector.dart | 5 +- .../views/camera/share_image_editor_view.dart | 75 +++++--- lib/src/views/camera/share_image_view.dart | 5 +- .../chat_text_entry.dart | 10 +- lib/src/views/components/better_text.dart | 7 +- lib/src/views/home.view.dart | 13 +- .../updates/62_database_migration.view.dart | 2 +- pubspec.lock | 36 ++-- pubspec.yaml | 2 +- 23 files changed, 351 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e04885f..0fdcb3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.0.62 -- Support for Groups -- Editing of text messages -- Deletion of messages -- Various UI improvements like a new context-menu -- Client-to-client (C2C) protocol converted to ProtoBuf -- Use of UUIDs in the database -- Completely new database schema -- Improved reliability of C2C messages -- Improved video handling -- Various bug fixes +- Support for groups +- Edit & Delete messages +- Switched to FFmpeg for improved video compression + - Video max. length increased to 60 seconds + - Removing audio after recording is possible + - Edited image is now embedded into the video +- New context menu and other UI enhancements +- Client-to-client protocol migrated to Protocol Buffers (Protobuf) +- Database identifiers converted to UUIDs +- Completely redesigned database schema +- Improved reliability of client-to-client messaging +- Multiple bug fixes ## 0.0.61 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3d0a176..00f096a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,11 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - ffmpeg_kit_flutter_new_min_gpl (7.1.1): + - ffmpeg_kit_flutter_new_min_gpl/min-gpl (= 7.1.1) + - Flutter + - ffmpeg_kit_flutter_new_min_gpl/min-gpl (7.1.1): + - Flutter - Firebase (12.4.0): - Firebase/Core (= 12.4.0) - Firebase/Core (12.4.0): @@ -233,8 +238,6 @@ PODS: - SwiftProtobuf (1.32.0) - url_launcher_ios (0.0.1): - Flutter - - video_compress (0.3.0): - - Flutter - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS @@ -248,6 +251,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - ffmpeg_kit_flutter_new_min_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) @@ -275,7 +279,6 @@ 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`) - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) @@ -312,6 +315,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cryptography_flutter_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + ffmpeg_kit_flutter_new_min_gpl: + :path: ".symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -354,8 +359,6 @@ 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" video_thumbnail: @@ -367,6 +370,7 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + ffmpeg_kit_flutter_new_min_gpl: 79212bc20869b4e12ec06705724c26b016e9d58e Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 @@ -407,7 +411,6 @@ SPEC CHECKSUMS: sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftProtobuf: 81e341191afbddd64aa031bd12862dccfab2f639 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b - video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 15ba964..585b49d 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -52,6 +52,7 @@ class MediaFiles extends Table { text().map(IntListTypeConverter()).nullable()(); IntColumn get displayLimitInMilliseconds => integer().nullable()(); + BoolColumn get removeAudio => boolean().nullable()(); BlobColumn get downloadToken => blob().nullable()(); BlobColumn get encryptionKey => blob().nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 9c63e40..cfe85e9 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1561,6 +1561,15 @@ class $MediaFilesTable extends MediaFiles late final GeneratedColumn displayLimitInMilliseconds = GeneratedColumn('display_limit_in_milliseconds', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _removeAudioMeta = + const VerificationMeta('removeAudio'); + @override + late final GeneratedColumn removeAudio = GeneratedColumn( + 'remove_audio', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("remove_audio" IN (0, 1))')); static const VerificationMeta _downloadTokenMeta = const VerificationMeta('downloadToken'); @override @@ -1604,6 +1613,7 @@ class $MediaFilesTable extends MediaFiles stored, reuploadRequestedBy, displayLimitInMilliseconds, + removeAudio, downloadToken, encryptionKey, encryptionMac, @@ -1649,6 +1659,12 @@ class $MediaFilesTable extends MediaFiles data['display_limit_in_milliseconds']!, _displayLimitInMillisecondsMeta)); } + if (data.containsKey('remove_audio')) { + context.handle( + _removeAudioMeta, + removeAudio.isAcceptableOrUnknown( + data['remove_audio']!, _removeAudioMeta)); + } if (data.containsKey('download_token')) { context.handle( _downloadTokenMeta, @@ -1709,6 +1725,8 @@ class $MediaFilesTable extends MediaFiles displayLimitInMilliseconds: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}display_limit_in_milliseconds']), + removeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}remove_audio']), downloadToken: attachedDatabase.typeMapping .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), encryptionKey: attachedDatabase.typeMapping @@ -1756,6 +1774,7 @@ class MediaFile extends DataClass implements Insertable { final bool stored; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; + final bool? removeAudio; final Uint8List? downloadToken; final Uint8List? encryptionKey; final Uint8List? encryptionMac; @@ -1771,6 +1790,7 @@ class MediaFile extends DataClass implements Insertable { required this.stored, this.reuploadRequestedBy, this.displayLimitInMilliseconds, + this.removeAudio, this.downloadToken, this.encryptionKey, this.encryptionMac, @@ -1804,6 +1824,9 @@ class MediaFile extends DataClass implements Insertable { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds); } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable(removeAudio); + } if (!nullToAbsent || downloadToken != null) { map['download_token'] = Variable(downloadToken); } @@ -1840,6 +1863,9 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds == null && nullToAbsent ? const Value.absent() : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), downloadToken: downloadToken == null && nullToAbsent ? const Value.absent() : Value(downloadToken), @@ -1875,6 +1901,7 @@ class MediaFile extends DataClass implements Insertable { serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: serializer.fromJson(json['displayLimitInMilliseconds']), + removeAudio: serializer.fromJson(json['removeAudio']), downloadToken: serializer.fromJson(json['downloadToken']), encryptionKey: serializer.fromJson(json['encryptionKey']), encryptionMac: serializer.fromJson(json['encryptionMac']), @@ -1899,6 +1926,7 @@ class MediaFile extends DataClass implements Insertable { 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), + 'removeAudio': serializer.toJson(removeAudio), 'downloadToken': serializer.toJson(downloadToken), 'encryptionKey': serializer.toJson(encryptionKey), 'encryptionMac': serializer.toJson(encryptionMac), @@ -1917,6 +1945,7 @@ class MediaFile extends DataClass implements Insertable { bool? stored, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -1938,6 +1967,7 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds: displayLimitInMilliseconds.present ? displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, downloadToken: downloadToken.present ? downloadToken.value : this.downloadToken, encryptionKey: @@ -1971,6 +2001,8 @@ class MediaFile extends DataClass implements Insertable { displayLimitInMilliseconds: data.displayLimitInMilliseconds.present ? data.displayLimitInMilliseconds.value : this.displayLimitInMilliseconds, + removeAudio: + data.removeAudio.present ? data.removeAudio.value : this.removeAudio, downloadToken: data.downloadToken.present ? data.downloadToken.value : this.downloadToken, @@ -1999,6 +2031,7 @@ class MediaFile extends DataClass implements Insertable { ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') ..write('encryptionMac: $encryptionMac, ') @@ -2019,6 +2052,7 @@ class MediaFile extends DataClass implements Insertable { stored, reuploadRequestedBy, displayLimitInMilliseconds, + removeAudio, $driftBlobEquality.hash(downloadToken), $driftBlobEquality.hash(encryptionKey), $driftBlobEquality.hash(encryptionMac), @@ -2037,6 +2071,7 @@ class MediaFile extends DataClass implements Insertable { other.stored == this.stored && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && @@ -2055,6 +2090,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value stored; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; + final Value removeAudio; final Value downloadToken; final Value encryptionKey; final Value encryptionMac; @@ -2071,6 +2107,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), this.encryptionMac = const Value.absent(), @@ -2088,6 +2125,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), this.downloadToken = const Value.absent(), this.encryptionKey = const Value.absent(), this.encryptionMac = const Value.absent(), @@ -2106,6 +2144,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? stored, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, + Expression? removeAudio, Expression? downloadToken, Expression? encryptionKey, Expression? encryptionMac, @@ -2126,6 +2165,7 @@ class MediaFilesCompanion extends UpdateCompanion { 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, if (downloadToken != null) 'download_token': downloadToken, if (encryptionKey != null) 'encryption_key': encryptionKey, if (encryptionMac != null) 'encryption_mac': encryptionMac, @@ -2145,6 +2185,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? stored, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, + Value? removeAudio, Value? downloadToken, Value? encryptionKey, Value? encryptionMac, @@ -2163,6 +2204,7 @@ class MediaFilesCompanion extends UpdateCompanion { reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, downloadToken: downloadToken ?? this.downloadToken, encryptionKey: encryptionKey ?? this.encryptionKey, encryptionMac: encryptionMac ?? this.encryptionMac, @@ -2209,6 +2251,9 @@ class MediaFilesCompanion extends UpdateCompanion { map['display_limit_in_milliseconds'] = Variable(displayLimitInMilliseconds.value); } + if (removeAudio.present) { + map['remove_audio'] = Variable(removeAudio.value); + } if (downloadToken.present) { map['download_token'] = Variable(downloadToken.value); } @@ -2242,6 +2287,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') ..write('downloadToken: $downloadToken, ') ..write('encryptionKey: $encryptionKey, ') ..write('encryptionMac: $encryptionMac, ') @@ -7795,6 +7841,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, + Value removeAudio, Value downloadToken, Value encryptionKey, Value encryptionMac, @@ -7812,6 +7859,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, + Value removeAudio, Value downloadToken, Value encryptionKey, Value encryptionMac, @@ -7887,6 +7935,9 @@ class $$MediaFilesTableFilterComposer column: $table.displayLimitInMilliseconds, builder: (column) => ColumnFilters(column)); + ColumnFilters get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnFilters(column)); @@ -7966,6 +8017,9 @@ class $$MediaFilesTableOrderingComposer column: $table.displayLimitInMilliseconds, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnOrderings(column)); @@ -8025,6 +8079,9 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get displayLimitInMilliseconds => $composableBuilder( column: $table.displayLimitInMilliseconds, builder: (column) => column); + GeneratedColumn get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => column); @@ -8094,6 +8151,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -8111,6 +8169,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, downloadToken: downloadToken, encryptionKey: encryptionKey, encryptionMac: encryptionMac, @@ -8128,6 +8187,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), Value downloadToken = const Value.absent(), Value encryptionKey = const Value.absent(), Value encryptionMac = const Value.absent(), @@ -8145,6 +8205,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, downloadToken: downloadToken, encryptionKey: encryptionKey, encryptionMac: encryptionMac, diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 55a7ba0..664ea30 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -74,6 +74,7 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { if (mediaService.mediaFile.uploadState == UploadState.initialized || mediaService.mediaFile.uploadState == UploadState.preprocessing) { await mediaService.setUploadState(UploadState.preprocessing); + if (!mediaService.tempPath.existsSync()) { await mediaService.compressMedia(); } @@ -87,6 +88,8 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { } if (mediaService.uploadRequestPath.existsSync()) { await mediaService.setUploadState(UploadState.uploading); + // at this point the original file is not used any more, so it can be deleted + mediaService.originalPath.deleteSync(); } } diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/server_messages/contact.server_messages.dart index c49ac82..8e5aaa3 100644 --- a/lib/src/services/api/server_messages/contact.server_messages.dart +++ b/lib/src/services/api/server_messages/contact.server_messages.dart @@ -20,6 +20,23 @@ Future handleContactRequest( switch (contactRequest.type) { case EncryptedContent_ContactRequest_Type.REQUEST: Log.info('Got a contact request from $fromUserId'); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.accepted) { + // contact was already accepted, so just accept the request in the background. + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.ACCEPT, + ), + ), + ); + return; + } + } // Request the username by the server so an attacker can not // forge the displayed username in the contact request final username = await apiService.getUsername(fromUserId); diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index 8be5e6f..b09f33e 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -1,8 +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: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, @@ -10,6 +16,8 @@ Future compressImage( ) async { final stopwatch = Stopwatch()..start(); + // // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp + try { var compressedBytes = await FlutterImageCompress.compressWithFile( sourceFile.path, @@ -53,42 +61,40 @@ Future compressImage( ); } -Future compressVideo( - File sourceFile, - File destinationFile, -) async { - final stopwatch = Stopwatch()..start(); - - MediaInfo? mediaInfo; - try { - mediaInfo = await VideoCompress.compressVideo( - sourceFile.path, - quality: VideoQuality.Res1280x720Quality, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 - ); - - Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); - - if (mediaInfo.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - sourceFile.path, - quality: VideoQuality.Res960x540Quality, - includeAudio: true, - ); - } - } catch (e) { - Log.error('during video compression: $e'); +Future compressAndOverlayVideo(MediaFileService media) async { + if (media.tempPath.existsSync()) { + media.tempPath.deleteSync(); } - stopwatch.stop(); - Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); - if (mediaInfo == null) { - Log.error('Could not compress video using original video.'); - // as a fall back use the non compressed version - sourceFile.copySync(destinationFile.path); + final stopwatch = Stopwatch()..start(); + var command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.tempPath.path}"'; + + if (media.removeAudio) { + command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.tempPath.path}"'; + } + + final session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video', + ); } else { - await mediaInfo.file!.copy(destinationFile.path); + Log.info(command); + 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 + await twonlyDB.messagesDao.updateMessagesByMediaId( + media.mediaFile.mediaId, + const MessagesCompanion(isDeletedFromSender: Value(true)), + ); + media.fullMediaRemoval(); + await media.setUploadState(UploadState.uploaded); } } diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index e2852d6..0b11ae4 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -107,6 +107,22 @@ class MediaFileService { await updateFromDB(); } + bool get removeAudio => mediaFile.removeAudio ?? false; + + Future toggleRemoveAudio() async { + // var removeAudio = false; + // if (mediaFile.removeAudio != null) { + // removeAudio = !mediaFile.removeAudio!; + // } + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + removeAudio: Value(!removeAudio), + ), + ); + await updateFromDB(); + } + Future setUploadState(UploadState uploadState) async { await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, @@ -160,11 +176,12 @@ class MediaFileService { Log.error('Could not compress as original media does not exists.'); return; } + switch (mediaFile.type) { case MediaType.image: await compressImage(originalPath, tempPath); case MediaType.video: - await compressVideo(originalPath, tempPath); + await compressAndOverlayVideo(this); case MediaType.gif: originalPath.renameSync(tempPath.path); Log.error('Compression for .gif is not implemented yet.'); @@ -266,7 +283,7 @@ class MediaFileService { File get thumbnailPath => _buildFilePath( 'stored', namePrefix: '.thumbnail', - extensionParam: 'png', + extensionParam: 'webp', ); File get encryptedPath => _buildFilePath( 'tmp', @@ -280,4 +297,9 @@ class MediaFileService { 'tmp', namePrefix: '.original', ); + File get overlayImagePath => _buildFilePath( + 'tmp', + namePrefix: '.overlay', + extensionParam: 'png', + ); } diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index e73c8e4..cc3437e 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -1,25 +1,29 @@ import 'dart:io'; +import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new/return_code.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:video_thumbnail/video_thumbnail.dart'; Future createThumbnailsForVideo( File sourceFile, File destinationFile, ) async { - final fileExtension = sourceFile.path.split('.').last.toLowerCase(); - if (fileExtension != 'mp4') { - Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); - return; - } + final stopwatch = Stopwatch()..start(); - try { - await VideoThumbnail.thumbnailFile( - video: sourceFile.path, - thumbnailPath: destinationFile.path, - maxWidth: 450, - quality: 75, + final command = + '-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 session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', ); - } catch (e) { - Log.error('Could not create the video thumbnail: $e'); + } else { + Log.info(command); + Log.error('Compression failed for the video with exit code $returnCode.'); + Log.error(await session.getAllLogsAsString()); + // Report this error to the user? } } diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index 499a335..3d4f944 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -23,8 +23,7 @@ class CameraZoomButtons extends StatefulWidget { final double scaleFactor; final Function updateScaleFactor; final SelectedCameraDetails selectedCameraDetails; - final Future Function(int sCameraId, bool init, bool enableAudio) - selectCamera; + final Future Function(int sCameraId, bool init) selectCamera; @override State createState() => _CameraZoomButtonsState(); @@ -106,7 +105,7 @@ class _CameraZoomButtonsState extends State { ), onPressed: () async { if (showWideAngleZoomIOS) { - await widget.selectCamera(2, true, false); + await widget.selectCamera(2, true); } else { final level = await widget.controller.getMinZoomLevel(); widget.updateScaleFactor(level); @@ -130,7 +129,7 @@ class _CameraZoomButtonsState extends State { onPressed: () async { if (showWideAngleZoomIOS && widget.selectedCameraDetails.cameraId == 2) { - await widget.selectCamera(0, true, false); + await widget.selectCamera(0, true); } else { widget.updateScaleFactor(1.0); } diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index edea3bc..4ab20d1 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -23,13 +23,12 @@ import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; -int maxVideoRecordingTime = 15; +int maxVideoRecordingTime = 60; Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( SelectedCameraDetails details, int sCameraId, bool init, - bool enableAudio, ) async { var cameraId = sCameraId; if (cameraId >= gCameras.length) return null; @@ -49,7 +48,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( final cameraController = CameraController( gCameras[cameraId], ResolutionPreset.high, - enableAudio: enableAudio, + enableAudio: await Permission.microphone.isGranted, ); await cameraController.initialize().then((_) async { @@ -93,11 +92,8 @@ class CameraPreviewControllerView extends StatelessWidget { this.sendToGroup, }); final Group? sendToGroup; - final Future Function( - int sCameraId, - bool init, - bool enableAudio, - ) selectCamera; + final Future Function(int sCameraId, bool init) + selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; @@ -119,8 +115,7 @@ class CameraPreviewControllerView extends StatelessWidget { } else { return PermissionHandlerView( onSuccess: () { - // setState(() {}); - selectCamera(0, true, false); + selectCamera(0, true); }, ); } @@ -145,7 +140,6 @@ class CameraPreviewView extends StatefulWidget { final Future Function( int sCameraId, bool init, - bool enableAudio, ) selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; @@ -156,19 +150,17 @@ class CameraPreviewView extends StatefulWidget { } class _CameraPreviewViewState extends State { - bool sharePreviewIsShown = false; - bool galleryLoadedImageIsShown = false; - bool showSelfieFlash = false; - double basePanY = 0; - double baseScaleFactor = 0; - bool cameraLoaded = false; - bool isVideoRecording = false; - bool hasAudioPermission = true; - bool videoWithAudio = true; - DateTime? videoRecordingStarted; - Timer? videoRecordingTimer; + bool _sharePreviewIsShown = false; + bool _galleryLoadedImageIsShown = false; + bool _showSelfieFlash = false; + double _basePanY = 0; + double _baseScaleFactor = 0; + bool _isVideoRecording = false; + bool _hasAudioPermission = true; + DateTime? _videoRecordingStarted; + Timer? _videoRecordingTimer; - DateTime currentTime = DateTime.now(); + DateTime _currentTime = DateTime.now(); final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); @@ -179,9 +171,9 @@ class _CameraPreviewViewState extends State { } Future initAsync() async { - hasAudioPermission = await Permission.microphone.isGranted; + _hasAudioPermission = await Permission.microphone.isGranted; - if (!hasAudioPermission && !gUser.requestedAudioPermission) { + if (!_hasAudioPermission && !gUser.requestedAudioPermission) { await updateUserdata((u) { u.requestedAudioPermission = true; return u; @@ -194,7 +186,7 @@ class _CameraPreviewViewState extends State { @override void dispose() { - videoRecordingTimer?.cancel(); + _videoRecordingTimer?.cancel(); super.dispose(); } @@ -205,7 +197,7 @@ class _CameraPreviewViewState extends State { if (statuses[Permission.microphone]!.isPermanentlyDenied) { await openAppSettings(); } else { - hasAudioPermission = await Permission.microphone.isGranted; + _hasAudioPermission = await Permission.microphone.isGranted; setState(() {}); } } @@ -248,16 +240,16 @@ class _CameraPreviewViewState extends State { } Future takePicture() async { - if (sharePreviewIsShown || isVideoRecording) return; + if (_sharePreviewIsShown || _isVideoRecording) return; late Future imageBytes; setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); if (widget.selectedCameraDetails.isFlashOn) { if (isFront) { setState(() { - showSelfieFlash = true; + _showSelfieFlash = true; }); } else { await widget.cameraController?.setFlashMode(FlashMode.torch); @@ -285,7 +277,7 @@ class _CameraPreviewViewState extends State { return; } setState(() { - sharePreviewIsShown = false; + _sharePreviewIsShown = false; }); } @@ -311,7 +303,7 @@ class _CameraPreviewViewState extends State { ..deleteSync(); // Start with compressing the video, to speed up the process in case the video is not changed. - unawaited(mediaFileService.compressMedia()); + // unawaited(mediaFileService.compressMedia()); } final shouldReturn = await Navigator.push( @@ -333,8 +325,8 @@ class _CameraPreviewViewState extends State { ) as bool?; if (mounted) { setState(() { - sharePreviewIsShown = false; - showSelfieFlash = false; + _sharePreviewIsShown = false; + _showSelfieFlash = false; }); } if (!mounted) return true; @@ -350,7 +342,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( widget.selectedCameraDetails.cameraId, false, - false, ); return false; } @@ -368,9 +359,9 @@ class _CameraPreviewViewState extends State { return; } - widget.selectedCameraDetails.scaleFactor = (baseScaleFactor + + widget.selectedCameraDetails.scaleFactor = (_baseScaleFactor + // ignore: avoid_dynamic_calls - (basePanY - (details.localPosition.dy as double)) / 30) + (_basePanY - (details.localPosition.dy as double)) / 30) .clamp(1, widget.selectedCameraDetails.maxAvailableZoom); await widget.cameraController! @@ -382,8 +373,8 @@ class _CameraPreviewViewState extends State { Future pickImageFromGallery() async { setState(() { - galleryLoadedImageIsShown = true; - sharePreviewIsShown = true; + _galleryLoadedImageIsShown = true; + _sharePreviewIsShown = true; }); final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); @@ -397,8 +388,8 @@ class _CameraPreviewViewState extends State { ); } setState(() { - galleryLoadedImageIsShown = false; - sharePreviewIsShown = false; + _galleryLoadedImageIsShown = false; + _sharePreviewIsShown = false; }); } @@ -407,41 +398,32 @@ class _CameraPreviewViewState extends State { widget.cameraController!.value.isRecordingVideo) { return; } - var cameraController = widget.cameraController; - if (hasAudioPermission && videoWithAudio) { - cameraController = await widget.selectCamera( - widget.selectedCameraDetails.cameraId, - false, - await Permission.microphone.isGranted && videoWithAudio, - ); - } - setState(() { - isVideoRecording = true; + _isVideoRecording = true; }); try { - await cameraController?.startVideoRecording(); - videoRecordingTimer = + await widget.cameraController?.startVideoRecording(); + _videoRecordingTimer = Timer.periodic(const Duration(milliseconds: 15), (timer) { setState(() { - currentTime = DateTime.now(); + _currentTime = DateTime.now(); }); - if (videoRecordingStarted != null && - currentTime.difference(videoRecordingStarted!).inSeconds >= + if (_videoRecordingStarted != null && + _currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) { timer.cancel(); - videoRecordingTimer = null; + _videoRecordingTimer = null; stopVideoRecording(); } }); setState(() { - videoRecordingStarted = DateTime.now(); - isVideoRecording = true; + _videoRecordingStarted = DateTime.now(); + _isVideoRecording = true; }); } on CameraException catch (e) { setState(() { - isVideoRecording = false; + _isVideoRecording = false; }); _showCameraException(e); return; @@ -449,14 +431,14 @@ class _CameraPreviewViewState extends State { } Future stopVideoRecording() async { - if (videoRecordingTimer != null) { - videoRecordingTimer?.cancel(); - videoRecordingTimer = null; + if (_videoRecordingTimer != null) { + _videoRecordingTimer?.cancel(); + _videoRecordingTimer = null; } setState(() { - videoRecordingStarted = null; - isVideoRecording = false; + _videoRecordingStarted = null; + _isVideoRecording = false; }); if (widget.cameraController == null || @@ -465,7 +447,7 @@ class _CameraPreviewViewState extends State { } setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); try { @@ -509,15 +491,15 @@ class _CameraPreviewViewState extends State { return; } setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); }, onLongPressMoveUpdate: onPanUpdate, onLongPressStart: (details) { setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); // Get the position of the pointer final renderBox = @@ -540,7 +522,7 @@ class _CameraPreviewViewState extends State { onPanUpdate: onPanUpdate, child: Stack( children: [ - if (galleryLoadedImageIsShown) + if (_galleryLoadedImageIsShown) Center( child: SizedBox( width: 20, @@ -551,11 +533,11 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!sharePreviewIsShown && + if (!_sharePreviewIsShown && widget.sendToGroup != null && - !isVideoRecording) + !_isVideoRecording) SendToWidget(sendTo: widget.sendToGroup!.groupName), - if (!sharePreviewIsShown && !isVideoRecording) + if (!_sharePreviewIsShown && !_isVideoRecording) Positioned( right: 5, top: 0, @@ -573,7 +555,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( (widget.selectedCameraDetails.cameraId + 1) % 2, false, - false, ); }, ), @@ -598,7 +579,7 @@ class _CameraPreviewViewState extends State { setState(() {}); }, ), - if (!hasAudioPermission) + if (!_hasAudioPermission) ActionButton( Icons.mic_off_rounded, color: Colors.white.withAlpha(160), @@ -606,27 +587,12 @@ class _CameraPreviewViewState extends State { 'Allow microphone access for video recording.', onPressed: requestMicrophonePermission, ), - if (hasAudioPermission) - ActionButton( - videoWithAudio - ? Icons.volume_up_rounded - : Icons.volume_off_rounded, - tooltipText: 'Record video with audio.', - color: videoWithAudio - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - setState(() { - videoWithAudio = !videoWithAudio; - }); - }, - ), ], ), ), ), ), - if (!sharePreviewIsShown) + if (!_sharePreviewIsShown) Positioned( bottom: 30, left: 0, @@ -638,7 +604,7 @@ class _CameraPreviewViewState extends State { if (widget.cameraController!.value.isInitialized && widget.selectedCameraDetails.isZoomAble && !isFront && - !isVideoRecording) + !_isVideoRecording) SizedBox( width: 120, child: CameraZoomButtons( @@ -655,7 +621,7 @@ class _CameraPreviewViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!isVideoRecording) + if (!_isVideoRecording) GestureDetector( onTap: pickImageFromGallery, child: Align( @@ -687,7 +653,7 @@ class _CameraPreviewViewState extends State { shape: BoxShape.circle, border: Border.all( width: 7, - color: isVideoRecording + color: _isVideoRecording ? Colors.red : Colors.white, ), @@ -695,7 +661,7 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!isVideoRecording) const SizedBox(width: 80), + if (!_isVideoRecording) const SizedBox(width: 80), ], ), ], @@ -703,10 +669,10 @@ class _CameraPreviewViewState extends State { ), ), VideoRecordingTimer( - videoRecordingStarted: videoRecordingStarted, + videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!sharePreviewIsShown && widget.sendToGroup != null) + if (!_sharePreviewIsShown && widget.sendToGroup != null) Positioned( left: 5, top: 10, @@ -718,7 +684,7 @@ class _CameraPreviewViewState extends State { }, ), ), - if (showSelfieFlash) + if (_showSelfieFlash) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(22), diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index 8586b4d..cb36407 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -22,7 +22,7 @@ class CameraSendToViewState extends State { @override void initState() { super.initState(); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); } @override @@ -36,13 +36,11 @@ class CameraSendToViewState extends State { Future selectCamera( int sCameraId, bool init, - bool enableAudio, ) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -61,7 +59,7 @@ class CameraSendToViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } @override diff --git a/lib/src/views/camera/image_editor/data/layer.dart b/lib/src/views/camera/image_editor/data/layer.dart index 707e348..7f6bdf8 100755 --- a/lib/src/views/camera/image_editor/data/layer.dart +++ b/lib/src/views/camera/image_editor/data/layer.dart @@ -34,7 +34,9 @@ class BackgroundLayerData extends Layer { ImageItem image; } -class FilterLayerData extends Layer {} +class FilterLayerData extends Layer { + int page = 1; +} /// Attributes used by [EmojiLayer] class EmojiLayerData extends Layer { diff --git a/lib/src/views/camera/image_editor/layers/filter_layer.dart b/lib/src/views/camera/image_editor/layers/filter_layer.dart index c0179d0..13e76a2 100644 --- a/lib/src/views/camera/image_editor/layers/filter_layer.dart +++ b/lib/src/views/camera/image_editor/layers/filter_layer.dart @@ -116,6 +116,7 @@ class _FilterLayerState extends State { } }, onPageChanged: (index) { + widget.layerData.page = index; if (index == 0) { // If the user swipes to the first duplicated page, jump to the last page pageController.jumpToPage(pages.length); diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart index 491abf9..7271518 100644 --- a/lib/src/views/camera/share_image_components/best_friends_selector.dart +++ b/lib/src/views/camera/share_image_components/best_friends_selector.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; @@ -148,9 +149,7 @@ class UserCheckbox extends StatelessWidget { Row( children: [ Text( - group.groupName.length > 12 - ? '${group.groupName.substring(0, 9)}...' - : group.groupName, + substringBy(group.groupName, 12), overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d84708d..32ff434 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -211,6 +211,25 @@ class _ShareImageEditorView extends State { }, ), ), + if (media.type == MediaType.video) + ActionButton( + (mediaService.removeAudio) + ? Icons.volume_off_rounded + : Icons.volume_up_rounded, + tooltipText: 'Enable Audio in Video', + color: (mediaService.removeAudio) + ? Colors.white.withAlpha(160) + : Colors.white, + onPressed: () async { + await mediaService.toggleRemoveAudio(); + if (mediaService.removeAudio) { + await videoController?.setVolume(0); + } else { + await videoController?.setVolume(100); + } + if (mounted) setState(() {}); + }, + ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.shieldHeart, @@ -281,8 +300,7 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - final mediaStoreFuture = - (media.type == MediaType.image) ? storeImageAsOriginal() : null; + final mediaStoreFuture = storeImageAsOriginal(); await videoController?.pause(); if (isDisposed || !mounted) return; @@ -312,33 +330,39 @@ class _ShareImageEditorView extends State { } } - if (layers.length > 1 || media.type == MediaType.video) { - for (final x in layers) { - x.showCustomButtons = false; - } - setState(() {}); - final image = await screenshotController.capture( - pixelRatio: pixelRatio, - ); - if (image == null) { - Log.error('screenshotController did not return image bytes'); - return null; - } - - for (final x in layers) { - x.showCustomButtons = true; - } - setState(() {}); - return image; + for (final x in layers) { + x.showCustomButtons = false; + } + setState(() {}); + final image = await screenshotController.capture( + pixelRatio: pixelRatio, + ); + if (image == null) { + Log.error('screenshotController did not return image bytes'); + return null; } - return null; + for (final x in layers) { + x.showCustomButtons = true; + } + setState(() {}); + return image; } Future storeImageAsOriginal() async { + if (mediaService.overlayImagePath.existsSync()) { + mediaService.overlayImagePath.deleteSync(); + } + if (mediaService.tempPath.existsSync()) { + mediaService.tempPath.deleteSync(); + } final imageBytes = await getEditedImageBytes(); if (imageBytes == null) return false; - mediaService.originalPath.writeAsBytesSync(imageBytes); + if (media.type == MediaType.image) { + mediaService.originalPath.writeAsBytesSync(imageBytes); + } else { + mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + } // In case the image was already stored, then rename the stored image. if (mediaService.storedPath.existsSync()) { @@ -373,12 +397,7 @@ class _ShareImageEditorView extends State { sendingOrLoadingImage = true; }); - if (media.type == MediaType.image) { - await storeImageAsOriginal(); - } - if (media.type == MediaType.video) { - Log.error('TODO: COMBINE VIDEO AND IMAGE!!!'); - } + await storeImageAsOriginal(); if (!context.mounted) return; diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 1db8c71..7acf01e 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; @@ -64,7 +65,7 @@ class _ShareImageView extends State { await widget.mediaStoreFuture; } mediaStoreFutureReady = true; - unawaited(startBackgroundMediaUpload(widget.mediaFileService)); + // unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); } @@ -323,7 +324,7 @@ class UserList extends StatelessWidget { return ListTile( title: Row( children: [ - Text(group.groupName), + Text(substringBy(group.groupName, 12)), FlameCounterWidget( groupId: group.groupId, prefix: true, diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 121fa94..a56dcde 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -25,6 +25,7 @@ class ChatTextEntry extends StatelessWidget { @override Widget build(BuildContext context) { var text = message.content ?? ''; + var textColor = Colors.white; if (EmojiAnimation.supported(text)) { return Container( @@ -59,7 +60,10 @@ class ChatTextEntry extends StatelessWidget { if (message.isDeletedFromSender) { text = context.lang.messageWasDeleted; - color = Colors.grey; + color = isDarkMode(context) ? Colors.black : Colors.grey; + if (isDarkMode(context)) { + textColor = const Color.fromARGB(255, 99, 99, 99); + } } return Container( @@ -78,10 +82,10 @@ class ChatTextEntry extends StatelessWidget { children: [ if (expanded) Expanded( - child: BetterText(text: text), + child: BetterText(text: text, textColor: textColor), ) else ...[ - BetterText(text: text), + BetterText(text: text, textColor: textColor), SizedBox( width: spacerWidth, ), diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index 97399a2..7b4608f 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -5,8 +5,9 @@ import 'package:twonly/src/utils/log.dart'; import 'package:url_launcher/url_launcher.dart'; class BetterText extends StatelessWidget { - const BetterText({required this.text, super.key}); + const BetterText({required this.text, required this.textColor, super.key}); final String text; + final Color textColor; @override Widget build(BuildContext context) { @@ -65,8 +66,8 @@ class BetterText extends StatelessWidget { softWrap: true, textAlign: TextAlign.start, overflow: TextOverflow.visible, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: textColor, fontSize: 17, decoration: TextDecoration.none, fontWeight: FontWeight.normal, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index d7e3bd8..7571421 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -70,7 +70,7 @@ class HomeViewState extends State { } if (cameraController == null && !initCameraStarted && offsetRatio < 1) { initCameraStarted = true; - unawaited(selectCamera(selectedCameraDetails.cameraId, false, false)); + unawaited(selectCamera(selectedCameraDetails.cameraId, false)); } if (offsetRatio == 1) { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { @@ -97,7 +97,7 @@ class HomeViewState extends State { .listen((NotificationResponse? response) async { globalUpdateOfHomeViewPageIndex(0); }); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); unawaited(initAsync()); } @@ -109,16 +109,11 @@ class HomeViewState extends State { super.dispose(); } - Future selectCamera( - int sCameraId, - bool init, - bool enableAudio, - ) async { + Future selectCamera(int sCameraId, bool init) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -138,7 +133,7 @@ class HomeViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } Future initAsync() async { diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index 15821cb..b04a407 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -357,7 +357,7 @@ class _DatabaseMigrationViewState extends State { children: [ const SizedBox(height: 40), const Text( - 'twonly. Jetzt besser als je zuvor.', + 'twonly. Besser als je zuvor.', textAlign: TextAlign.center, style: TextStyle( fontSize: 35, diff --git a/pubspec.lock b/pubspec.lock index a1584b0..7e04a56 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "8.4.1" archive: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: camera - sha256: "87a27e0553e3432119c1c2f6e4b9a1bbf7d2c660552b910bfa59185a9facd632" + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab url: "https://pub.dev" source: hosted - version: "0.11.2+1" + version: "0.11.3" camera_android_camerax: dependency: "direct overridden" description: @@ -402,6 +402,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + 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: @@ -850,10 +866,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 url: "https://pub.dev" source: hosted - version: "0.8.13+5" + version: "0.8.13+7" image_picker_for_web: dependency: transitive description: @@ -1771,14 +1787,6 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.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 f9a646d..bdde2b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: device_info_plus: ^12.1.0 drift: ^2.25.1 drift_flutter: ^0.2.4 + ffmpeg_kit_flutter_new: ^4.1.0 firebase_core: ^4.2.0 firebase_messaging: ^16.0.3 fixnum: ^1.1.1 @@ -67,7 +68,6 @@ dependencies: share_plus: ^12.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1 - video_compress: ^3.1.4 video_player: ^2.9.5 video_thumbnail: ^0.5.6 web_socket_channel: ^3.0.1 From 0bacbe6671cf6fc485df592b6740bafaca687727 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Oct 2025 23:32:38 +0100 Subject: [PATCH 32/76] removed dep video_thumbnail --- CHANGELOG.md | 6 +++--- ios/Podfile.lock | 21 +++++++-------------- ios/Runner.xcodeproj/project.pbxproj | 4 ++-- pubspec.lock | 8 -------- pubspec.yaml | 1 - 5 files changed, 12 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fdcb3b..51a6183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ - Support for groups - Edit & Delete messages - Switched to FFmpeg for improved video compression - - Video max. length increased to 60 seconds - - Removing audio after recording is possible - - Edited image is now embedded into the video +- Video max. length increased to 60 seconds +- Removing audio after recording is possible +- Edited image is now embedded into the video - New context menu and other UI enhancements - Client-to-client protocol migrated to Protocol Buffers (Protobuf) - Database identifiers converted to UUIDs diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 00f096a..440ffc5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,10 +9,10 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter - - ffmpeg_kit_flutter_new_min_gpl (7.1.1): - - ffmpeg_kit_flutter_new_min_gpl/min-gpl (= 7.1.1) + - ffmpeg_kit_flutter_new (1.0.0): + - ffmpeg_kit_flutter_new/full-gpl (= 1.0.0) - Flutter - - ffmpeg_kit_flutter_new_min_gpl/min-gpl (7.1.1): + - ffmpeg_kit_flutter_new/full-gpl (1.0.0): - Flutter - Firebase (12.4.0): - Firebase/Core (= 12.4.0) @@ -241,9 +241,6 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS - - video_thumbnail (0.0.1): - - Flutter - - libwebp DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) @@ -251,7 +248,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - - ffmpeg_kit_flutter_new_min_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios`) + - ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) @@ -280,7 +277,6 @@ DEPENDENCIES: - SwiftProtobuf - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) SPEC REPOS: trunk: @@ -315,8 +311,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cryptography_flutter_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" - ffmpeg_kit_flutter_new_min_gpl: - :path: ".symlinks/plugins/ffmpeg_kit_flutter_new_min_gpl/ios" + ffmpeg_kit_flutter_new: + :path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -361,8 +357,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" - video_thumbnail: - :path: ".symlinks/plugins/video_thumbnail/ios" SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad @@ -370,7 +364,7 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe - ffmpeg_kit_flutter_new_min_gpl: 79212bc20869b4e12ec06705724c26b016e9d58e + ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 @@ -412,7 +406,6 @@ SPEC CHECKSUMS: SwiftProtobuf: 81e341191afbddd64aa031bd12862dccfab2f639 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a - video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 PODFILE CHECKSUM: 47470fbd5b59affa461eaf943ac57acce81e0ee8 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3b02435..2997989 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -263,8 +263,8 @@ D21FCEAC2D9F2B750088701D /* Embed Foundation Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - A3027D78D4FF6E79C9EFD470 /* [CP] Copy Pods Resources */, 32D7521D6B8F508A844DBC22 /* [CP] Embed Pods Frameworks */, + A7154597C13DDED7C7F2355C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -485,7 +485,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A3027D78D4FF6E79C9EFD470 /* [CP] Copy Pods Resources */ = { + A7154597C13DDED7C7F2355C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( diff --git a/pubspec.lock b/pubspec.lock index 7e04a56..eb37e53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1827,14 +1827,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" - video_thumbnail: - dependency: "direct main" - description: - name: video_thumbnail - sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" - url: "https://pub.dev" - source: hosted - version: "0.5.6" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bdde2b1..2984317 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,7 +69,6 @@ dependencies: tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1 video_player: ^2.9.5 - video_thumbnail: ^0.5.6 web_socket_channel: ^3.0.1 dependency_overrides: From 37790aa3040bc13699a506b774ee7b8b8506f636 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Oct 2025 00:19:16 +0100 Subject: [PATCH 33/76] fixing some null pointers --- lib/src/views/chats/media_viewer.view.dart | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index e1ad7dc..c290f5f 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -189,6 +189,7 @@ class _MediaViewerViewState extends State { Future handleNextDownloadedMedia( bool showTwonly, ) async { + if (allMediaFiles.isEmpty) return; currentMessage = allMediaFiles.removeAt(0); final currentMediaLocal = await MediaFileService.fromMediaId(currentMessage!.mediaId!); @@ -224,7 +225,8 @@ class _MediaViewerViewState extends State { currentMediaLocal.mediaFile.displayLimitInMilliseconds == null, ); await videoController?.initialize().then((_) { - videoController!.play(); + if (videoController == null) return; + videoController?.play(); videoController?.addListener(() { setState(() { progress = 1 - @@ -259,20 +261,20 @@ class _MediaViewerViewState extends State { void startTimer() { nextMediaTimer?.cancel(); progressTimer?.cancel(); - nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { - if (context.mounted) { - nextMediaOrExit(); - } - }); - progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { - if (canBeSeenUntil != null) { + if (canBeSeenUntil != null) { + nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { + if (context.mounted) { + nextMediaOrExit(); + } + }); + progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { final difference = canBeSeenUntil!.difference(DateTime.now()); // Calculate the progress as a value between 0.0 and 1.0 progress = difference.inMilliseconds / (currentMedia!.mediaFile.displayLimitInMilliseconds!); setState(() {}); - } - }); + }); + } } Future onPressedSaveToGallery() async { @@ -464,7 +466,8 @@ class _MediaViewerViewState extends State { Positioned.fill( child: VideoPlayer(videoController!), ), - if (currentMedia!.mediaFile.type == MediaType.image) + if (currentMedia != null && + currentMedia!.mediaFile.type == MediaType.image) Positioned.fill( child: Image.file( currentMedia!.tempPath, From f2bd80c2dc7d58601e883094d15afdfcfc726886 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Oct 2025 09:32:07 +0100 Subject: [PATCH 34/76] multiple reactions possible and quoted message size improved --- lib/src/database/daos/reactions.dao.dart | 84 ++++++++++------- .../client/generated/messages.pbjson.dart | 89 +++++++++---------- lib/src/model/protobuf/client/messages.proto | 4 +- .../reaction.server_message.dart | 25 +++--- .../all_reactions.bottom_sheet.dart | 46 ++++++++-- .../chat_list_entry.dart | 1 + .../chat_media_entry.dart | 4 +- .../chat_reaction_row.dart | 5 +- .../chat_text_entry.dart | 3 +- .../message_context_menu.dart | 7 +- .../emoji_reactions_row.component.dart | 3 +- 11 files changed, 161 insertions(+), 110 deletions(-) diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart index 997d3f8..59ef34a 100644 --- a/lib/src/database/daos/reactions.dao.dart +++ b/lib/src/database/daos/reactions.dao.dart @@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; part 'reactions.dao.g.dart'; @@ -18,21 +19,29 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { int contactId, String messageId, String groupId, - String? emoji, + String emoji, + bool remove, ) async { + if (!isEmoji(emoji)) { + Log.error('Did not update reaction as it is not an emoji!'); + return; + } final msg = await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); if (msg == null || msg.groupId != groupId) return; try { - await (delete(reactions) - ..where( - (t) => - t.senderId.equals(contactId) & t.messageId.equals(messageId), - )) - .go(); - if (emoji != null) { - await into(reactions).insert( + if (remove) { + await (delete(reactions) + ..where( + (t) => + t.senderId.equals(contactId) & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) + .go(); + } else { + await into(reactions).insertOnConflictUpdate( ReactionsCompanion( messageId: Value(messageId), emoji: Value(emoji), @@ -45,6 +54,42 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { } } + Future updateMyReaction( + String messageId, + String emoji, + bool remove, + ) async { + if (!isEmoji(emoji)) { + Log.error('Did not update reaction as it is not an emoji!'); + return; + } + final msg = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (msg == null) return; + + try { + await (delete(reactions) + ..where( + (t) => + t.senderId.isNull() & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) + .go(); + if (!remove) { + await into(reactions).insert( + ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: const Value(null), + ), + ); + } + } catch (e) { + Log.error(e); + } + } + Stream> watchReactions(String messageId) { return (select(reactions) ..where((t) => t.messageId.equals(messageId)) @@ -81,25 +126,4 @@ class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { .map((row) => (row.readTable(reactions), row.readTableOrNull(contacts))) .watch(); } - - Future updateMyReaction(String messageId, String? emoji) async { - try { - await (delete(reactions) - ..where( - (t) => t.senderId.isNull() & t.messageId.equals(messageId), - )) - .go(); - if (emoji != null) { - await into(reactions).insert( - ReactionsCompanion( - messageId: Value(messageId), - emoji: Value(emoji), - senderId: const Value(null), - ), - ); - } - } catch (e) { - Log.error(e); - } - } } diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 43fb535..4300965 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -143,12 +143,8 @@ const EncryptedContent_Reaction$json = { '1': 'Reaction', '2': [ {'1': 'targetMessageId', '3': 1, '4': 1, '5': 9, '10': 'targetMessageId'}, - {'1': 'emoji', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'emoji', '17': true}, - {'1': 'remove', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'remove', '17': true}, - ], - '8': [ - {'1': '_emoji'}, - {'1': '_remove'}, + {'1': 'emoji', '3': 2, '4': 1, '5': 9, '10': 'emoji'}, + {'1': 'remove', '3': 3, '4': 1, '5': 8, '10': 'remove'}, ], }; @@ -333,45 +329,44 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z' 'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX' 'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ' - 'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA' - 'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI' - 'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3' - 'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu' - 'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3' - 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11' - 'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX' - 'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ' - 'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg' - '9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu' - 'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG' - 'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp' - 'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX' - 'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN' - 'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi' - 'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u' - 'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA' - 'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ' - 'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX' - 'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu' - 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR' - 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn' - 'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA' - 'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK' - 'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG' - 'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa8AEKDUNvbnRh' - 'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS' - '5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29t' - 'cHJlc3NlZIgBARIlCgtkaXNwbGF5TmFtZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeX' - 'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEIOCgxf' - 'ZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW' - '50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5' - 'GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBF' - 'R5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVh' - 'dGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlch' - 'I2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdl' - 'Eh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZW' - 'N0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21l' - 'ZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3' - 'RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2Fn' - 'ZQ=='); + 'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg' + '90YXJnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIF' + 'ZW1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZR' + 'gBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3Nl' + 'bmRlck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYX' + 'JnZXRNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgE' + 'IAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCg' + 'oGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJ' + 'ZEIHCgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZX' + 'NzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlw' + 'ZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk' + '1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJl' + 'c0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTW' + 'Vzc2FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByAB' + 'KAxIAlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cH' + 'Rpb25LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0K' + 'D2VuY3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCg' + 'hSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxp' + 'bWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQh' + 'AKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2Ua' + 'pwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVX' + 'BkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdl' + 'SWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1' + 'IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5D' + 'b250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVB' + 'ABEgoKBkFDQ0VQVBACGvABCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0' + 'ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2' + 'VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESJQoLZGlzcGxheU5hbWUYAyABKAlI' + 'AVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2' + 'F2YXRhclN2Z0NvbXByZXNzZWRCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBl' + 'GAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGA' + 'IgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQg' + 'ASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICg' + 'Zfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3Vu' + 'dGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1' + 'IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5k' + 'QgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQh' + 'AKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RV' + 'cGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV' + '9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2U='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 64f947e..9bdbd6f 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -54,8 +54,8 @@ message EncryptedContent { message Reaction { string targetMessageId = 1; - optional string emoji = 2; - optional bool remove = 3; + string emoji = 2; + bool remove = 3; } message MessageUpdate { diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/server_messages/reaction.server_message.dart index 1eaf4db..5db2fbd 100644 --- a/lib/src/services/api/server_messages/reaction.server_message.dart +++ b/lib/src/services/api/server_messages/reaction.server_message.dart @@ -7,21 +7,16 @@ Future handleReaction( String groupId, EncryptedContent_Reaction reaction, ) async { - Log.info('Got a reaction from $fromUserId'); - if (reaction.hasRemove()) { - if (reaction.remove) { - await twonlyDB.reactionsDao - .updateReaction(fromUserId, reaction.targetMessageId, groupId, null); - return; - } - } - if (reaction.hasEmoji()) { - await twonlyDB.reactionsDao.updateReaction( - fromUserId, - reaction.targetMessageId, - groupId, - reaction.emoji, - ); + Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})'); + await twonlyDB.reactionsDao.updateReaction( + fromUserId, + reaction.targetMessageId, + groupId, + reaction.emoji, + reaction.remove, + ); + + if (!reaction.remove) { await twonlyDB.groupsDao .increaseLastMessageExchange(groupId, DateTime.now()); } diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 7ec8b6d..9d25e2d 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; class AllReactionsView extends StatefulWidget { @@ -47,14 +48,18 @@ class _AllReactionsViewState extends State { setState(() {}); } - Future removeReaction() async { - await twonlyDB.reactionsDao - .updateMyReaction(widget.message.messageId, null); + Future removeReaction(String emoji) async { + await twonlyDB.reactionsDao.updateMyReaction( + widget.message.messageId, + emoji, + true, + ); await sendCipherTextToGroup( widget.message.groupId, pb.EncryptedContent( reaction: pb.EncryptedContent_Reaction( targetMessageId: widget.message.messageId, + emoji: emoji, remove: true, ), ), @@ -97,12 +102,17 @@ class _AllReactionsViewState extends State { child: ListView( children: reactionsUsers.map((entry) { return GestureDetector( - onTap: (entry.$2 != null) ? null : removeReaction, + onTap: (entry.$2 != null) + ? null + : () { + removeReaction(entry.$1.emoji); + }, child: Container( padding: const EdgeInsets.symmetric( vertical: 5, horizontal: 30, ), + color: Colors.transparent, margin: const EdgeInsets.only(left: 4), child: Row( children: [ @@ -130,10 +140,30 @@ class _AllReactionsViewState extends State { ], ), ), - Text( - entry.$1.emoji, - style: const TextStyle(fontSize: 25), - ), + if (EmojiAnimation.animatedIcons + .containsKey(entry.$1.emoji)) + SizedBox( + height: 25, + child: EmojiAnimation(emoji: entry.$1.emoji), + ) + else + SizedBox( + height: 24, + child: Center( + child: Text( + entry.$1.emoji, + style: const TextStyle(fontSize: 22), + strutStyle: const StrutStyle( + forceStrutHeight: true, + height: 1.6, + ), + ), + ), + ), + // Text( + // entry.$1.emoji, + // style: const TextStyle(fontSize: 25), + // ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 97d6933..4b8fd27 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -133,6 +133,7 @@ class _ChatListEntryState extends State { group: widget.group, mediaService: mediaService!, galleryItems: widget.galleryItems, + minWidth: reactionsForWidth * 43, ), ), if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 0337ba2..bb53503 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -22,10 +22,12 @@ class ChatMediaEntry extends StatefulWidget { required this.group, required this.galleryItems, required this.mediaService, + required this.minWidth, super.key, }); final Message message; + final double minWidth; final Group group; final List galleryItems; final MediaFileService mediaService; @@ -117,7 +119,7 @@ class _ChatMediaEntryState extends State { onDoubleTap: onDoubleTap, onTap: (widget.message.type == MessageType.media) ? onTap : null, child: SizedBox( - width: 150, + width: (widget.minWidth > 150) ? widget.minWidth : 150, height: (widget.message.mediaStored && widget.mediaService.imagePreviewAvailable) ? 271 diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index b0d6991..c6d4a27 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -113,9 +113,10 @@ class ReactionRow extends StatelessWidget { child: Text( entry.$2.toString(), textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 15, - color: Colors.black, + color: + isDarkMode(context) ? Colors.white : Colors.black, decoration: TextDecoration.none, fontWeight: FontWeight.normal, ), diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index a56dcde..16c7571 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -53,8 +53,7 @@ class ChatTextEntry extends StatelessWidget { if (message.isDeletedFromSender) { color = context.color.surfaceBright; displayTime = false; - } else if (measureTextWidth(text) > 270 || - message.quotesMessageId != null) { + } else if (measureTextWidth(text) > 270) { expanded = true; } diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 4d6a33f..8fbfb26 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -50,8 +50,11 @@ class MessageContextMenu extends StatelessWidget { ) as EmojiLayerData?; if (layer == null) return; - await twonlyDB.reactionsDao - .updateMyReaction(message.messageId, layer.text); + await twonlyDB.reactionsDao.updateMyReaction( + message.messageId, + layer.text, + false, + ); await sendCipherTextToGroup( message.groupId, diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart index 210b6f1..5ff1e3c 100644 --- a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -36,7 +36,7 @@ class _EmojiReactionWidgetState extends State { child: GestureDetector( onTap: () async { await twonlyDB.reactionsDao - .updateMyReaction(widget.messageId, widget.emoji); + .updateMyReaction(widget.messageId, widget.emoji, false); await sendCipherTextToGroup( widget.groupId, @@ -44,6 +44,7 @@ class _EmojiReactionWidgetState extends State { reaction: EncryptedContent_Reaction( targetMessageId: widget.messageId, emoji: widget.emoji, + remove: false, ), ), null, From cb1ad9bdc772bbcef1eb8c29d0d919d5dcd36cba Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Oct 2025 09:36:53 +0100 Subject: [PATCH 35/76] always display edit icon when modified --- .../chat_messages_components/chat_text_entry.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 16c7571..c9f7d8a 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -89,7 +89,7 @@ class ChatTextEntry extends StatelessWidget { width: spacerWidth, ), ], - if (displayTime) + if (displayTime || message.modifiedAt != null) Align( alignment: AlignmentGeometry.centerRight, child: Padding( @@ -108,8 +108,14 @@ class ChatTextEntry extends StatelessWidget { ), ), ), + // if (displayTime) Text( - friendlyTime(context, message.createdAt), + friendlyTime( + context, + (message.modifiedAt != null) + ? message.modifiedAt! + : message.createdAt, + ), style: TextStyle( fontSize: 10, color: Colors.white.withAlpha(150), From 404aee1e1816604b1b65ab982a9a7b032746bd7e Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Oct 2025 10:14:25 +0100 Subject: [PATCH 36/76] upload non finished uploads after restart --- lib/main.dart | 2 ++ lib/src/database/daos/mediafiles.dao.dart | 10 +++++++ .../api/mediafiles/upload.service.dart | 26 +++++++++++++++++++ .../mediafiles/thumbnail.service.dart | 7 ++--- .../chat_text_entry.dart | 1 - .../memories/memories_item_thumbnail.dart | 13 +++++++++- 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ec86621..9df9558 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -55,6 +56,7 @@ void main() async { twonlyDB = TwonlyDB(); await initFileDownloader(); + unawaited(finishStartedPreprocessing()); unawaited(MediaFileService.purgeTempFolder()); await twonlyDB.messagesDao.purgeMessageTable(); diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index cc01ca9..b5da4a1 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -84,6 +84,16 @@ class MediaFilesDao extends DatabaseAccessor .get(); } + Future> getAllMediaFilesPendingUpload() async { + return (select(mediaFiles) + ..where( + (t) => + t.uploadState.equals(UploadState.initialized.name) | + t.uploadState.equals(UploadState.preprocessing.name), + )) + .get(); + } + Stream> watchAllStoredMediaFiles() { return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 664ea30..1db4ef1 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -19,6 +19,20 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +Future finishStartedPreprocessing() async { + final mediaFiles = + await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); + + for (final mediaFile in mediaFiles) { + try { + final service = await MediaFileService.fromMedia(mediaFile); + await startBackgroundMediaUpload(service); + } catch (e) { + Log.error(e); + } + } +} + Future initializeMediaUpload( MediaType type, int? displayLimitInMilliseconds, @@ -77,15 +91,22 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { if (!mediaService.tempPath.existsSync()) { await mediaService.compressMedia(); + if (!mediaService.tempPath.existsSync()) { + return; + } } if (!mediaService.encryptedPath.existsSync()) { await _encryptMediaFiles(mediaService); + if (!mediaService.encryptedPath.existsSync()) { + return; + } } if (!mediaService.uploadRequestPath.existsSync()) { await _createUploadRequest(mediaService); } + if (mediaService.uploadRequestPath.existsSync()) { await mediaService.setUploadState(UploadState.uploading); // at this point the original file is not used any more, so it can be deleted @@ -101,6 +122,11 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { Future _encryptMediaFiles(MediaFileService mediaService) async { /// if there is a video wait until it is finished with compression + if (!mediaService.tempPath.existsSync()) { + Log.error('Could not encrypted image as it does not exists'); + return; + } + final dataToEncrypt = await mediaService.tempPath.readAsBytes(); final chacha20 = FlutterChacha20.poly1305Aead(); diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index cc3437e..df7397c 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -10,7 +10,7 @@ Future createThumbnailsForVideo( final stopwatch = Stopwatch()..start(); final command = - '-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}'; + '-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 session = await FFmpegKit.execute(command); final returnCode = await session.getReturnCode(); @@ -22,8 +22,9 @@ Future createThumbnailsForVideo( ); } else { Log.info(command); - Log.error('Compression failed for the video with exit code $returnCode.'); + Log.error( + 'Thumbnail creation failed for the video with exit code $returnCode.', + ); Log.error(await session.getAllLogsAsString()); - // Report this error to the user? } } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index c9f7d8a..ee5f4b7 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -108,7 +108,6 @@ class ChatTextEntry extends StatelessWidget { ), ), ), - // if (displayTime) Text( friendlyTime( context, diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index d4434d4..a0ec884 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -20,9 +20,19 @@ class MemoriesItemThumbnail extends StatefulWidget { class _MemoriesItemThumbnailState extends State { @override void initState() { + initAsync(); super.initState(); } + Future initAsync() async { + if (!widget.galleryItem.mediaService.thumbnailPath.existsSync()) { + if (widget.galleryItem.mediaService.storedPath.existsSync()) { + await widget.galleryItem.mediaService.createThumbnail(); + if (mounted) setState(() {}); + } + } + } + @override void dispose() { super.dispose(); @@ -46,7 +56,8 @@ class _MemoriesItemThumbnailState extends State { children: [ if (media.thumbnailPath.existsSync()) Image.file(media.thumbnailPath) - else if (media.storedPath.existsSync()) + else if (media.storedPath.existsSync() && + media.mediaFile.type == MediaType.image) Image.file(media.storedPath) else const Text('Media file removed.'), From d40e33b2477fb4c46f00f32d42331a90f68512a6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Oct 2025 11:13:13 +0100 Subject: [PATCH 37/76] twonly Safe is now mandatory --- lib/app.dart | 15 +- lib/src/localization/app_de.arb | 8 +- lib/src/localization/app_en.arb | 10 +- .../generated/app_localizations.dart | 20 +- .../generated/app_localizations_de.dart | 15 +- .../generated/app_localizations_en.dart | 15 +- lib/src/services/api.service.dart | 4 +- .../create_backup.twonly_safe.dart | 12 +- lib/src/views/onboarding/recover.view.dart | 4 +- lib/src/views/onboarding/register.view.dart | 24 -- .../views/settings/backup/backup.view.dart | 137 ++++---- .../backup/twonly_safe_backup.view.dart | 296 ++++++++++-------- .../backup/twonly_safe_server.view.dart | 2 +- scripts/generate_proto.sh | 2 +- 14 files changed, 290 insertions(+), 274 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 14e339b..0025085 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart'; +import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; import 'package:twonly/src/views/updates/62_database_migration.view.dart'; class App extends StatefulWidget { @@ -181,9 +182,17 @@ class _AppMainWidgetState extends State { if (_showDatabaseMigration) { child = const DatabaseMigrationView(); } else if (_isUserCreated) { - child = HomeView( - initialPage: widget.initialPage, - ); + if (gUser.twonlySafeBackup == null) { + child = TwonlyIdentityBackupView( + callBack: () { + setState(() {}); + }, + ); + } else { + child = HomeView( + initialPage: widget.initialPage, + ); + } } else if (_showOnboarding) { child = OnboardingView( callbackOnSuccess: () => setState(() { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 9b1026f..4e91f4c 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -311,6 +311,7 @@ "backupLastBackupSize": "Backup-Größe", "backupLastBackupResult": "Ergebnis", "deleteBackupTitle": "Bist du sicher?", + "backupNoPasswordRecovery": "Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.", "deleteBackupBody": "Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.", "backupData": "Daten-Backup", "backupDataDesc": "Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.", @@ -318,18 +319,19 @@ "backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.", "backupInsecurePasswordOk": "Trotzdem fortfahren", "backupInsecurePasswordCancel": "Erneut versuchen", - "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", - "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Safe-Backup wiederherstellen möchtest.", + "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", + "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.", "password": "Passwort", "passwordRepeated": "Passwort wiederholen", "passwordRepeatedNotEqual": "Passwörter stimmen nicht überein.", "backupPasswordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein.", "backupExpertSettings": "Experteneinstellungen", "backupEnableBackup": "Automatische Sicherung aktivieren", - "backupOwnServerDesc": "Speichere dein twonly Safe-Backups auf einem Server deiner Wahl.", + "backupOwnServerDesc": "Speichere dein twonly Backup auf einem Server deiner Wahl.", "backupUseOwnServer": "Server verwenden", "backupResetServer": "Standardserver verwenden", "backupTwonlySaveNow": "Jetzt speichern", + "backupChangePassword": "Password ändern", "inviteFriends": "Freunde einladen", "inviteFriendsShareBtn": "Teilen", "inviteFriendsShareText": "Wechseln wir zu twonly: {url}", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 795dfb6..8b565d0 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -457,6 +457,7 @@ "backupFailed": "Failed", "backupSuccess": "Success", "backupTwonlySafeDesc": "Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.", + "backupNoPasswordRecovery": "Due to twonly's security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.", "backupServer": "Server", "backupMaxBackupSize": "max. backup size", "backupStorageRetention": "Storage retention", @@ -471,20 +472,21 @@ "backupInsecurePasswordDesc": "The chosen password is very insecure and can therefore easily be guessed by attackers. Please choose a secure password.", "backupInsecurePasswordOk": "Continue anyway", "backupInsecurePasswordCancel": "Try again", - "backupTwonlySafeLongDesc": "twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.", - "backupSelectStrongPassword": "Choose a secure password. This is required if you want to restore your twonly Safe backup.", + "backupTwonlySafeLongDesc": "twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.", + "backupSelectStrongPassword": "Choose a secure password. This is required if you want to restore your twonly Backup.", "password": "Password", "passwordRepeated": "Repeat password", "passwordRepeatedNotEqual": "Passwords do not match.", "backupPasswordRequirement": "Password must be at least 8 characters long.", "backupExpertSettings": "Expert settings", "backupEnableBackup": "Activate automatic backup", - "backupOwnServerDesc": "Save your twonly safe backups at twonly or on any server of your choice.", + "backupOwnServerDesc": "Save your twonly Backup at twonly or on any server of your choice.", "backupUseOwnServer": "Use server", "backupResetServer": "Use standard server", "backupTwonlySaveNow": "Save now", + "backupChangePassword": "Change password", "twonlySafeRecoverTitle": "Recovery", - "twonlySafeRecoverDesc": "If you have created a backup with twonly Safe, you can restore it here.", + "twonlySafeRecoverDesc": "If you have created a backup with twonly Backup, you can restore it here.", "twonlySafeRecoverBtn": "Restore backup", "inviteFriends": "Invite your friends", "inviteFriendsShareBtn": "Share", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 90fd5c2..dab9c79 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1844,6 +1844,12 @@ abstract class AppLocalizations { /// **'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'** String get backupTwonlySafeDesc; + /// No description provided for @backupNoPasswordRecovery. + /// + /// In en, this message translates to: + /// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'** + String get backupNoPasswordRecovery; + /// No description provided for @backupServer. /// /// In en, this message translates to: @@ -1931,13 +1937,13 @@ abstract class AppLocalizations { /// No description provided for @backupTwonlySafeLongDesc. /// /// In en, this message translates to: - /// **'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'** + /// **'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'** String get backupTwonlySafeLongDesc; /// No description provided for @backupSelectStrongPassword. /// /// In en, this message translates to: - /// **'Choose a secure password. This is required if you want to restore your twonly Safe backup.'** + /// **'Choose a secure password. This is required if you want to restore your twonly Backup.'** String get backupSelectStrongPassword; /// No description provided for @password. @@ -1979,7 +1985,7 @@ abstract class AppLocalizations { /// No description provided for @backupOwnServerDesc. /// /// In en, this message translates to: - /// **'Save your twonly safe backups at twonly or on any server of your choice.'** + /// **'Save your twonly Backup at twonly or on any server of your choice.'** String get backupOwnServerDesc; /// No description provided for @backupUseOwnServer. @@ -2000,6 +2006,12 @@ abstract class AppLocalizations { /// **'Save now'** String get backupTwonlySaveNow; + /// No description provided for @backupChangePassword. + /// + /// In en, this message translates to: + /// **'Change password'** + String get backupChangePassword; + /// No description provided for @twonlySafeRecoverTitle. /// /// In en, this message translates to: @@ -2009,7 +2021,7 @@ abstract class AppLocalizations { /// No description provided for @twonlySafeRecoverDesc. /// /// In en, this message translates to: - /// **'If you have created a backup with twonly Safe, you can restore it here.'** + /// **'If you have created a backup with twonly Backup, you can restore it here.'** String get twonlySafeRecoverDesc; /// No description provided for @twonlySafeRecoverBtn. diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index e41102a..f372df6 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -973,6 +973,10 @@ class AppLocalizationsDe extends AppLocalizations { String get backupTwonlySafeDesc => 'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.'; + @override + String get backupNoPasswordRecovery => + 'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.'; + @override String get backupServer => 'Server'; @@ -1020,11 +1024,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupTwonlySafeLongDesc => - 'twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.'; + 'twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.'; @override String get backupSelectStrongPassword => - 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Safe-Backup wiederherstellen möchtest.'; + 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.'; @override String get password => 'Passwort'; @@ -1047,7 +1051,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupOwnServerDesc => - 'Speichere dein twonly Safe-Backups auf einem Server deiner Wahl.'; + 'Speichere dein twonly Backup auf einem Server deiner Wahl.'; @override String get backupUseOwnServer => 'Server verwenden'; @@ -1058,12 +1062,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupTwonlySaveNow => 'Jetzt speichern'; + @override + String get backupChangePassword => 'Password ändern'; + @override String get twonlySafeRecoverTitle => 'Recovery'; @override String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Safe, you can restore it here.'; + 'If you have created a backup with twonly Backup, you can restore it here.'; @override String get twonlySafeRecoverBtn => 'Restore backup'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 71ffab8..db0c3cc 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -967,6 +967,10 @@ class AppLocalizationsEn extends AppLocalizations { String get backupTwonlySafeDesc => 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; + @override + String get backupNoPasswordRecovery => + 'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'; + @override String get backupServer => 'Server'; @@ -1014,11 +1018,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupTwonlySafeLongDesc => - 'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'; + 'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'; @override String get backupSelectStrongPassword => - 'Choose a secure password. This is required if you want to restore your twonly Safe backup.'; + 'Choose a secure password. This is required if you want to restore your twonly Backup.'; @override String get password => 'Password'; @@ -1041,7 +1045,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupOwnServerDesc => - 'Save your twonly safe backups at twonly or on any server of your choice.'; + 'Save your twonly Backup at twonly or on any server of your choice.'; @override String get backupUseOwnServer => 'Use server'; @@ -1052,12 +1056,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupTwonlySaveNow => 'Save now'; + @override + String get backupChangePassword => 'Change password'; + @override String get twonlySafeRecoverTitle => 'Recovery'; @override String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Safe, you can restore it here.'; + 'If you have created a backup with twonly Backup, you can restore it here.'; @override String get twonlySafeRecoverBtn => 'Restore backup'; diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 3683f2d..7de2f52 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -311,7 +311,9 @@ class ApiService { } if (res.error == ErrorCode.NewDeviceRegistered) { globalCallbackNewDeviceRegistered(); - Log.error('Device is disabled, as a newer device restore twonly Safe.'); + Log.error( + 'Device is disabled, as a newer device restore twonly Backup.', + ); appIsOutdated = true; await close(() {}); return Result.error(ErrorCode.InternalError); diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 80dd6e3..21212ec 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -40,7 +40,7 @@ Future performTwonlySafeBackup({bool force = false}) async { } } - Log.info('Starting new twonly Safe-Backup!'); + Log.info('Starting new twonly Backup!'); final baseDir = (await getApplicationSupportDirectory()).path; @@ -161,7 +161,7 @@ Future performTwonlySafeBackup({bool force = false}) async { await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); Log.info( - 'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.', + 'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.', ); if (gUser.backupServer != null) { @@ -187,7 +187,7 @@ Future performTwonlySafeBackup({bool force = false}) async { }, ); if (await FileDownloader().enqueue(task)) { - Log.info('Starting upload from twonly Safe backup.'); + Log.info('Starting upload from twonly Backup.'); await updateUserdata((user) { user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; user.twonlySafeBackup!.lastBackupDone = DateTime.now(); @@ -196,7 +196,7 @@ Future performTwonlySafeBackup({bool force = false}) async { }); gUpdateBackupView(); } else { - Log.error('Error starting UploadTask for twonly Safe.'); + Log.error('Error starting UploadTask for twonly Backup.'); } } @@ -204,7 +204,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async { if (update.status == TaskStatus.failed || update.status == TaskStatus.canceled) { Log.error( - 'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}', + 'twonly Backup upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}', ); await updateUserdata((user) { if (user.twonlySafeBackup != null) { @@ -214,7 +214,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async { }); } else if (update.status == TaskStatus.complete) { Log.error( - 'twonly Safe uploaded with status code ${update.responseStatusCode}', + 'twonly Backup uploaded with status code ${update.responseStatusCode}', ); await updateUserdata((user) { if (user.twonlySafeBackup != null) { diff --git a/lib/src/views/onboarding/recover.view.dart b/lib/src/views/onboarding/recover.view.dart index 4590aef..e9b061c 100644 --- a/lib/src/views/onboarding/recover.view.dart +++ b/lib/src/views/onboarding/recover.view.dart @@ -60,13 +60,13 @@ class _BackupRecoveryViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('twonly Safe ${context.lang.twonlySafeRecoverTitle}'), + title: Text('twonly Backup ${context.lang.twonlySafeRecoverTitle}'), actions: [ IconButton( onPressed: () async { await showAlertDialog( context, - 'twonly Safe', + 'twonly Backup', context.lang.backupTwonlySafeLongDesc, ); }, diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 4a1b97a..e652f63 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -166,30 +166,6 @@ class _RegisterViewState extends State { ), textAlign: TextAlign.center, ), - // const SizedBox(height: 5), - // Center( - // child: Padding( - // padding: EdgeInsets.only(left: 10, right: 10), - // child: Text( - // context.lang.registerUsernameLimits, - // textAlign: TextAlign.center, - // style: const TextStyle(fontSize: 9), - // ), - // ), - // ), - // const SizedBox(height: 30), - // Center( - // child: Text( - // context.lang.registerTwonlyCodeText, - // textAlign: TextAlign.center, - // ), - // ), - // const SizedBox(height: 10), - // TextField( - // controller: inviteCodeController, - // decoration: - // getInputDecoration(context.lang.registerTwonlyCodeLabel), - // ), const SizedBox(height: 30), Column( children: [ diff --git a/lib/src/views/settings/backup/backup.view.dart b/lib/src/views/settings/backup/backup.view.dart index 92a42eb..28417bf 100644 --- a/lib/src/views/settings/backup/backup.view.dart +++ b/lib/src/views/settings/backup/backup.view.dart @@ -3,10 +3,8 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; void Function() gUpdateBackupView = () {}; @@ -25,8 +23,6 @@ BackupServer defaultBackupServer = BackupServer( ); class _BackupViewState extends State { - TwonlySafeBackup? twonlySafeBackup; - BackupServer backupServer = defaultBackupServer; bool isLoading = false; int activePageIdx = 0; @@ -47,11 +43,6 @@ class _BackupViewState extends State { } Future initAsync() async { - twonlySafeBackup = gUser.twonlySafeBackup; - backupServer = defaultBackupServer; - if (gUser.backupServer != null) { - backupServer = gUser.backupServer!; - } setState(() {}); } @@ -68,8 +59,25 @@ class _BackupViewState extends State { } } + Future changeTwonlySafePassword() async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const TwonlyIdentityBackupView( + isPasswordChangeOnly: true, + ); + }, + ), + ); + setState(() { + // gUser was updated + }); + } + @override Widget build(BuildContext context) { + final backupServer = gUser.backupServer ?? defaultBackupServer; return Scaffold( appBar: AppBar( title: Text(context.lang.settingsBackup), @@ -83,10 +91,13 @@ class _BackupViewState extends State { }, children: [ BackupOption( - title: 'twonly Safe', + title: 'twonly Backup', description: context.lang.backupTwonlySafeDesc, - autoBackupEnabled: twonlySafeBackup != null, - child: (twonlySafeBackup == null) + bottomButton: FilledButton( + onPressed: changeTwonlySafePassword, + child: Text(context.lang.backupChangePassword), + ), + child: (gUser.twonlySafeBackup == null) ? null : Column( children: [ @@ -114,16 +125,20 @@ class _BackupViewState extends State { context.lang.backupLastBackupDate, formatDateTime( context, - twonlySafeBackup!.lastBackupDone, + gUser.twonlySafeBackup!.lastBackupDone, ) ), ( context.lang.backupLastBackupSize, - formatBytes(twonlySafeBackup!.lastBackupSize) + formatBytes( + gUser.twonlySafeBackup!.lastBackupSize, + ) ), ( context.lang.backupLastBackupResult, - backupStatus(twonlySafeBackup!.backupUploadState) + backupStatus( + gUser.twonlySafeBackup!.backupUploadState, + ) ), ].map((pair) { return TableRow( @@ -134,8 +149,9 @@ class _BackupViewState extends State { ), TableCell( child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: 4, + ), child: Text( pair.$2, textAlign: TextAlign.right, @@ -148,7 +164,7 @@ class _BackupViewState extends State { ], ), const SizedBox(height: 10), - FilledButton( + OutlinedButton( onPressed: isLoading ? null : () async { @@ -164,37 +180,10 @@ class _BackupViewState extends State { ), ], ), - onTap: () async { - if (twonlySafeBackup != null) { - final disable = await showAlertDialog( - context, - context.lang.deleteBackupTitle, - context.lang.deleteBackupBody, - ); - if (disable) { - await disableTwonlySafe(); - } - } else { - setState(() { - isLoading = true; - }); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const TwonlyIdentityBackupView(); - }, - ), - ); - } - await initAsync(); - }, ), BackupOption( title: '${context.lang.backupData} (Coming Soon)', description: context.lang.backupDataDesc, - autoBackupEnabled: false, - onTap: null, ), ], ), @@ -209,7 +198,7 @@ class _BackupViewState extends State { items: [ const BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.vault, size: 17), - label: 'twonly Safe', + label: 'twonly Backup', ), BottomNavigationBarItem( icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 17), @@ -236,51 +225,35 @@ class BackupOption extends StatelessWidget { const BackupOption({ required this.title, required this.description, - required this.autoBackupEnabled, - required this.onTap, + this.bottomButton, super.key, this.child, }); final String title; final String description; final Widget? child; - final bool autoBackupEnabled; - final void Function()? onTap; + final Widget? bottomButton; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: autoBackupEnabled ? null : onTap, - child: Card( - margin: const EdgeInsets.all(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: - const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text(description), - const SizedBox(height: 8), - if (child != null) child! else Container(), - Expanded(child: Container()), - Center( - child: autoBackupEnabled - ? OutlinedButton( - onPressed: onTap, - child: Text(context.lang.disable), - ) - : FilledButton( - onPressed: onTap, - child: Text(context.lang.enable), - ), - ), - ], - ), + return Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text(description), + const SizedBox(height: 8), + if (child != null) child! else Container(), + Expanded(child: Container()), + if (bottomButton != null) Center(child: bottomButton), + ], ), ), ); diff --git a/lib/src/views/settings/backup/twonly_safe_backup.view.dart b/lib/src/views/settings/backup/twonly_safe_backup.view.dart index 7d7915f..64f7ba1 100644 --- a/lib/src/views/settings/backup/twonly_safe_backup.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_backup.view.dart @@ -8,7 +8,16 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_server.view.dart'; class TwonlyIdentityBackupView extends StatefulWidget { - const TwonlyIdentityBackupView({super.key}); + const TwonlyIdentityBackupView({ + this.isPasswordChangeOnly = false, + this.callBack, + super.key, + }); + + // in case a callback is defined the callback + // is called instead of the Navigator.pop() + final VoidCallback? callBack; + final bool isPasswordChangeOnly; @override State createState() => @@ -56,148 +65,165 @@ class _TwonlyIdentityBackupViewState extends State { isLoading = false; }); - Navigator.pop(context); + if (widget.callBack != null) { + widget.callBack!(); + } else { + Navigator.pop(context); + } } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('twonly Safe'), - actions: [ - IconButton( - onPressed: () async { - await showAlertDialog( - context, - 'twonly Safe', - context.lang.backupTwonlySafeLongDesc, - ); - }, - icon: const FaIcon(FontAwesomeIcons.circleInfo), - iconSize: 18, - ), - ], - ), - body: Padding( - padding: - const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), - child: ListView( - children: [ - Text( - context.lang.backupSelectStrongPassword, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Stack( - children: [ - TextField( - controller: passwordCtrl, - onChanged: (value) { - setState(() {}); - }, - style: const TextStyle(fontSize: 17), - obscureText: obscureText, - decoration: getInputDecoration( - context, - context.lang.password, - ), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: IconButton( - onPressed: () { - setState(() { - obscureText = !obscureText; - }); - }, - icon: FaIcon( - obscureText - ? FontAwesomeIcons.eye - : FontAwesomeIcons.eyeSlash, - size: 16, - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsetsGeometry.all(5), - child: Text( - context.lang.backupPasswordRequirement, - style: TextStyle( - fontSize: 13, - color: (passwordCtrl.text.length < 8 && - passwordCtrl.text.isNotEmpty) - ? Colors.red - : Colors.transparent, - ), - ), - ), - const SizedBox(height: 5), - TextField( - controller: repeatedPasswordCtrl, - onChanged: (value) { - setState(() {}); + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: const Text('twonly Backup'), + actions: [ + IconButton( + onPressed: () async { + await showAlertDialog( + context, + 'twonly Backup', + context.lang.backupTwonlySafeLongDesc, + ); }, - style: const TextStyle(fontSize: 17), - obscureText: true, - decoration: getInputDecoration( - context, - context.lang.passwordRepeated, - ), - ), - Padding( - padding: const EdgeInsetsGeometry.all(5), - child: Text( - context.lang.passwordRepeatedNotEqual, - style: TextStyle( - fontSize: 13, - color: (passwordCtrl.text != repeatedPasswordCtrl.text && - repeatedPasswordCtrl.text.isNotEmpty) - ? Colors.red - : Colors.transparent, - ), - ), - ), - const SizedBox(height: 10), - Center( - child: OutlinedButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const TwonlySafeServerView(); - }, - ), - ); - }, - child: Text(context.lang.backupExpertSettings), - ), - ), - const SizedBox(height: 10), - Center( - child: FilledButton.icon( - onPressed: (!isLoading && - (passwordCtrl.text == repeatedPasswordCtrl.text && - passwordCtrl.text.length >= 8 || - kDebugMode)) - ? onPressedEnableTwonlySafe - : null, - icon: isLoading - ? const SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator(strokeWidth: 1), - ) - : const Icon(Icons.lock_clock_rounded), - label: Text(context.lang.backupEnableBackup), - ), + icon: const FaIcon(FontAwesomeIcons.circleInfo), + iconSize: 18, ), ], ), + body: Padding( + padding: + const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), + child: ListView( + children: [ + Text( + context.lang.backupSelectStrongPassword, + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Stack( + children: [ + TextField( + controller: passwordCtrl, + onChanged: (value) { + setState(() {}); + }, + style: const TextStyle(fontSize: 17), + obscureText: obscureText, + decoration: getInputDecoration( + context, + context.lang.password, + ), + ), + Positioned( + right: 0, + top: 0, + bottom: 0, + child: IconButton( + onPressed: () { + setState(() { + obscureText = !obscureText; + }); + }, + icon: FaIcon( + obscureText + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 16, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsetsGeometry.all(5), + child: Text( + context.lang.backupPasswordRequirement, + style: TextStyle( + fontSize: 13, + color: (passwordCtrl.text.length < 8 && + passwordCtrl.text.isNotEmpty) + ? Colors.red + : Colors.transparent, + ), + ), + ), + const SizedBox(height: 5), + TextField( + controller: repeatedPasswordCtrl, + onChanged: (value) { + setState(() {}); + }, + style: const TextStyle(fontSize: 17), + obscureText: true, + decoration: getInputDecoration( + context, + context.lang.passwordRepeated, + ), + ), + Padding( + padding: const EdgeInsetsGeometry.all(5), + child: Text( + context.lang.passwordRepeatedNotEqual, + style: TextStyle( + fontSize: 13, + color: (passwordCtrl.text != repeatedPasswordCtrl.text && + repeatedPasswordCtrl.text.isNotEmpty) + ? Colors.red + : Colors.transparent, + ), + ), + ), + const SizedBox(height: 10), + Center( + child: OutlinedButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const TwonlySafeServerView(); + }, + ), + ); + }, + child: Text(context.lang.backupExpertSettings), + ), + ), + const SizedBox(height: 10), + Text( + context.lang.backupNoPasswordRecovery, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + const SizedBox(height: 10), + Center( + child: FilledButton.icon( + onPressed: (!isLoading && + (passwordCtrl.text == repeatedPasswordCtrl.text && + passwordCtrl.text.length >= 8 || + kDebugMode)) + ? onPressedEnableTwonlySafe + : null, + icon: isLoading + ? const SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator(strokeWidth: 1), + ) + : const Icon(Icons.lock_clock_rounded), + label: Text( + widget.isPasswordChangeOnly + ? context.lang.backupChangePassword + : context.lang.backupEnableBackup, + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/src/views/settings/backup/twonly_safe_server.view.dart b/lib/src/views/settings/backup/twonly_safe_server.view.dart index a7c2cf8..c0e4bfd 100644 --- a/lib/src/views/settings/backup/twonly_safe_server.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_server.view.dart @@ -107,7 +107,7 @@ class _TwonlySafeServerViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('twonly Safe Server'), + title: const Text('twonly Backup Server'), ), body: Padding( padding: const EdgeInsets.all(40), diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index ceb9d77..3824b77 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -7,7 +7,7 @@ if [ ! -f "pubspec.yaml" ]; then exit 1 fi -# Definitions for twonly Safe +# Definitions for twonly Backup GENERATED_DIR="./lib/src/model/protobuf/client/generated/" CLIENT_DIR="./lib/src/model/protobuf/client/" From 09cb0552c0f7bd607a90a48e2805e419b63134a3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 30 Oct 2025 00:13:31 +0100 Subject: [PATCH 38/76] starting with group states --- lib/src/database/daos/messages.dao.dart | 26 +++++++++++--------- lib/src/model/protobuf/client/groups.proto | 10 ++++++++ lib/src/model/protobuf/client/messages.proto | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 lib/src/model/protobuf/client/groups.proto diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index bc6a26e..85b2928 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -270,12 +270,13 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { actionAt: Value(timestamp), ), ); - if (await haveAllMembers(messageId, MessageActionType.openedAt)) { - await twonlyDB.messagesDao.updateMessageId( - messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), - ); - } + // Directly show as message opened as soon as one person has opened it + // if (await haveAllMembers(messageId, MessageActionType.openedAt)) { + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(openedAt: Value(DateTime.now())), + ); + // } } Future handleMessageAckByServer( @@ -291,12 +292,13 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { actionAt: Value(timestamp), ), ); - if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) { - await twonlyDB.messagesDao.updateMessageId( - messageId, - MessagesCompanion(ackByServer: Value(DateTime.now())), - ); - } + // if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) { + /// always update the state, so it will be shown as soon as one member gets the message + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(ackByServer: Value(DateTime.now())), + ); + // } } Future haveAllMembers( diff --git a/lib/src/model/protobuf/client/groups.proto b/lib/src/model/protobuf/client/groups.proto new file mode 100644 index 0000000..140d754 --- /dev/null +++ b/lib/src/model/protobuf/client/groups.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +// Stored encrypted on the server in the members columns. +message GroupState { + repeated int64 memberIds = 1; + repeated int64 adminIds = 2; + string groupName = 3; + optional int64 deleteMessagesAfterMilliseconds = 4; + bytes _padding = 5; +} \ No newline at end of file diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 9bdbd6f..57760f5 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -44,6 +44,31 @@ message EncryptedContent { optional PushKeys pushKeys = 11; optional Reaction reaction = 12; optional TextMessage textMessage = 13; + optional NewGroup newGroup = 14; + optional JoinGroup joinGroup = 15; + optional GroupUpdate groupUpdate = 16; + + + message NewGroup { + // key for the state stored on the server + string groupId = 1; + string stateId = 2; + bytes stateKey = 3; + bytes groupPublicKey = 4; + } + + message JoinGroup { + // key for the state stored on the server + string groupId = 1; + bytes groupPublicKey = 4; + } + + message GroupUpdate { + optional int64 removedAdmin = 1; + optional int64 removedUser = 2; + optional int64 groupName = 3; + optional int64 deleteMessagesAfterMilliseconds = 4; + } message TextMessage { string senderMessageId = 1; From 7748ecec3c3f0bb113f8d6bc8e03ba88bcda4f6a Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 30 Oct 2025 20:47:15 +0100 Subject: [PATCH 39/76] fixing background notification --- lib/src/database/daos/groups.dao.dart | 8 ++ lib/src/database/tables/groups.table.dart | 3 + lib/src/database/twonly.db.g.dart | 126 ++++++++++++++++++ lib/src/model/protobuf/client/messages.proto | 1 + lib/src/services/fcm.service.dart | 14 +- .../background.notifications.dart | 5 +- 6 files changed, 152 insertions(+), 5 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b5c741d..4f76cc7 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -162,6 +162,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { final totalMediaCounter = group.totalMediaCounter + 1; var flameCounter = group.flameCounter; + var maxFlameCounter = group.maxFlameCounter; + var maxFlameCounterFrom = group.maxFlameCounterFrom; if (group.lastMessageReceived != null && group.lastMessageSend != null) { final now = DateTime.now(); @@ -198,6 +200,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { if (updateFlame) { flameCounter += 1; lastFlameCounterChange = Value(timestamp); + if (flameCounter > maxFlameCounter) { + maxFlameCounter = flameCounter; + maxFlameCounterFrom = DateTime.now(); + } } } } else { @@ -218,6 +224,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { lastMessageReceived: lastMessageReceived, lastMessageSend: lastMessageSend, flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: Value(maxFlameCounterFrom), ), ); } diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 9bbac87..708622c 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -29,6 +29,9 @@ class Groups extends Table { IntColumn get flameCounter => integer().withDefault(const Constant(0))(); + IntColumn get maxFlameCounter => integer().withDefault(const Constant(0))(); + DateTimeColumn get maxFlameCounterFrom => dateTime().nullable()(); + DateTimeColumn get lastMessageExchange => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index cfe85e9..57b9870 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -775,6 +775,20 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const Constant(0)); + static const VerificationMeta _maxFlameCounterMeta = + const VerificationMeta('maxFlameCounter'); + @override + late final GeneratedColumn maxFlameCounter = GeneratedColumn( + 'max_flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _maxFlameCounterFromMeta = + const VerificationMeta('maxFlameCounterFrom'); + @override + late final GeneratedColumn maxFlameCounterFrom = + GeneratedColumn('max_flame_counter_from', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _lastMessageExchangeMeta = const VerificationMeta('lastMessageExchange'); @override @@ -800,6 +814,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { lastFlameCounterChange, lastFlameSync, flameCounter, + maxFlameCounter, + maxFlameCounterFrom, lastMessageExchange ]; @override @@ -901,6 +917,18 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { flameCounter.isAcceptableOrUnknown( data['flame_counter']!, _flameCounterMeta)); } + if (data.containsKey('max_flame_counter')) { + context.handle( + _maxFlameCounterMeta, + maxFlameCounter.isAcceptableOrUnknown( + data['max_flame_counter']!, _maxFlameCounterMeta)); + } + if (data.containsKey('max_flame_counter_from')) { + context.handle( + _maxFlameCounterFromMeta, + maxFlameCounterFrom.isAcceptableOrUnknown( + data['max_flame_counter_from']!, _maxFlameCounterFromMeta)); + } if (data.containsKey('last_message_exchange')) { context.handle( _lastMessageExchangeMeta, @@ -949,6 +977,11 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { DriftSqlType.dateTime, data['${effectivePrefix}last_flame_sync']), flameCounter: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}flame_counter'])!, + maxFlameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}max_flame_counter'])!, + maxFlameCounterFrom: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}max_flame_counter_from']), lastMessageExchange: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}last_message_exchange'])!, @@ -977,6 +1010,8 @@ class Group extends DataClass implements Insertable { final DateTime? lastFlameCounterChange; final DateTime? lastFlameSync; final int flameCounter; + final int maxFlameCounter; + final DateTime? maxFlameCounterFrom; final DateTime lastMessageExchange; const Group( {required this.groupId, @@ -994,6 +1029,8 @@ class Group extends DataClass implements Insertable { this.lastFlameCounterChange, this.lastFlameSync, required this.flameCounter, + required this.maxFlameCounter, + this.maxFlameCounterFrom, required this.lastMessageExchange}); @override Map toColumns(bool nullToAbsent) { @@ -1023,6 +1060,10 @@ class Group extends DataClass implements Insertable { map['last_flame_sync'] = Variable(lastFlameSync); } map['flame_counter'] = Variable(flameCounter); + map['max_flame_counter'] = Variable(maxFlameCounter); + if (!nullToAbsent || maxFlameCounterFrom != null) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom); + } map['last_message_exchange'] = Variable(lastMessageExchange); return map; } @@ -1052,6 +1093,10 @@ class Group extends DataClass implements Insertable { ? const Value.absent() : Value(lastFlameSync), flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: maxFlameCounterFrom == null && nullToAbsent + ? const Value.absent() + : Value(maxFlameCounterFrom), lastMessageExchange: Value(lastMessageExchange), ); } @@ -1078,6 +1123,9 @@ class Group extends DataClass implements Insertable { serializer.fromJson(json['lastFlameCounterChange']), lastFlameSync: serializer.fromJson(json['lastFlameSync']), flameCounter: serializer.fromJson(json['flameCounter']), + maxFlameCounter: serializer.fromJson(json['maxFlameCounter']), + maxFlameCounterFrom: + serializer.fromJson(json['maxFlameCounterFrom']), lastMessageExchange: serializer.fromJson(json['lastMessageExchange']), ); @@ -1103,6 +1151,8 @@ class Group extends DataClass implements Insertable { serializer.toJson(lastFlameCounterChange), 'lastFlameSync': serializer.toJson(lastFlameSync), 'flameCounter': serializer.toJson(flameCounter), + 'maxFlameCounter': serializer.toJson(maxFlameCounter), + 'maxFlameCounterFrom': serializer.toJson(maxFlameCounterFrom), 'lastMessageExchange': serializer.toJson(lastMessageExchange), }; } @@ -1123,6 +1173,8 @@ class Group extends DataClass implements Insertable { Value lastFlameCounterChange = const Value.absent(), Value lastFlameSync = const Value.absent(), int? flameCounter, + int? maxFlameCounter, + Value maxFlameCounterFrom = const Value.absent(), DateTime? lastMessageExchange}) => Group( groupId: groupId ?? this.groupId, @@ -1148,6 +1200,10 @@ class Group extends DataClass implements Insertable { lastFlameSync: lastFlameSync.present ? lastFlameSync.value : this.lastFlameSync, flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom.present + ? maxFlameCounterFrom.value + : this.maxFlameCounterFrom, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, ); Group copyWithCompanion(GroupsCompanion data) { @@ -1188,6 +1244,12 @@ class Group extends DataClass implements Insertable { flameCounter: data.flameCounter.present ? data.flameCounter.value : this.flameCounter, + maxFlameCounter: data.maxFlameCounter.present + ? data.maxFlameCounter.value + : this.maxFlameCounter, + maxFlameCounterFrom: data.maxFlameCounterFrom.present + ? data.maxFlameCounterFrom.value + : this.maxFlameCounterFrom, lastMessageExchange: data.lastMessageExchange.present ? data.lastMessageExchange.value : this.lastMessageExchange, @@ -1213,6 +1275,8 @@ class Group extends DataClass implements Insertable { ..write('lastFlameCounterChange: $lastFlameCounterChange, ') ..write('lastFlameSync: $lastFlameSync, ') ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') ..write('lastMessageExchange: $lastMessageExchange') ..write(')')) .toString(); @@ -1235,6 +1299,8 @@ class Group extends DataClass implements Insertable { lastFlameCounterChange, lastFlameSync, flameCounter, + maxFlameCounter, + maxFlameCounterFrom, lastMessageExchange); @override bool operator ==(Object other) => @@ -1256,6 +1322,8 @@ class Group extends DataClass implements Insertable { other.lastFlameCounterChange == this.lastFlameCounterChange && other.lastFlameSync == this.lastFlameSync && other.flameCounter == this.flameCounter && + other.maxFlameCounter == this.maxFlameCounter && + other.maxFlameCounterFrom == this.maxFlameCounterFrom && other.lastMessageExchange == this.lastMessageExchange); } @@ -1275,6 +1343,8 @@ class GroupsCompanion extends UpdateCompanion { final Value lastFlameCounterChange; final Value lastFlameSync; final Value flameCounter; + final Value maxFlameCounter; + final Value maxFlameCounterFrom; final Value lastMessageExchange; final Value rowid; const GroupsCompanion({ @@ -1293,6 +1363,8 @@ class GroupsCompanion extends UpdateCompanion { this.lastFlameCounterChange = const Value.absent(), this.lastFlameSync = const Value.absent(), this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.rowid = const Value.absent(), }); @@ -1312,6 +1384,8 @@ class GroupsCompanion extends UpdateCompanion { this.lastFlameCounterChange = const Value.absent(), this.lastFlameSync = const Value.absent(), this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), this.lastMessageExchange = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), @@ -1334,6 +1408,8 @@ class GroupsCompanion extends UpdateCompanion { Expression? lastFlameCounterChange, Expression? lastFlameSync, Expression? flameCounter, + Expression? maxFlameCounter, + Expression? maxFlameCounterFrom, Expression? lastMessageExchange, Expression? rowid, }) { @@ -1356,6 +1432,9 @@ class GroupsCompanion extends UpdateCompanion { 'last_flame_counter_change': lastFlameCounterChange, if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, if (flameCounter != null) 'flame_counter': flameCounter, + if (maxFlameCounter != null) 'max_flame_counter': maxFlameCounter, + if (maxFlameCounterFrom != null) + 'max_flame_counter_from': maxFlameCounterFrom, if (lastMessageExchange != null) 'last_message_exchange': lastMessageExchange, if (rowid != null) 'rowid': rowid, @@ -1378,6 +1457,8 @@ class GroupsCompanion extends UpdateCompanion { Value? lastFlameCounterChange, Value? lastFlameSync, Value? flameCounter, + Value? maxFlameCounter, + Value? maxFlameCounterFrom, Value? lastMessageExchange, Value? rowid}) { return GroupsCompanion( @@ -1398,6 +1479,8 @@ class GroupsCompanion extends UpdateCompanion { lastFlameCounterChange ?? this.lastFlameCounterChange, lastFlameSync: lastFlameSync ?? this.lastFlameSync, flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom ?? this.maxFlameCounterFrom, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, rowid: rowid ?? this.rowid, ); @@ -1454,6 +1537,13 @@ class GroupsCompanion extends UpdateCompanion { if (flameCounter.present) { map['flame_counter'] = Variable(flameCounter.value); } + if (maxFlameCounter.present) { + map['max_flame_counter'] = Variable(maxFlameCounter.value); + } + if (maxFlameCounterFrom.present) { + map['max_flame_counter_from'] = + Variable(maxFlameCounterFrom.value); + } if (lastMessageExchange.present) { map['last_message_exchange'] = Variable(lastMessageExchange.value); @@ -1483,6 +1573,8 @@ class GroupsCompanion extends UpdateCompanion { ..write('lastFlameCounterChange: $lastFlameCounterChange, ') ..write('lastFlameSync: $lastFlameSync, ') ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') ..write('lastMessageExchange: $lastMessageExchange, ') ..write('rowid: $rowid') ..write(')')) @@ -7414,6 +7506,8 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ Value lastFlameCounterChange, Value lastFlameSync, Value flameCounter, + Value maxFlameCounter, + Value maxFlameCounterFrom, Value lastMessageExchange, Value rowid, }); @@ -7433,6 +7527,8 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value lastFlameCounterChange, Value lastFlameSync, Value flameCounter, + Value maxFlameCounter, + Value maxFlameCounterFrom, Value lastMessageExchange, Value rowid, }); @@ -7516,6 +7612,14 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get flameCounter => $composableBuilder( column: $table.flameCounter, builder: (column) => ColumnFilters(column)); + ColumnFilters get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, + builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnFilters(column)); @@ -7606,6 +7710,14 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { column: $table.flameCounter, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnOrderings(column)); @@ -7667,6 +7779,12 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get flameCounter => $composableBuilder( column: $table.flameCounter, builder: (column) => column); + GeneratedColumn get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, builder: (column) => column); + + GeneratedColumn get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, builder: (column) => column); + GeneratedColumn get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => column); @@ -7730,6 +7848,8 @@ class $$GroupsTableTableManager extends RootTableManager< Value lastFlameCounterChange = const Value.absent(), Value lastFlameSync = const Value.absent(), Value flameCounter = const Value.absent(), + Value maxFlameCounter = const Value.absent(), + Value maxFlameCounterFrom = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -7749,6 +7869,8 @@ class $$GroupsTableTableManager extends RootTableManager< lastFlameCounterChange: lastFlameCounterChange, lastFlameSync: lastFlameSync, flameCounter: flameCounter, + maxFlameCounter: maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom, lastMessageExchange: lastMessageExchange, rowid: rowid, ), @@ -7768,6 +7890,8 @@ class $$GroupsTableTableManager extends RootTableManager< Value lastFlameCounterChange = const Value.absent(), Value lastFlameSync = const Value.absent(), Value flameCounter = const Value.absent(), + Value maxFlameCounter = const Value.absent(), + Value maxFlameCounterFrom = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -7787,6 +7911,8 @@ class $$GroupsTableTableManager extends RootTableManager< lastFlameCounterChange: lastFlameCounterChange, lastFlameSync: lastFlameSync, flameCounter: flameCounter, + maxFlameCounter: maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom, lastMessageExchange: lastMessageExchange, rowid: rowid, ), diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 57760f5..29dc4e1 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -68,6 +68,7 @@ message EncryptedContent { optional int64 removedUser = 2; optional int64 groupName = 3; optional int64 deleteMessagesAfterMilliseconds = 4; + bytes stateKey = 5; } message TextMessage { diff --git a/lib/src/services/fcm.service.dart b/lib/src/services/fcm.service.dart index 899936f..33ffaa8 100644 --- a/lib/src/services/fcm.service.dart +++ b/lib/src/services/fcm.service.dart @@ -81,10 +81,18 @@ Future handleRemoteMessage(RemoteMessage message) async { if (!Platform.isAndroid) { Log.error('Got message in Dart while on iOS'); } + if (message.notification != null && globalIsAppInBackground) { + Log.error( + 'Got notification but app is in background, so the SDK already have shown the message.', + ); + return; + } - if (message.notification != null) { - final title = message.notification!.title ?? ''; - final body = message.notification!.body ?? ''; + if (message.notification != null || message.data['title'] != null) { + final title = + message.notification?.title ?? message.data['title'] as String? ?? ''; + final body = + message.notification?.body ?? message.data['body'] as String? ?? ''; await customLocalPushNotification(title, body); } else if (message.data['push_data'] != null) { await handlePushData(message.data['push_data'] as String); diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 360a375..be510f0 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -16,16 +16,17 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future customLocalPushNotification(String title, String msg) async { - const androidNotificationDetails = AndroidNotificationDetails( + final androidNotificationDetails = AndroidNotificationDetails( '1', 'System', channelDescription: 'System messages.', importance: Importance.max, priority: Priority.max, + styleInformation: BigTextStyleInformation(msg), ); const darwinNotificationDetails = DarwinNotificationDetails(); - const notificationDetails = NotificationDetails( + final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); From 76ff64ff747cb9f98107eb53265478a7643ad695 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Nov 2025 01:46:02 +0100 Subject: [PATCH 40/76] starting with c2c #227 --- .metadata | 30 + README.md | 10 +- lib/src/database/daos/groups.dao.dart | 31 +- lib/src/database/daos/groups.dao.g.dart | 1 + lib/src/database/tables/groups.table.dart | 54 +- lib/src/database/twonly.db.dart | 1 + lib/src/database/twonly.db.g.dart | 1619 ++++++++++++++++- lib/src/localization/app_de.arb | 8 +- lib/src/localization/app_en.arb | 8 +- .../generated/app_localizations.dart | 36 + .../generated/app_localizations_de.dart | 18 + .../generated/app_localizations_en.dart | 18 + .../protobuf/api/http/http_requests.pb.dart | 344 ++++ .../api/http/http_requests.pbjson.dart | 68 + .../api/websocket/client_to_server.pb.dart | 14 + .../websocket/client_to_server.pbjson.dart | 20 +- .../protobuf/client/generated/groups.pb.dart | 192 ++ .../client/generated/groups.pbenum.dart | 11 + .../client/generated/groups.pbjson.dart | 54 + .../client/generated/groups.pbserver.dart | 14 + .../client/generated/messages.pb.dart | 242 +++ .../client/generated/messages.pbjson.dart | 135 +- lib/src/model/protobuf/client/groups.proto | 10 +- lib/src/model/protobuf/client/messages.proto | 19 +- lib/src/services/api.service.dart | 6 + .../contact.c2c.dart} | 0 .../api/client2client/groups.c2c.dart | 78 + .../media.c2c.dart} | 0 .../messages.c2c.dart} | 0 .../prekeys.c2c.dart} | 0 .../pushkeys.c2c.dart} | 0 .../reaction.c2c.dart} | 0 .../text_message.c2c.dart} | 0 lib/src/services/api/server_messages.dart | 46 +- lib/src/services/group.services.dart | 179 ++ lib/src/views/chats/add_new_user.view.dart | 3 +- lib/src/views/chats/start_new_chat.view.dart | 29 +- .../group_create_select_group_name.view.dart | 128 ++ .../group_create_select_members.view.dart | 250 +++ scripts/generate_proto.sh | 11 +- 40 files changed, 3518 insertions(+), 169 deletions(-) create mode 100644 .metadata create mode 100644 lib/src/model/protobuf/client/generated/groups.pb.dart create mode 100644 lib/src/model/protobuf/client/generated/groups.pbenum.dart create mode 100644 lib/src/model/protobuf/client/generated/groups.pbjson.dart create mode 100644 lib/src/model/protobuf/client/generated/groups.pbserver.dart rename lib/src/services/api/{server_messages/contact.server_messages.dart => client2client/contact.c2c.dart} (100%) create mode 100644 lib/src/services/api/client2client/groups.c2c.dart rename lib/src/services/api/{server_messages/media.server_messages.dart => client2client/media.c2c.dart} (100%) rename lib/src/services/api/{server_messages/messages.server_messages.dart => client2client/messages.c2c.dart} (100%) rename lib/src/services/api/{server_messages/prekeys.server_messages.dart => client2client/prekeys.c2c.dart} (100%) rename lib/src/services/api/{server_messages/pushkeys.server_messages.dart => client2client/pushkeys.c2c.dart} (100%) rename lib/src/services/api/{server_messages/reaction.server_message.dart => client2client/reaction.c2c.dart} (100%) rename lib/src/services/api/{server_messages/text_message.server_messages.dart => client2client/text_message.c2c.dart} (100%) create mode 100644 lib/src/services/group.services.dart create mode 100644 lib/src/views/groups/group_create_select_group_name.view.dart create mode 100644 lib/src/views/groups/group_create_select_members.view.dart diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..fca9f99 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: macos + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md index 09663de..02eac22 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,16 @@ This repository contains the complete source code of the [twonly](https://twonly - Offer a Snapchat™ like experience - End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll) +- twonly is Open Source and can be downloaded directly from GitHub +- Developed by humans not by AI or Vibe Coding - No email or phone number required to register - Privacy friendly - Everything is stored on the device -- Open-Source +- Backend is exclusively hosted in European -## In work +## Planned -- For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM -- For Android: Reproducible Builds + Publishing F-Droid +- For Android: Optional support for [UnifiedPush](https://unifiedpush.org/) +- For Android: Reproducible Builds - Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata ## Security Issues diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 4f76cc7..b28d460 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -8,7 +8,7 @@ import 'package:twonly/src/utils/misc.dart'; part 'groups.dao.g.dart'; -@DriftAccessor(tables: [Groups, GroupMembers]) +@DriftAccessor(tables: [Groups, GroupMembers, GroupHistories]) class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -37,11 +37,21 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future createNewGroup(GroupsCompanion group) async { - final insertGroup = group.copyWith( - groupId: Value(uuid.v4()), - isGroupAdmin: const Value(true), - ); - return _insertGroup(insertGroup); + return _insertGroup(group); + } + + Future insertGroupMember(GroupMembersCompanion members) async { + await into(groupMembers).insert(members); + } + + Future insertGroupAction(GroupHistoriesCompanion action) async { + var insertAction = action; + if (!action.groupHistoryId.present) { + insertAction = action.copyWith( + groupHistoryId: Value(uuid.v4()), + ); + } + await into(groupHistories).insert(insertAction); } Future createNewDirectChat( @@ -53,6 +63,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { groupId: Value(groupIdDirectChat), isDirectChat: const Value(true), isGroupAdmin: const Value(true), + joinedGroup: const Value(true), ); final result = await _insertGroup(insertGroup); @@ -138,6 +149,14 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return (select(groups)..where((t) => t.isDirectChat.equals(true))).get(); } + Future> getAllNotJoinedGroups() { + return (select(groups) + ..where( + (t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false), + )) + .get(); + } + Future getDirectChat(int userId) async { final query = ((select(groups)..where((t) => t.isDirectChat.equals(true))).join([ diff --git a/lib/src/database/daos/groups.dao.g.dart b/lib/src/database/daos/groups.dao.g.dart index 3489f8c..4f873ec 100644 --- a/lib/src/database/daos/groups.dao.g.dart +++ b/lib/src/database/daos/groups.dao.g.dart @@ -7,4 +7,5 @@ mixin _$GroupsDaoMixin on DatabaseAccessor { $GroupsTable get groups => attachedDatabase.groups; $ContactsTable get contacts => attachedDatabase.contacts; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; + $GroupHistoriesTable get groupHistories => attachedDatabase.groupHistories; } diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 708622c..e745572 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -1,15 +1,25 @@ import 'package:drift/drift.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; +const int defaultDeleteMessagesAfterMilliseconds = 1000 * 60 * 60 * 24; + @DataClassName('Group') class Groups extends Table { TextColumn get groupId => text()(); - BoolColumn get isGroupAdmin => boolean()(); - BoolColumn get isDirectChat => boolean()(); + BoolColumn get isGroupAdmin => boolean().withDefault(const Constant(false))(); + BoolColumn get isDirectChat => boolean().withDefault(const Constant(false))(); BoolColumn get pinned => boolean().withDefault(const Constant(false))(); BoolColumn get archived => boolean().withDefault(const Constant(false))(); + BoolColumn get joinedGroup => boolean().withDefault(const Constant(false))(); + BoolColumn get leftGroup => boolean().withDefault(const Constant(false))(); + + IntColumn get stateVersionId => integer().withDefault(const Constant(0))(); + + BlobColumn get stateEncryptionKey => blob().nullable()(); + BlobColumn get myGroupPrivateKey => blob().nullable()(); + TextColumn get groupName => text()(); IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))(); @@ -17,8 +27,8 @@ class Groups extends Table { BoolColumn get alsoBestFriend => boolean().withDefault(const Constant(false))(); - IntColumn get deleteMessagesAfterMilliseconds => - integer().withDefault(const Constant(1000 * 60 * 60 * 24))(); + IntColumn get deleteMessagesAfterMilliseconds => integer() + .withDefault(const Constant(defaultDeleteMessagesAfterMilliseconds))(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @@ -39,17 +49,49 @@ class Groups extends Table { Set get primaryKey => {groupId}; } -enum MemberState { invited, accepted, admin } +enum MemberState { normal, admin } @DataClassName('GroupMember') class GroupMembers extends Table { - TextColumn get groupId => text()(); + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); IntColumn get contactId => integer().references(Contacts, #userId)(); TextColumn get memberState => textEnum().nullable()(); + BlobColumn get groupPublicKey => blob().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {groupId, contactId}; } + +enum GroupActionType { + createdGroup, + removedMember, + addMember, + leftGroup, + promoteToAdmin, + demoteToMember, + updatedGroupName, +} + +@DataClassName('GroupHistory') +class GroupHistories extends Table { + TextColumn get groupHistoryId => text()(); + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); + + IntColumn get affectedContactId => + integer().nullable().references(Contacts, #userId)(); + + TextColumn get oldGroupName => text().nullable()(); + TextColumn get newGroupName => text().nullable()(); + + TextColumn get type => textEnum()(); + + DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupHistoryId}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index b5681f2..6b02b5c 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -44,6 +44,7 @@ part 'twonly.db.g.dart'; SignalContactPreKeys, SignalContactSignedPreKeys, MessageActions, + GroupHistories ], daos: [ MessagesDao, diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 57b9870..76dd93a 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -671,18 +671,20 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { late final GeneratedColumn isGroupAdmin = GeneratedColumn( 'is_group_admin', aliasedName, false, type: DriftSqlType.bool, - requiredDuringInsert: true, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_group_admin" IN (0, 1))')); + 'CHECK ("is_group_admin" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _isDirectChatMeta = const VerificationMeta('isDirectChat'); @override late final GeneratedColumn isDirectChat = GeneratedColumn( 'is_direct_chat', aliasedName, false, type: DriftSqlType.bool, - requiredDuringInsert: true, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_direct_chat" IN (0, 1))')); + 'CHECK ("is_direct_chat" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); @override late final GeneratedColumn pinned = GeneratedColumn( @@ -702,6 +704,46 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _joinedGroupMeta = + const VerificationMeta('joinedGroup'); + @override + late final GeneratedColumn joinedGroup = GeneratedColumn( + 'joined_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("joined_group" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _leftGroupMeta = + const VerificationMeta('leftGroup'); + @override + late final GeneratedColumn leftGroup = GeneratedColumn( + 'left_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _stateVersionIdMeta = + const VerificationMeta('stateVersionId'); + @override + late final GeneratedColumn stateVersionId = GeneratedColumn( + 'state_version_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _stateEncryptionKeyMeta = + const VerificationMeta('stateEncryptionKey'); + @override + late final GeneratedColumn stateEncryptionKey = + GeneratedColumn('state_encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _myGroupPrivateKeyMeta = + const VerificationMeta('myGroupPrivateKey'); + @override + late final GeneratedColumn myGroupPrivateKey = + GeneratedColumn('my_group_private_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _groupNameMeta = const VerificationMeta('groupName'); @override @@ -734,7 +776,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { 'delete_messages_after_milliseconds', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(1000 * 60 * 60 * 24)); + defaultValue: const Constant(defaultDeleteMessagesAfterMilliseconds)); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -804,6 +846,11 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { isDirectChat, pinned, archived, + joinedGroup, + leftGroup, + stateVersionId, + stateEncryptionKey, + myGroupPrivateKey, groupName, totalMediaCounter, alsoBestFriend, @@ -839,16 +886,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { _isGroupAdminMeta, isGroupAdmin.isAcceptableOrUnknown( data['is_group_admin']!, _isGroupAdminMeta)); - } else if (isInserting) { - context.missing(_isGroupAdminMeta); } if (data.containsKey('is_direct_chat')) { context.handle( _isDirectChatMeta, isDirectChat.isAcceptableOrUnknown( data['is_direct_chat']!, _isDirectChatMeta)); - } else if (isInserting) { - context.missing(_isDirectChatMeta); } if (data.containsKey('pinned')) { context.handle(_pinnedMeta, @@ -858,6 +901,34 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { context.handle(_archivedMeta, archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); } + if (data.containsKey('joined_group')) { + context.handle( + _joinedGroupMeta, + joinedGroup.isAcceptableOrUnknown( + data['joined_group']!, _joinedGroupMeta)); + } + if (data.containsKey('left_group')) { + context.handle(_leftGroupMeta, + leftGroup.isAcceptableOrUnknown(data['left_group']!, _leftGroupMeta)); + } + if (data.containsKey('state_version_id')) { + context.handle( + _stateVersionIdMeta, + stateVersionId.isAcceptableOrUnknown( + data['state_version_id']!, _stateVersionIdMeta)); + } + if (data.containsKey('state_encryption_key')) { + context.handle( + _stateEncryptionKeyMeta, + stateEncryptionKey.isAcceptableOrUnknown( + data['state_encryption_key']!, _stateEncryptionKeyMeta)); + } + if (data.containsKey('my_group_private_key')) { + context.handle( + _myGroupPrivateKeyMeta, + myGroupPrivateKey.isAcceptableOrUnknown( + data['my_group_private_key']!, _myGroupPrivateKeyMeta)); + } if (data.containsKey('group_name')) { context.handle(_groupNameMeta, groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta)); @@ -954,6 +1025,16 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, archived: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + joinedGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!, + leftGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!, + stateVersionId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!, + stateEncryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}state_encryption_key']), + myGroupPrivateKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}my_group_private_key']), groupName: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, totalMediaCounter: attachedDatabase.typeMapping.read( @@ -1000,6 +1081,11 @@ class Group extends DataClass implements Insertable { final bool isDirectChat; final bool pinned; final bool archived; + final bool joinedGroup; + final bool leftGroup; + final int stateVersionId; + final Uint8List? stateEncryptionKey; + final Uint8List? myGroupPrivateKey; final String groupName; final int totalMediaCounter; final bool alsoBestFriend; @@ -1019,6 +1105,11 @@ class Group extends DataClass implements Insertable { required this.isDirectChat, required this.pinned, required this.archived, + required this.joinedGroup, + required this.leftGroup, + required this.stateVersionId, + this.stateEncryptionKey, + this.myGroupPrivateKey, required this.groupName, required this.totalMediaCounter, required this.alsoBestFriend, @@ -1040,6 +1131,15 @@ class Group extends DataClass implements Insertable { map['is_direct_chat'] = Variable(isDirectChat); map['pinned'] = Variable(pinned); map['archived'] = Variable(archived); + map['joined_group'] = Variable(joinedGroup); + map['left_group'] = Variable(leftGroup); + map['state_version_id'] = Variable(stateVersionId); + if (!nullToAbsent || stateEncryptionKey != null) { + map['state_encryption_key'] = Variable(stateEncryptionKey); + } + if (!nullToAbsent || myGroupPrivateKey != null) { + map['my_group_private_key'] = Variable(myGroupPrivateKey); + } map['group_name'] = Variable(groupName); map['total_media_counter'] = Variable(totalMediaCounter); map['also_best_friend'] = Variable(alsoBestFriend); @@ -1075,6 +1175,15 @@ class Group extends DataClass implements Insertable { isDirectChat: Value(isDirectChat), pinned: Value(pinned), archived: Value(archived), + joinedGroup: Value(joinedGroup), + leftGroup: Value(leftGroup), + stateVersionId: Value(stateVersionId), + stateEncryptionKey: stateEncryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(stateEncryptionKey), + myGroupPrivateKey: myGroupPrivateKey == null && nullToAbsent + ? const Value.absent() + : Value(myGroupPrivateKey), groupName: Value(groupName), totalMediaCounter: Value(totalMediaCounter), alsoBestFriend: Value(alsoBestFriend), @@ -1110,6 +1219,13 @@ class Group extends DataClass implements Insertable { isDirectChat: serializer.fromJson(json['isDirectChat']), pinned: serializer.fromJson(json['pinned']), archived: serializer.fromJson(json['archived']), + joinedGroup: serializer.fromJson(json['joinedGroup']), + leftGroup: serializer.fromJson(json['leftGroup']), + stateVersionId: serializer.fromJson(json['stateVersionId']), + stateEncryptionKey: + serializer.fromJson(json['stateEncryptionKey']), + myGroupPrivateKey: + serializer.fromJson(json['myGroupPrivateKey']), groupName: serializer.fromJson(json['groupName']), totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), @@ -1139,6 +1255,11 @@ class Group extends DataClass implements Insertable { 'isDirectChat': serializer.toJson(isDirectChat), 'pinned': serializer.toJson(pinned), 'archived': serializer.toJson(archived), + 'joinedGroup': serializer.toJson(joinedGroup), + 'leftGroup': serializer.toJson(leftGroup), + 'stateVersionId': serializer.toJson(stateVersionId), + 'stateEncryptionKey': serializer.toJson(stateEncryptionKey), + 'myGroupPrivateKey': serializer.toJson(myGroupPrivateKey), 'groupName': serializer.toJson(groupName), 'totalMediaCounter': serializer.toJson(totalMediaCounter), 'alsoBestFriend': serializer.toJson(alsoBestFriend), @@ -1163,6 +1284,11 @@ class Group extends DataClass implements Insertable { bool? isDirectChat, bool? pinned, bool? archived, + bool? joinedGroup, + bool? leftGroup, + int? stateVersionId, + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), String? groupName, int? totalMediaCounter, bool? alsoBestFriend, @@ -1182,6 +1308,15 @@ class Group extends DataClass implements Insertable { isDirectChat: isDirectChat ?? this.isDirectChat, pinned: pinned ?? this.pinned, archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey.present + ? stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey.present + ? myGroupPrivateKey.value + : this.myGroupPrivateKey, groupName: groupName ?? this.groupName, totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, @@ -1217,6 +1352,18 @@ class Group extends DataClass implements Insertable { : this.isDirectChat, pinned: data.pinned.present ? data.pinned.value : this.pinned, archived: data.archived.present ? data.archived.value : this.archived, + joinedGroup: + data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup, + leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + stateVersionId: data.stateVersionId.present + ? data.stateVersionId.value + : this.stateVersionId, + stateEncryptionKey: data.stateEncryptionKey.present + ? data.stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: data.myGroupPrivateKey.present + ? data.myGroupPrivateKey.value + : this.myGroupPrivateKey, groupName: data.groupName.present ? data.groupName.value : this.groupName, totalMediaCounter: data.totalMediaCounter.present ? data.totalMediaCounter.value @@ -1264,6 +1411,11 @@ class Group extends DataClass implements Insertable { ..write('isDirectChat: $isDirectChat, ') ..write('pinned: $pinned, ') ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') ..write('groupName: $groupName, ') ..write('totalMediaCounter: $totalMediaCounter, ') ..write('alsoBestFriend: $alsoBestFriend, ') @@ -1283,25 +1435,31 @@ class Group extends DataClass implements Insertable { } @override - int get hashCode => Object.hash( - groupId, - isGroupAdmin, - isDirectChat, - pinned, - archived, - groupName, - totalMediaCounter, - alsoBestFriend, - deleteMessagesAfterMilliseconds, - createdAt, - lastMessageSend, - lastMessageReceived, - lastFlameCounterChange, - lastFlameSync, - flameCounter, - maxFlameCounter, - maxFlameCounterFrom, - lastMessageExchange); + int get hashCode => Object.hashAll([ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + stateVersionId, + $driftBlobEquality.hash(stateEncryptionKey), + $driftBlobEquality.hash(myGroupPrivateKey), + groupName, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -1311,6 +1469,13 @@ class Group extends DataClass implements Insertable { other.isDirectChat == this.isDirectChat && other.pinned == this.pinned && other.archived == this.archived && + other.joinedGroup == this.joinedGroup && + other.leftGroup == this.leftGroup && + other.stateVersionId == this.stateVersionId && + $driftBlobEquality.equals( + other.stateEncryptionKey, this.stateEncryptionKey) && + $driftBlobEquality.equals( + other.myGroupPrivateKey, this.myGroupPrivateKey) && other.groupName == this.groupName && other.totalMediaCounter == this.totalMediaCounter && other.alsoBestFriend == this.alsoBestFriend && @@ -1333,6 +1498,11 @@ class GroupsCompanion extends UpdateCompanion { final Value isDirectChat; final Value pinned; final Value archived; + final Value joinedGroup; + final Value leftGroup; + final Value stateVersionId; + final Value stateEncryptionKey; + final Value myGroupPrivateKey; final Value groupName; final Value totalMediaCounter; final Value alsoBestFriend; @@ -1353,6 +1523,11 @@ class GroupsCompanion extends UpdateCompanion { this.isDirectChat = const Value.absent(), this.pinned = const Value.absent(), this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), this.groupName = const Value.absent(), this.totalMediaCounter = const Value.absent(), this.alsoBestFriend = const Value.absent(), @@ -1370,10 +1545,15 @@ class GroupsCompanion extends UpdateCompanion { }); GroupsCompanion.insert({ required String groupId, - required bool isGroupAdmin, - required bool isDirectChat, + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), this.pinned = const Value.absent(), this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), required String groupName, this.totalMediaCounter = const Value.absent(), this.alsoBestFriend = const Value.absent(), @@ -1389,8 +1569,6 @@ class GroupsCompanion extends UpdateCompanion { this.lastMessageExchange = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), - isGroupAdmin = Value(isGroupAdmin), - isDirectChat = Value(isDirectChat), groupName = Value(groupName); static Insertable custom({ Expression? groupId, @@ -1398,6 +1576,11 @@ class GroupsCompanion extends UpdateCompanion { Expression? isDirectChat, Expression? pinned, Expression? archived, + Expression? joinedGroup, + Expression? leftGroup, + Expression? stateVersionId, + Expression? stateEncryptionKey, + Expression? myGroupPrivateKey, Expression? groupName, Expression? totalMediaCounter, Expression? alsoBestFriend, @@ -1419,6 +1602,12 @@ class GroupsCompanion extends UpdateCompanion { if (isDirectChat != null) 'is_direct_chat': isDirectChat, if (pinned != null) 'pinned': pinned, if (archived != null) 'archived': archived, + if (joinedGroup != null) 'joined_group': joinedGroup, + if (leftGroup != null) 'left_group': leftGroup, + if (stateVersionId != null) 'state_version_id': stateVersionId, + if (stateEncryptionKey != null) + 'state_encryption_key': stateEncryptionKey, + if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey, if (groupName != null) 'group_name': groupName, if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, @@ -1447,6 +1636,11 @@ class GroupsCompanion extends UpdateCompanion { Value? isDirectChat, Value? pinned, Value? archived, + Value? joinedGroup, + Value? leftGroup, + Value? stateVersionId, + Value? stateEncryptionKey, + Value? myGroupPrivateKey, Value? groupName, Value? totalMediaCounter, Value? alsoBestFriend, @@ -1467,6 +1661,11 @@ class GroupsCompanion extends UpdateCompanion { isDirectChat: isDirectChat ?? this.isDirectChat, pinned: pinned ?? this.pinned, archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, groupName: groupName ?? this.groupName, totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, @@ -1504,6 +1703,23 @@ class GroupsCompanion extends UpdateCompanion { if (archived.present) { map['archived'] = Variable(archived.value); } + if (joinedGroup.present) { + map['joined_group'] = Variable(joinedGroup.value); + } + if (leftGroup.present) { + map['left_group'] = Variable(leftGroup.value); + } + if (stateVersionId.present) { + map['state_version_id'] = Variable(stateVersionId.value); + } + if (stateEncryptionKey.present) { + map['state_encryption_key'] = + Variable(stateEncryptionKey.value); + } + if (myGroupPrivateKey.present) { + map['my_group_private_key'] = + Variable(myGroupPrivateKey.value); + } if (groupName.present) { map['group_name'] = Variable(groupName.value); } @@ -1562,6 +1778,11 @@ class GroupsCompanion extends UpdateCompanion { ..write('isDirectChat: $isDirectChat, ') ..write('pinned: $pinned, ') ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') ..write('groupName: $groupName, ') ..write('totalMediaCounter: $totalMediaCounter, ') ..write('alsoBestFriend: $alsoBestFriend, ') @@ -3741,7 +3962,10 @@ class $GroupMembersTable extends GroupMembers @override late final GeneratedColumn groupId = GeneratedColumn( 'group_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); static const VerificationMeta _contactIdMeta = const VerificationMeta('contactId'); @override @@ -3757,6 +3981,12 @@ class $GroupMembersTable extends GroupMembers type: DriftSqlType.string, requiredDuringInsert: false) .withConverter( $GroupMembersTable.$convertermemberStaten); + static const VerificationMeta _groupPublicKeyMeta = + const VerificationMeta('groupPublicKey'); + @override + late final GeneratedColumn groupPublicKey = + GeneratedColumn('group_public_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -3767,7 +3997,7 @@ class $GroupMembersTable extends GroupMembers defaultValue: currentDateAndTime); @override List get $columns => - [groupId, contactId, memberState, createdAt]; + [groupId, contactId, memberState, groupPublicKey, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -3790,6 +4020,12 @@ class $GroupMembersTable extends GroupMembers } else if (isInserting) { context.missing(_contactIdMeta); } + if (data.containsKey('group_public_key')) { + context.handle( + _groupPublicKeyMeta, + groupPublicKey.isAcceptableOrUnknown( + data['group_public_key']!, _groupPublicKeyMeta)); + } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); @@ -3810,6 +4046,8 @@ class $GroupMembersTable extends GroupMembers memberState: $GroupMembersTable.$convertermemberStaten.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}member_state'])), + groupPublicKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}group_public_key']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); @@ -3831,11 +4069,13 @@ class GroupMember extends DataClass implements Insertable { final String groupId; final int contactId; final MemberState? memberState; + final Uint8List? groupPublicKey; final DateTime createdAt; const GroupMember( {required this.groupId, required this.contactId, this.memberState, + this.groupPublicKey, required this.createdAt}); @override Map toColumns(bool nullToAbsent) { @@ -3846,6 +4086,9 @@ class GroupMember extends DataClass implements Insertable { map['member_state'] = Variable( $GroupMembersTable.$convertermemberStaten.toSql(memberState)); } + if (!nullToAbsent || groupPublicKey != null) { + map['group_public_key'] = Variable(groupPublicKey); + } map['created_at'] = Variable(createdAt); return map; } @@ -3857,6 +4100,9 @@ class GroupMember extends DataClass implements Insertable { memberState: memberState == null && nullToAbsent ? const Value.absent() : Value(memberState), + groupPublicKey: groupPublicKey == null && nullToAbsent + ? const Value.absent() + : Value(groupPublicKey), createdAt: Value(createdAt), ); } @@ -3869,6 +4115,7 @@ class GroupMember extends DataClass implements Insertable { contactId: serializer.fromJson(json['contactId']), memberState: $GroupMembersTable.$convertermemberStaten .fromJson(serializer.fromJson(json['memberState'])), + groupPublicKey: serializer.fromJson(json['groupPublicKey']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -3880,6 +4127,7 @@ class GroupMember extends DataClass implements Insertable { 'contactId': serializer.toJson(contactId), 'memberState': serializer.toJson( $GroupMembersTable.$convertermemberStaten.toJson(memberState)), + 'groupPublicKey': serializer.toJson(groupPublicKey), 'createdAt': serializer.toJson(createdAt), }; } @@ -3888,11 +4136,14 @@ class GroupMember extends DataClass implements Insertable { {String? groupId, int? contactId, Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), DateTime? createdAt}) => GroupMember( groupId: groupId ?? this.groupId, contactId: contactId ?? this.contactId, memberState: memberState.present ? memberState.value : this.memberState, + groupPublicKey: + groupPublicKey.present ? groupPublicKey.value : this.groupPublicKey, createdAt: createdAt ?? this.createdAt, ); GroupMember copyWithCompanion(GroupMembersCompanion data) { @@ -3901,6 +4152,9 @@ class GroupMember extends DataClass implements Insertable { contactId: data.contactId.present ? data.contactId.value : this.contactId, memberState: data.memberState.present ? data.memberState.value : this.memberState, + groupPublicKey: data.groupPublicKey.present + ? data.groupPublicKey.value + : this.groupPublicKey, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -3911,13 +4165,15 @@ class GroupMember extends DataClass implements Insertable { ..write('groupId: $groupId, ') ..write('contactId: $contactId, ') ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') ..write('createdAt: $createdAt') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(groupId, contactId, memberState, createdAt); + int get hashCode => Object.hash(groupId, contactId, memberState, + $driftBlobEquality.hash(groupPublicKey), createdAt); @override bool operator ==(Object other) => identical(this, other) || @@ -3925,6 +4181,8 @@ class GroupMember extends DataClass implements Insertable { other.groupId == this.groupId && other.contactId == this.contactId && other.memberState == this.memberState && + $driftBlobEquality.equals( + other.groupPublicKey, this.groupPublicKey) && other.createdAt == this.createdAt); } @@ -3932,12 +4190,14 @@ class GroupMembersCompanion extends UpdateCompanion { final Value groupId; final Value contactId; final Value memberState; + final Value groupPublicKey; final Value createdAt; final Value rowid; const GroupMembersCompanion({ this.groupId = const Value.absent(), this.contactId = const Value.absent(), this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }); @@ -3945,6 +4205,7 @@ class GroupMembersCompanion extends UpdateCompanion { required String groupId, required int contactId, this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), @@ -3953,6 +4214,7 @@ class GroupMembersCompanion extends UpdateCompanion { Expression? groupId, Expression? contactId, Expression? memberState, + Expression? groupPublicKey, Expression? createdAt, Expression? rowid, }) { @@ -3960,6 +4222,7 @@ class GroupMembersCompanion extends UpdateCompanion { if (groupId != null) 'group_id': groupId, if (contactId != null) 'contact_id': contactId, if (memberState != null) 'member_state': memberState, + if (groupPublicKey != null) 'group_public_key': groupPublicKey, if (createdAt != null) 'created_at': createdAt, if (rowid != null) 'rowid': rowid, }); @@ -3969,12 +4232,14 @@ class GroupMembersCompanion extends UpdateCompanion { {Value? groupId, Value? contactId, Value? memberState, + Value? groupPublicKey, Value? createdAt, Value? rowid}) { return GroupMembersCompanion( groupId: groupId ?? this.groupId, contactId: contactId ?? this.contactId, memberState: memberState ?? this.memberState, + groupPublicKey: groupPublicKey ?? this.groupPublicKey, createdAt: createdAt ?? this.createdAt, rowid: rowid ?? this.rowid, ); @@ -3993,6 +4258,9 @@ class GroupMembersCompanion extends UpdateCompanion { map['member_state'] = Variable( $GroupMembersTable.$convertermemberStaten.toSql(memberState.value)); } + if (groupPublicKey.present) { + map['group_public_key'] = Variable(groupPublicKey.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -4008,6 +4276,7 @@ class GroupMembersCompanion extends UpdateCompanion { ..write('groupId: $groupId, ') ..write('contactId: $contactId, ') ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') ..write('createdAt: $createdAt, ') ..write('rowid: $rowid') ..write(')')) @@ -6590,6 +6859,430 @@ class MessageActionsCompanion extends UpdateCompanion { } } +class $GroupHistoriesTable extends GroupHistories + with TableInfo<$GroupHistoriesTable, GroupHistory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupHistoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupHistoryIdMeta = + const VerificationMeta('groupHistoryId'); + @override + late final GeneratedColumn groupHistoryId = GeneratedColumn( + 'group_history_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + static const VerificationMeta _affectedContactIdMeta = + const VerificationMeta('affectedContactId'); + @override + late final GeneratedColumn affectedContactId = GeneratedColumn( + 'affected_contact_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + static const VerificationMeta _oldGroupNameMeta = + const VerificationMeta('oldGroupName'); + @override + late final GeneratedColumn oldGroupName = GeneratedColumn( + 'old_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _newGroupNameMeta = + const VerificationMeta('newGroupName'); + @override + late final GeneratedColumn newGroupName = GeneratedColumn( + 'new_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($GroupHistoriesTable.$convertertype); + static const VerificationMeta _actionAtMeta = + const VerificationMeta('actionAt'); + @override + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupHistoryId, + groupId, + affectedContactId, + oldGroupName, + newGroupName, + type, + actionAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_histories'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_history_id')) { + context.handle( + _groupHistoryIdMeta, + groupHistoryId.isAcceptableOrUnknown( + data['group_history_id']!, _groupHistoryIdMeta)); + } else if (isInserting) { + context.missing(_groupHistoryIdMeta); + } + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('affected_contact_id')) { + context.handle( + _affectedContactIdMeta, + affectedContactId.isAcceptableOrUnknown( + data['affected_contact_id']!, _affectedContactIdMeta)); + } + if (data.containsKey('old_group_name')) { + context.handle( + _oldGroupNameMeta, + oldGroupName.isAcceptableOrUnknown( + data['old_group_name']!, _oldGroupNameMeta)); + } + if (data.containsKey('new_group_name')) { + context.handle( + _newGroupNameMeta, + newGroupName.isAcceptableOrUnknown( + data['new_group_name']!, _newGroupNameMeta)); + } + if (data.containsKey('action_at')) { + context.handle(_actionAtMeta, + actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupHistoryId}; + @override + GroupHistory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupHistory( + groupHistoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}group_history_id'])!, + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + affectedContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}affected_contact_id']), + oldGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}old_group_name']), + newGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}new_group_name']), + type: $GroupHistoriesTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + $GroupHistoriesTable createAlias(String alias) { + return $GroupHistoriesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(GroupActionType.values); +} + +class GroupHistory extends DataClass implements Insertable { + final String groupHistoryId; + final String groupId; + final int? affectedContactId; + final String? oldGroupName; + final String? newGroupName; + final GroupActionType type; + final DateTime actionAt; + const GroupHistory( + {required this.groupHistoryId, + required this.groupId, + this.affectedContactId, + this.oldGroupName, + this.newGroupName, + required this.type, + required this.actionAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_history_id'] = Variable(groupHistoryId); + map['group_id'] = Variable(groupId); + if (!nullToAbsent || affectedContactId != null) { + map['affected_contact_id'] = Variable(affectedContactId); + } + if (!nullToAbsent || oldGroupName != null) { + map['old_group_name'] = Variable(oldGroupName); + } + if (!nullToAbsent || newGroupName != null) { + map['new_group_name'] = Variable(newGroupName); + } + { + map['type'] = + Variable($GroupHistoriesTable.$convertertype.toSql(type)); + } + map['action_at'] = Variable(actionAt); + return map; + } + + GroupHistoriesCompanion toCompanion(bool nullToAbsent) { + return GroupHistoriesCompanion( + groupHistoryId: Value(groupHistoryId), + groupId: Value(groupId), + affectedContactId: affectedContactId == null && nullToAbsent + ? const Value.absent() + : Value(affectedContactId), + oldGroupName: oldGroupName == null && nullToAbsent + ? const Value.absent() + : Value(oldGroupName), + newGroupName: newGroupName == null && nullToAbsent + ? const Value.absent() + : Value(newGroupName), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory GroupHistory.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupHistory( + groupHistoryId: serializer.fromJson(json['groupHistoryId']), + groupId: serializer.fromJson(json['groupId']), + affectedContactId: serializer.fromJson(json['affectedContactId']), + oldGroupName: serializer.fromJson(json['oldGroupName']), + newGroupName: serializer.fromJson(json['newGroupName']), + type: $GroupHistoriesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupHistoryId': serializer.toJson(groupHistoryId), + 'groupId': serializer.toJson(groupId), + 'affectedContactId': serializer.toJson(affectedContactId), + 'oldGroupName': serializer.toJson(oldGroupName), + 'newGroupName': serializer.toJson(newGroupName), + 'type': serializer + .toJson($GroupHistoriesTable.$convertertype.toJson(type)), + 'actionAt': serializer.toJson(actionAt), + }; + } + + GroupHistory copyWith( + {String? groupHistoryId, + String? groupId, + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + GroupActionType? type, + DateTime? actionAt}) => + GroupHistory( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + affectedContactId: affectedContactId.present + ? affectedContactId.value + : this.affectedContactId, + oldGroupName: + oldGroupName.present ? oldGroupName.value : this.oldGroupName, + newGroupName: + newGroupName.present ? newGroupName.value : this.newGroupName, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + GroupHistory copyWithCompanion(GroupHistoriesCompanion data) { + return GroupHistory( + groupHistoryId: data.groupHistoryId.present + ? data.groupHistoryId.value + : this.groupHistoryId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + affectedContactId: data.affectedContactId.present + ? data.affectedContactId.value + : this.affectedContactId, + oldGroupName: data.oldGroupName.present + ? data.oldGroupName.value + : this.oldGroupName, + newGroupName: data.newGroupName.present + ? data.newGroupName.value + : this.newGroupName, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupHistory(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupHistoryId, groupId, affectedContactId, + oldGroupName, newGroupName, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupHistory && + other.groupHistoryId == this.groupHistoryId && + other.groupId == this.groupId && + other.affectedContactId == this.affectedContactId && + other.oldGroupName == this.oldGroupName && + other.newGroupName == this.newGroupName && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class GroupHistoriesCompanion extends UpdateCompanion { + final Value groupHistoryId; + final Value groupId; + final Value affectedContactId; + final Value oldGroupName; + final Value newGroupName; + final Value type; + final Value actionAt; + final Value rowid; + const GroupHistoriesCompanion({ + this.groupHistoryId = const Value.absent(), + this.groupId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupHistoriesCompanion.insert({ + required String groupHistoryId, + required String groupId, + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + required GroupActionType type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupHistoryId = Value(groupHistoryId), + groupId = Value(groupId), + type = Value(type); + static Insertable custom({ + Expression? groupHistoryId, + Expression? groupId, + Expression? affectedContactId, + Expression? oldGroupName, + Expression? newGroupName, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupHistoryId != null) 'group_history_id': groupHistoryId, + if (groupId != null) 'group_id': groupId, + if (affectedContactId != null) 'affected_contact_id': affectedContactId, + if (oldGroupName != null) 'old_group_name': oldGroupName, + if (newGroupName != null) 'new_group_name': newGroupName, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupHistoriesCompanion copyWith( + {Value? groupHistoryId, + Value? groupId, + Value? affectedContactId, + Value? oldGroupName, + Value? newGroupName, + Value? type, + Value? actionAt, + Value? rowid}) { + return GroupHistoriesCompanion( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + affectedContactId: affectedContactId ?? this.affectedContactId, + oldGroupName: oldGroupName ?? this.oldGroupName, + newGroupName: newGroupName ?? this.newGroupName, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupHistoryId.present) { + map['group_history_id'] = Variable(groupHistoryId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (affectedContactId.present) { + map['affected_contact_id'] = Variable(affectedContactId.value); + } + if (oldGroupName.present) { + map['old_group_name'] = Variable(oldGroupName.value); + } + if (newGroupName.present) { + map['new_group_name'] = Variable(newGroupName.value); + } + if (type.present) { + map['type'] = Variable( + $GroupHistoriesTable.$convertertype.toSql(type.value)); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesCompanion(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$TwonlyDB extends GeneratedDatabase { _$TwonlyDB(QueryExecutor e) : super(e); $TwonlyDBManager get managers => $TwonlyDBManager(this); @@ -6617,6 +7310,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { late final $SignalContactSignedPreKeysTable signalContactSignedPreKeys = $SignalContactSignedPreKeysTable(this); late final $MessageActionsTable messageActions = $MessageActionsTable(this); + late final $GroupHistoriesTable groupHistories = $GroupHistoriesTable(this); late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); late final SignalDao signalDao = SignalDao(this as TwonlyDB); @@ -6644,7 +7338,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase { signalSessionStores, signalContactPreKeys, signalContactSignedPreKeys, - messageActions + messageActions, + groupHistories ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( @@ -6684,6 +7379,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('reactions', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('group_members', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('contacts', limitUpdateKind: UpdateKind.delete), @@ -6720,6 +7422,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase { TableUpdate('message_actions', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('group_histories', kind: UpdateKind.delete), + ], + ), ], ); } @@ -6859,6 +7568,22 @@ final class $$ContactsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$GroupHistoriesTable, List> + _groupHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupHistories, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.groupHistories.affectedContactId)); + + $$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs { + final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories) + .filter((f) => f.affectedContactId.userId + .sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$ContactsTableFilterComposer @@ -7041,6 +7766,27 @@ class $$ContactsTableFilterComposer )); return f(composer); } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableFilterComposer f) f) { + final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.affectedContactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableFilterComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableOrderingComposer @@ -7274,6 +8020,27 @@ class $$ContactsTableAnnotationComposer )); return f(composer); } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableAnnotationComposer a) f) { + final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.affectedContactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableTableManager extends RootTableManager< @@ -7293,7 +8060,8 @@ class $$ContactsTableTableManager extends RootTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs})> { + bool signalContactSignedPreKeysRefs, + bool groupHistoriesRefs})> { $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) : super(TableManagerState( db: db, @@ -7374,7 +8142,8 @@ class $$ContactsTableTableManager extends RootTableManager< groupMembersRefs = false, receiptsRefs = false, signalContactPreKeysRefs = false, - signalContactSignedPreKeysRefs = false}) { + signalContactSignedPreKeysRefs = false, + groupHistoriesRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ @@ -7384,7 +8153,8 @@ class $$ContactsTableTableManager extends RootTableManager< if (receiptsRefs) db.receipts, if (signalContactPreKeysRefs) db.signalContactPreKeys, if (signalContactSignedPreKeysRefs) - db.signalContactSignedPreKeys + db.signalContactSignedPreKeys, + if (groupHistoriesRefs) db.groupHistories ], addJoins: null, getPrefetchedDataCallback: (items) async { @@ -7464,6 +8234,19 @@ class $$ContactsTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.contactId == item.userId), + typedResults: items), + if (groupHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._groupHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .groupHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.affectedContactId == item.userId), typedResults: items) ]; }, @@ -7489,13 +8272,19 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs})>; + bool signalContactSignedPreKeysRefs, + bool groupHistoriesRefs})>; typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required String groupId, - required bool isGroupAdmin, - required bool isDirectChat, + Value isGroupAdmin, + Value isDirectChat, Value pinned, Value archived, + Value joinedGroup, + Value leftGroup, + Value stateVersionId, + Value stateEncryptionKey, + Value myGroupPrivateKey, required String groupName, Value totalMediaCounter, Value alsoBestFriend, @@ -7517,6 +8306,11 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value isDirectChat, Value pinned, Value archived, + Value joinedGroup, + Value leftGroup, + Value stateVersionId, + Value stateEncryptionKey, + Value myGroupPrivateKey, Value groupName, Value totalMediaCounter, Value alsoBestFriend, @@ -7551,6 +8345,38 @@ final class $$GroupsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$GroupMembersTable, List> + _groupMembersRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.groupMembers, + aliasName: + $_aliasNameGenerator(db.groups.groupId, db.groupMembers.groupId)); + + $$GroupMembersTableProcessedTableManager get groupMembersRefs { + final manager = $$GroupMembersTableTableManager($_db, $_db.groupMembers) + .filter((f) => + f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupMembersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$GroupHistoriesTable, List> + _groupHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupHistories, + aliasName: $_aliasNameGenerator( + db.groups.groupId, db.groupHistories.groupId)); + + $$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs { + final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories) + .filter((f) => + f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { @@ -7576,6 +8402,24 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnFilters(column)); + ColumnFilters get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => ColumnFilters(column)); + + ColumnFilters get leftGroup => $composableBuilder( + column: $table.leftGroup, builder: (column) => ColumnFilters(column)); + + ColumnFilters get stateVersionId => $composableBuilder( + column: $table.stateVersionId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, + builder: (column) => ColumnFilters(column)); + ColumnFilters get groupName => $composableBuilder( column: $table.groupName, builder: (column) => ColumnFilters(column)); @@ -7644,6 +8488,48 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { )); return f(composer); } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableFilterComposer f) f) { + final $$GroupMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableFilterComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableFilterComposer f) f) { + final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableFilterComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { @@ -7671,6 +8557,24 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get leftGroup => $composableBuilder( + column: $table.leftGroup, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stateVersionId => $composableBuilder( + column: $table.stateVersionId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get groupName => $composableBuilder( column: $table.groupName, builder: (column) => ColumnOrderings(column)); @@ -7747,6 +8651,21 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get archived => $composableBuilder(column: $table.archived, builder: (column) => column); + GeneratedColumn get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => column); + + GeneratedColumn get leftGroup => + $composableBuilder(column: $table.leftGroup, builder: (column) => column); + + GeneratedColumn get stateVersionId => $composableBuilder( + column: $table.stateVersionId, builder: (column) => column); + + GeneratedColumn get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, builder: (column) => column); + + GeneratedColumn get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, builder: (column) => column); + GeneratedColumn get groupName => $composableBuilder(column: $table.groupName, builder: (column) => column); @@ -7808,6 +8727,48 @@ class $$GroupsTableAnnotationComposer )); return f(composer); } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableAnnotationComposer a) f) { + final $$GroupMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableAnnotationComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableAnnotationComposer a) f) { + final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$GroupsTableTableManager extends RootTableManager< @@ -7821,7 +8782,8 @@ class $$GroupsTableTableManager extends RootTableManager< $$GroupsTableUpdateCompanionBuilder, (Group, $$GroupsTableReferences), Group, - PrefetchHooks Function({bool messagesRefs})> { + PrefetchHooks Function( + {bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs})> { $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) : super(TableManagerState( db: db, @@ -7838,6 +8800,11 @@ class $$GroupsTableTableManager extends RootTableManager< Value isDirectChat = const Value.absent(), Value pinned = const Value.absent(), Value archived = const Value.absent(), + Value joinedGroup = const Value.absent(), + Value leftGroup = const Value.absent(), + Value stateVersionId = const Value.absent(), + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), Value groupName = const Value.absent(), Value totalMediaCounter = const Value.absent(), Value alsoBestFriend = const Value.absent(), @@ -7859,6 +8826,11 @@ class $$GroupsTableTableManager extends RootTableManager< isDirectChat: isDirectChat, pinned: pinned, archived: archived, + joinedGroup: joinedGroup, + leftGroup: leftGroup, + stateVersionId: stateVersionId, + stateEncryptionKey: stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey, groupName: groupName, totalMediaCounter: totalMediaCounter, alsoBestFriend: alsoBestFriend, @@ -7876,10 +8848,15 @@ class $$GroupsTableTableManager extends RootTableManager< ), createCompanionCallback: ({ required String groupId, - required bool isGroupAdmin, - required bool isDirectChat, + Value isGroupAdmin = const Value.absent(), + Value isDirectChat = const Value.absent(), Value pinned = const Value.absent(), Value archived = const Value.absent(), + Value joinedGroup = const Value.absent(), + Value leftGroup = const Value.absent(), + Value stateVersionId = const Value.absent(), + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), required String groupName, Value totalMediaCounter = const Value.absent(), Value alsoBestFriend = const Value.absent(), @@ -7901,6 +8878,11 @@ class $$GroupsTableTableManager extends RootTableManager< isDirectChat: isDirectChat, pinned: pinned, archived: archived, + joinedGroup: joinedGroup, + leftGroup: leftGroup, + stateVersionId: stateVersionId, + stateEncryptionKey: stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey, groupName: groupName, totalMediaCounter: totalMediaCounter, alsoBestFriend: alsoBestFriend, @@ -7920,10 +8902,17 @@ class $$GroupsTableTableManager extends RootTableManager< .map((e) => (e.readTable(table), $$GroupsTableReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({messagesRefs = false}) { + prefetchHooksCallback: ( + {messagesRefs = false, + groupMembersRefs = false, + groupHistoriesRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (messagesRefs) db.messages], + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (groupMembersRefs) db.groupMembers, + if (groupHistoriesRefs) db.groupHistories + ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ @@ -7937,6 +8926,31 @@ class $$GroupsTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.groupId == item.groupId), + typedResults: items), + if (groupMembersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$GroupsTableReferences._groupMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0) + .groupMembersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), + typedResults: items), + if (groupHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$GroupsTableReferences + ._groupHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0) + .groupHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), typedResults: items) ]; }, @@ -7956,7 +8970,8 @@ typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< $$GroupsTableUpdateCompanionBuilder, (Group, $$GroupsTableReferences), Group, - PrefetchHooks Function({bool messagesRefs})>; + PrefetchHooks Function( + {bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs})>; typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ required String mediaId, required MediaType type, @@ -9887,6 +10902,7 @@ typedef $$GroupMembersTableCreateCompanionBuilder = GroupMembersCompanion required String groupId, required int contactId, Value memberState, + Value groupPublicKey, Value createdAt, Value rowid, }); @@ -9895,6 +10911,7 @@ typedef $$GroupMembersTableUpdateCompanionBuilder = GroupMembersCompanion Value groupId, Value contactId, Value memberState, + Value groupPublicKey, Value createdAt, Value rowid, }); @@ -9903,6 +10920,20 @@ final class $$GroupMembersTableReferences extends BaseReferences<_$TwonlyDB, $GroupMembersTable, GroupMember> { $$GroupMembersTableReferences(super.$_db, super.$_table, super.$_typedResult); + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.groupMembers.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + static $ContactsTable _contactIdTable(_$TwonlyDB db) => db.contacts.createAlias( $_aliasNameGenerator(db.groupMembers.contactId, db.contacts.userId)); @@ -9928,17 +10959,38 @@ class $$GroupMembersTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get memberState => $composableBuilder( column: $table.memberState, builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, + builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableFilterComposer get contactId { final $$ContactsTableFilterComposer composer = $composerBuilder( composer: this, @@ -9969,15 +11021,36 @@ class $$GroupMembersTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get memberState => $composableBuilder( column: $table.memberState, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableOrderingComposer get contactId { final $$ContactsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -10008,16 +11081,36 @@ class $$GroupMembersTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get groupId => - $composableBuilder(column: $table.groupId, builder: (column) => column); - GeneratedColumnWithTypeConverter get memberState => $composableBuilder( column: $table.memberState, builder: (column) => column); + GeneratedColumn get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableAnnotationComposer get contactId { final $$ContactsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -10050,7 +11143,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< $$GroupMembersTableUpdateCompanionBuilder, (GroupMember, $$GroupMembersTableReferences), GroupMember, - PrefetchHooks Function({bool contactId})> { + PrefetchHooks Function({bool groupId, bool contactId})> { $$GroupMembersTableTableManager(_$TwonlyDB db, $GroupMembersTable table) : super(TableManagerState( db: db, @@ -10065,6 +11158,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< Value groupId = const Value.absent(), Value contactId = const Value.absent(), Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -10072,6 +11166,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< groupId: groupId, contactId: contactId, memberState: memberState, + groupPublicKey: groupPublicKey, createdAt: createdAt, rowid: rowid, ), @@ -10079,6 +11174,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< required String groupId, required int contactId, Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -10086,6 +11182,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< groupId: groupId, contactId: contactId, memberState: memberState, + groupPublicKey: groupPublicKey, createdAt: createdAt, rowid: rowid, ), @@ -10095,7 +11192,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< $$GroupMembersTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ({contactId = false}) { + prefetchHooksCallback: ({groupId = false, contactId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -10112,6 +11209,16 @@ class $$GroupMembersTableTableManager extends RootTableManager< dynamic, dynamic, dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$GroupMembersTableReferences._groupIdTable(db), + referencedColumn: + $$GroupMembersTableReferences._groupIdTable(db).groupId, + ) as T; + } if (contactId) { state = state.withJoin( currentTable: table, @@ -10145,7 +11252,7 @@ typedef $$GroupMembersTableProcessedTableManager = ProcessedTableManager< $$GroupMembersTableUpdateCompanionBuilder, (GroupMember, $$GroupMembersTableReferences), GroupMember, - PrefetchHooks Function({bool contactId})>; + PrefetchHooks Function({bool groupId, bool contactId})>; typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({ required String receiptId, required int contactId, @@ -12102,6 +13209,396 @@ typedef $$MessageActionsTableProcessedTableManager = ProcessedTableManager< (MessageAction, $$MessageActionsTableReferences), MessageAction, PrefetchHooks Function({bool messageId})>; +typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion + Function({ + required String groupHistoryId, + required String groupId, + Value affectedContactId, + Value oldGroupName, + Value newGroupName, + required GroupActionType type, + Value actionAt, + Value rowid, +}); +typedef $$GroupHistoriesTableUpdateCompanionBuilder = GroupHistoriesCompanion + Function({ + Value groupHistoryId, + Value groupId, + Value affectedContactId, + Value oldGroupName, + Value newGroupName, + Value type, + Value actionAt, + Value rowid, +}); + +final class $$GroupHistoriesTableReferences + extends BaseReferences<_$TwonlyDB, $GroupHistoriesTable, GroupHistory> { + $$GroupHistoriesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.groupHistories.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _affectedContactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.groupHistories.affectedContactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get affectedContactId { + final $_column = $_itemColumn('affected_contact_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_affectedContactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$GroupHistoriesTableFilterComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get oldGroupName => $composableBuilder( + column: $table.oldGroupName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get newGroupName => $composableBuilder( + column: $table.newGroupName, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnFilters(column)); + + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get affectedContactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableOrderingComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get oldGroupName => $composableBuilder( + column: $table.oldGroupName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get newGroupName => $composableBuilder( + column: $table.newGroupName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnOrderings(column)); + + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get affectedContactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, builder: (column) => column); + + GeneratedColumn get oldGroupName => $composableBuilder( + column: $table.oldGroupName, builder: (column) => column); + + GeneratedColumn get newGroupName => $composableBuilder( + column: $table.newGroupName, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get actionAt => + $composableBuilder(column: $table.actionAt, builder: (column) => column); + + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get affectedContactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupHistoriesTable, + GroupHistory, + $$GroupHistoriesTableFilterComposer, + $$GroupHistoriesTableOrderingComposer, + $$GroupHistoriesTableAnnotationComposer, + $$GroupHistoriesTableCreateCompanionBuilder, + $$GroupHistoriesTableUpdateCompanionBuilder, + (GroupHistory, $$GroupHistoriesTableReferences), + GroupHistory, + PrefetchHooks Function({bool groupId, bool affectedContactId})> { + $$GroupHistoriesTableTableManager(_$TwonlyDB db, $GroupHistoriesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupHistoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupHistoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupHistoriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupHistoryId = const Value.absent(), + Value groupId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value type = const Value.absent(), + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupHistoriesCompanion( + groupHistoryId: groupHistoryId, + groupId: groupId, + affectedContactId: affectedContactId, + oldGroupName: oldGroupName, + newGroupName: newGroupName, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupHistoryId, + required String groupId, + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + required GroupActionType type, + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupHistoriesCompanion.insert( + groupHistoryId: groupHistoryId, + groupId: groupId, + affectedContactId: affectedContactId, + oldGroupName: oldGroupName, + newGroupName: newGroupName, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$GroupHistoriesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ( + {groupId = false, affectedContactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$GroupHistoriesTableReferences._groupIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._groupIdTable(db) + .groupId, + ) as T; + } + if (affectedContactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.affectedContactId, + referencedTable: $$GroupHistoriesTableReferences + ._affectedContactIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._affectedContactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$GroupHistoriesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupHistoriesTable, + GroupHistory, + $$GroupHistoriesTableFilterComposer, + $$GroupHistoriesTableOrderingComposer, + $$GroupHistoriesTableAnnotationComposer, + $$GroupHistoriesTableCreateCompanionBuilder, + $$GroupHistoriesTableUpdateCompanionBuilder, + (GroupHistory, $$GroupHistoriesTableReferences), + GroupHistory, + PrefetchHooks Function({bool groupId, bool affectedContactId})>; class $TwonlyDBManager { final _$TwonlyDB _db; @@ -12141,4 +13638,6 @@ class $TwonlyDBManager { _db, _db.signalContactSignedPreKeys); $$MessageActionsTableTableManager get messageActions => $$MessageActionsTableTableManager(_db, _db.messageActions); + $$GroupHistoriesTableTableManager get groupHistories => + $$GroupHistoriesTableTableManager(_db, _db.groupHistories); } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 4e91f4c..3c84c47 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -359,5 +359,11 @@ "durationShortSecond": "Sek.", "durationShortMinute": "Min.", "durationShortHour": "Std", - "durationShortDays": "Tagen" + "durationShortDays": "Tagen", + "newGroup": "Neue Gruppe", + "selectMembers": "Mitglieder auswählen", + "selectGroupName": "Gruppennamen wählen", + "groupNameInput": "Gruppennamen", + "groupMembers": "Mitglieder", + "createGroup": "Gruppe erstellen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 8b565d0..489cb1c 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -515,5 +515,11 @@ "durationShortSecond": "Sec.", "durationShortMinute": "Min.", "durationShortHour": "Hrs.", - "durationShortDays": "Days" + "durationShortDays": "Days", + "newGroup": "New group", + "selectMembers": "Select members", + "selectGroupName": "Select group name", + "groupNameInput": "Group name", + "groupMembers": "Members", + "createGroup": "Create group" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index dab9c79..3466610 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2197,6 +2197,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Days'** String get durationShortDays; + + /// No description provided for @newGroup. + /// + /// In en, this message translates to: + /// **'New group'** + String get newGroup; + + /// No description provided for @selectMembers. + /// + /// In en, this message translates to: + /// **'Select members'** + String get selectMembers; + + /// No description provided for @selectGroupName. + /// + /// In en, this message translates to: + /// **'Select group name'** + String get selectGroupName; + + /// No description provided for @groupNameInput. + /// + /// In en, this message translates to: + /// **'Group name'** + String get groupNameInput; + + /// No description provided for @groupMembers. + /// + /// In en, this message translates to: + /// **'Members'** + String get groupMembers; + + /// No description provided for @createGroup. + /// + /// In en, this message translates to: + /// **'Create group'** + String get createGroup; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index f372df6..3d16a0d 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1165,4 +1165,22 @@ class AppLocalizationsDe extends AppLocalizations { @override String get durationShortDays => 'Tagen'; + + @override + String get newGroup => 'Neue Gruppe'; + + @override + String get selectMembers => 'Mitglieder auswählen'; + + @override + String get selectGroupName => 'Gruppennamen wählen'; + + @override + String get groupNameInput => 'Gruppennamen'; + + @override + String get groupMembers => 'Mitglieder'; + + @override + String get createGroup => 'Gruppe erstellen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index db0c3cc..1c61b1f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1158,4 +1158,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get durationShortDays => 'Days'; + + @override + String get newGroup => 'New group'; + + @override + String get selectMembers => 'Select members'; + + @override + String get selectGroupName => 'Select group name'; + + @override + String get groupNameInput => 'Group name'; + + @override + String get groupMembers => 'Members'; + + @override + String get createGroup => 'Create group'; } diff --git a/lib/src/model/protobuf/api/http/http_requests.pb.dart b/lib/src/model/protobuf/api/http/http_requests.pb.dart index ad378f0..132252d 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pb.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pb.dart @@ -158,6 +158,350 @@ class UploadRequest extends $pb.GeneratedMessage { $core.List get messagesOnSuccess => $_getList(2); } +class UpdateGroupState_UpdateTBS extends $pb.GeneratedMessage { + factory UpdateGroupState_UpdateTBS({ + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? publicKey, + $core.List<$core.int>? removeAdmin, + $core.List<$core.int>? addAdmin, + $core.List<$core.int>? nonce, + }) { + final $result = create(); + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + if (removeAdmin != null) { + $result.removeAdmin = removeAdmin; + } + if (addAdmin != null) { + $result.addAdmin = addAdmin; + } + if (nonce != null) { + $result.nonce = nonce; + } + return $result; + } + UpdateGroupState_UpdateTBS._() : super(); + factory UpdateGroupState_UpdateTBS.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UpdateGroupState_UpdateTBS.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UpdateGroupState.UpdateTBS', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'removeAdmin', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'addAdmin', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UpdateGroupState_UpdateTBS clone() => UpdateGroupState_UpdateTBS()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UpdateGroupState_UpdateTBS copyWith(void Function(UpdateGroupState_UpdateTBS) updates) => super.copyWith((message) => updates(message as UpdateGroupState_UpdateTBS)) as UpdateGroupState_UpdateTBS; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UpdateGroupState_UpdateTBS create() => UpdateGroupState_UpdateTBS._(); + UpdateGroupState_UpdateTBS createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UpdateGroupState_UpdateTBS getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UpdateGroupState_UpdateTBS? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get versionId => $_getI64(0); + @$pb.TagNumber(1) + set versionId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasVersionId() => $_has(0); + @$pb.TagNumber(1) + void clearVersionId() => clearField(1); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(3) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(3) + void clearEncryptedGroupState() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get publicKey => $_getN(2); + @$pb.TagNumber(4) + set publicKey($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(4) + $core.bool hasPublicKey() => $_has(2); + @$pb.TagNumber(4) + void clearPublicKey() => clearField(4); + + /// public group key + @$pb.TagNumber(5) + $core.List<$core.int> get removeAdmin => $_getN(3); + @$pb.TagNumber(5) + set removeAdmin($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(5) + $core.bool hasRemoveAdmin() => $_has(3); + @$pb.TagNumber(5) + void clearRemoveAdmin() => clearField(5); + + @$pb.TagNumber(6) + $core.List<$core.int> get addAdmin => $_getN(4); + @$pb.TagNumber(6) + set addAdmin($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(6) + $core.bool hasAddAdmin() => $_has(4); + @$pb.TagNumber(6) + void clearAddAdmin() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get nonce => $_getN(5); + @$pb.TagNumber(7) + set nonce($core.List<$core.int> v) { $_setBytes(5, v); } + @$pb.TagNumber(7) + $core.bool hasNonce() => $_has(5); + @$pb.TagNumber(7) + void clearNonce() => clearField(7); +} + +/// plaintext message send to the server +class UpdateGroupState extends $pb.GeneratedMessage { + factory UpdateGroupState({ + UpdateGroupState_UpdateTBS? update, + $core.List<$core.int>? signature, + }) { + final $result = create(); + if (update != null) { + $result.update = update; + } + if (signature != null) { + $result.signature = signature; + } + return $result; + } + UpdateGroupState._() : super(); + factory UpdateGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UpdateGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UpdateGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'update', subBuilder: UpdateGroupState_UpdateTBS.create) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UpdateGroupState clone() => UpdateGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UpdateGroupState copyWith(void Function(UpdateGroupState) updates) => super.copyWith((message) => updates(message as UpdateGroupState)) as UpdateGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UpdateGroupState create() => UpdateGroupState._(); + UpdateGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UpdateGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UpdateGroupState? _defaultInstance; + + @$pb.TagNumber(1) + UpdateGroupState_UpdateTBS get update => $_getN(0); + @$pb.TagNumber(1) + set update(UpdateGroupState_UpdateTBS v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasUpdate() => $_has(0); + @$pb.TagNumber(1) + void clearUpdate() => clearField(1); + @$pb.TagNumber(1) + UpdateGroupState_UpdateTBS ensureUpdate() => $_ensure(0); + + @$pb.TagNumber(2) + $core.List<$core.int> get signature => $_getN(1); + @$pb.TagNumber(2) + set signature($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasSignature() => $_has(1); + @$pb.TagNumber(2) + void clearSignature() => clearField(2); +} + +class NewGroupState extends $pb.GeneratedMessage { + factory NewGroupState({ + $core.String? groupId, + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? publicKey, + }) { + final $result = create(); + if (groupId != null) { + $result.groupId = groupId; + } + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + return $result; + } + NewGroupState._() : super(); + factory NewGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory NewGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'NewGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NewGroupState clone() => NewGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NewGroupState copyWith(void Function(NewGroupState) updates) => super.copyWith((message) => updates(message as NewGroupState)) as NewGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NewGroupState create() => NewGroupState._(); + NewGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NewGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static NewGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get groupId => $_getSZ(0); + @$pb.TagNumber(1) + set groupId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasGroupId() => $_has(0); + @$pb.TagNumber(1) + void clearGroupId() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get versionId => $_getI64(1); + @$pb.TagNumber(2) + set versionId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasVersionId() => $_has(1); + @$pb.TagNumber(2) + void clearVersionId() => clearField(2); + + @$pb.TagNumber(4) + $core.List<$core.int> get encryptedGroupState => $_getN(2); + @$pb.TagNumber(4) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(4) + $core.bool hasEncryptedGroupState() => $_has(2); + @$pb.TagNumber(4) + void clearEncryptedGroupState() => clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get publicKey => $_getN(3); + @$pb.TagNumber(5) + set publicKey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(5) + $core.bool hasPublicKey() => $_has(3); + @$pb.TagNumber(5) + void clearPublicKey() => clearField(5); +} + +class GroupState extends $pb.GeneratedMessage { + factory GroupState({ + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + }) { + final $result = create(); + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + return $result; + } + GroupState._() : super(); + factory GroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GroupState clone() => GroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GroupState copyWith(void Function(GroupState) updates) => super.copyWith((message) => updates(message as GroupState)) as GroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GroupState create() => GroupState._(); + GroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GroupState? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get versionId => $_getI64(0); + @$pb.TagNumber(1) + set versionId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasVersionId() => $_has(0); + @$pb.TagNumber(1) + void clearVersionId() => clearField(1); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(3) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(3) + void clearEncryptedGroupState() => clearField(3); +} + const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart index d4c43f8..be8ef01 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart @@ -48,3 +48,71 @@ final $typed_data.Uint8List uploadRequestDescriptor = $convert.base64Decode( 'c3VjY2VzcxgDIAMoCzIaLmh0dHBfcmVxdWVzdHMuVGV4dE1lc3NhZ2VSEW1lc3NhZ2VzT25TdW' 'NjZXNz'); +@$core.Deprecated('Use updateGroupStateDescriptor instead') +const UpdateGroupState$json = { + '1': 'UpdateGroupState', + '2': [ + {'1': 'update', '3': 1, '4': 1, '5': 11, '6': '.http_requests.UpdateGroupState.UpdateTBS', '10': 'update'}, + {'1': 'signature', '3': 2, '4': 1, '5': 12, '10': 'signature'}, + ], + '3': [UpdateGroupState_UpdateTBS$json], +}; + +@$core.Deprecated('Use updateGroupStateDescriptor instead') +const UpdateGroupState_UpdateTBS$json = { + '1': 'UpdateTBS', + '2': [ + {'1': 'version_id', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'public_key', '3': 4, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'remove_admin', '3': 5, '4': 1, '5': 12, '9': 0, '10': 'removeAdmin', '17': true}, + {'1': 'add_admin', '3': 6, '4': 1, '5': 12, '9': 1, '10': 'addAdmin', '17': true}, + {'1': 'nonce', '3': 7, '4': 1, '5': 12, '10': 'nonce'}, + ], + '8': [ + {'1': '_remove_admin'}, + {'1': '_add_admin'}, + ], +}; + +/// Descriptor for `UpdateGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List updateGroupStateDescriptor = $convert.base64Decode( + 'ChBVcGRhdGVHcm91cFN0YXRlEkEKBnVwZGF0ZRgBIAEoCzIpLmh0dHBfcmVxdWVzdHMuVXBkYX' + 'RlR3JvdXBTdGF0ZS5VcGRhdGVUQlNSBnVwZGF0ZRIcCglzaWduYXR1cmUYAiABKAxSCXNpZ25h' + 'dHVyZRr8AQoJVXBkYXRlVEJTEh0KCnZlcnNpb25faWQYASABKARSCXZlcnNpb25JZBIyChVlbm' + 'NyeXB0ZWRfZ3JvdXBfc3RhdGUYAyABKAxSE2VuY3J5cHRlZEdyb3VwU3RhdGUSHQoKcHVibGlj' + 'X2tleRgEIAEoDFIJcHVibGljS2V5EiYKDHJlbW92ZV9hZG1pbhgFIAEoDEgAUgtyZW1vdmVBZG' + '1pbogBARIgCglhZGRfYWRtaW4YBiABKAxIAVIIYWRkQWRtaW6IAQESFAoFbm9uY2UYByABKAxS' + 'BW5vbmNlQg8KDV9yZW1vdmVfYWRtaW5CDAoKX2FkZF9hZG1pbg=='); + +@$core.Deprecated('Use newGroupStateDescriptor instead') +const NewGroupState$json = { + '1': 'NewGroupState', + '2': [ + {'1': 'groupId', '3': 1, '4': 1, '5': 9, '10': 'groupId'}, + {'1': 'versionId', '3': 2, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 4, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'public_key', '3': 5, '4': 1, '5': 12, '10': 'publicKey'}, + ], +}; + +/// Descriptor for `NewGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List newGroupStateDescriptor = $convert.base64Decode( + 'Cg1OZXdHcm91cFN0YXRlEhgKB2dyb3VwSWQYASABKAlSB2dyb3VwSWQSHAoJdmVyc2lvbklkGA' + 'IgASgEUgl2ZXJzaW9uSWQSMgoVZW5jcnlwdGVkX2dyb3VwX3N0YXRlGAQgASgMUhNlbmNyeXB0' + 'ZWRHcm91cFN0YXRlEh0KCnB1YmxpY19rZXkYBSABKAxSCXB1YmxpY0tleQ=='); + +@$core.Deprecated('Use groupStateDescriptor instead') +const GroupState$json = { + '1': 'GroupState', + '2': [ + {'1': 'versionId', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + ], +}; + +/// Descriptor for `GroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List groupStateDescriptor = $convert.base64Decode( + 'CgpHcm91cFN0YXRlEhwKCXZlcnNpb25JZBgBIAEoBFIJdmVyc2lvbklkEjIKFWVuY3J5cHRlZF' + '9ncm91cF9zdGF0ZRgDIAEoDFITZW5jcnlwdGVkR3JvdXBTdGF0ZQ=='); + diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 5d4ff99..2535881 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -206,6 +206,7 @@ class Handshake_Register extends $pb.GeneratedMessage { $fixnum.Int64? signedPrekeyId, $fixnum.Int64? registrationId, $core.bool? isIos, + $core.String? langCode, }) { final $result = create(); if (username != null) { @@ -232,6 +233,9 @@ class Handshake_Register extends $pb.GeneratedMessage { if (isIos != null) { $result.isIos = isIos; } + if (langCode != null) { + $result.langCode = langCode; + } return $result; } Handshake_Register._() : super(); @@ -247,6 +251,7 @@ class Handshake_Register extends $pb.GeneratedMessage { ..aInt64(6, _omitFieldNames ? '' : 'signedPrekeyId') ..aInt64(7, _omitFieldNames ? '' : 'registrationId') ..aOB(8, _omitFieldNames ? '' : 'isIos') + ..aOS(9, _omitFieldNames ? '' : 'langCode') ..hasRequiredFields = false ; @@ -342,6 +347,15 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool hasIsIos() => $_has(7); @$pb.TagNumber(8) void clearIsIos() => clearField(8); + + @$pb.TagNumber(9) + $core.String get langCode => $_getSZ(8); + @$pb.TagNumber(9) + set langCode($core.String v) { $_setString(8, v); } + @$pb.TagNumber(9) + $core.bool hasLangCode() => $_has(8); + @$pb.TagNumber(9) + void clearLangCode() => clearField(9); } class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index 7c05b1b..edb5470 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -77,11 +77,11 @@ const Handshake_Register$json = { {'1': 'signed_prekey_signature', '3': 5, '4': 1, '5': 12, '10': 'signedPrekeySignature'}, {'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '10': 'signedPrekeyId'}, {'1': 'registration_id', '3': 7, '4': 1, '5': 3, '10': 'registrationId'}, - {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '9': 1, '10': 'isIos', '17': true}, + {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, + {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, ], '8': [ {'1': '_invite_code'}, - {'1': '_is_ios'}, ], }; @@ -121,19 +121,19 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'ZW50X3RvX3NlcnZlci5IYW5kc2hha2UuR2V0QXV0aENoYWxsZW5nZUgAUhBnZXRhdXRoY2hhbG' 'xlbmdlEk4KDGdldGF1dGh0b2tlbhgDIAEoCzIoLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtl' 'LkdldEF1dGhUb2tlbkgAUgxnZXRhdXRodG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' - 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRrj' + 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRrw' 'AgoIUmVnaXN0ZXISGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGA' 'IgASgJSABSCmludml0ZUNvZGWIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVi' 'bGljSWRlbnRpdHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3' 'NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQ' 'c2lnbmVkX3ByZWtleV9pZBgGIAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2' - 'lkGAcgASgDUg5yZWdpc3RyYXRpb25JZBIaCgZpc19pb3MYCCABKAhIAVIFaXNJb3OIAQFCDgoM' - 'X2ludml0ZV9jb2RlQgkKB19pc19pb3MaEgoQR2V0QXV0aENoYWxsZW5nZRpDCgxHZXRBdXRoVG' - '9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25z' - 'ZRqsAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2' - 'VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3ZlcnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogB' - 'ARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aWNlSWSIAQFCDgoMX2FwcF92ZXJzaW9uQgwKCl' - '9kZXZpY2VfaWRCCwoJSGFuZHNoYWtl'); + 'lkGAcgASgDUg5yZWdpc3RyYXRpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdf' + 'Y29kZRgJIAEoCVIIbGFuZ0NvZGVCDgoMX2ludml0ZV9jb2RlGhIKEEdldEF1dGhDaGFsbGVuZ2' + 'UaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIaCghyZXNwb25zZRgC' + 'IAEoDFIIcmVzcG9uc2UarAEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW' + 'QSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJzaW9uGAMgASgJSABS' + 'CmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZUlkiAEBQg4KDF9hcH' + 'BfdmVyc2lvbkIMCgpfZGV2aWNlX2lkQgsKCUhhbmRzaGFrZQ=='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { diff --git a/lib/src/model/protobuf/client/generated/groups.pb.dart b/lib/src/model/protobuf/client/generated/groups.pb.dart new file mode 100644 index 0000000..e3d50f5 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pb.dart @@ -0,0 +1,192 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +/// Stored encrypted on the server in the members columns. +class EncryptedGroupState extends $pb.GeneratedMessage { + factory EncryptedGroupState({ + $core.Iterable<$fixnum.Int64>? memberIds, + $core.Iterable<$fixnum.Int64>? adminIds, + $core.String? groupName, + $fixnum.Int64? deleteMessagesAfterMilliseconds, + $core.List<$core.int>? padding, + }) { + final $result = create(); + if (memberIds != null) { + $result.memberIds.addAll(memberIds); + } + if (adminIds != null) { + $result.adminIds.addAll(adminIds); + } + if (groupName != null) { + $result.groupName = groupName; + } + if (deleteMessagesAfterMilliseconds != null) { + $result.deleteMessagesAfterMilliseconds = deleteMessagesAfterMilliseconds; + } + if (padding != null) { + $result.padding = padding; + } + return $result; + } + EncryptedGroupState._() : super(); + factory EncryptedGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedGroupState', createEmptyInstance: create) + ..p<$fixnum.Int64>(1, _omitFieldNames ? '' : 'memberIds', $pb.PbFieldType.K6, protoName: 'memberIds') + ..p<$fixnum.Int64>(2, _omitFieldNames ? '' : 'adminIds', $pb.PbFieldType.K6, protoName: 'adminIds') + ..aOS(3, _omitFieldNames ? '' : 'groupName', protoName: 'groupName') + ..aInt64(4, _omitFieldNames ? '' : 'deleteMessagesAfterMilliseconds', protoName: 'deleteMessagesAfterMilliseconds') + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'padding', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedGroupState clone() => EncryptedGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedGroupState copyWith(void Function(EncryptedGroupState) updates) => super.copyWith((message) => updates(message as EncryptedGroupState)) as EncryptedGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedGroupState create() => EncryptedGroupState._(); + EncryptedGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$fixnum.Int64> get memberIds => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$fixnum.Int64> get adminIds => $_getList(1); + + @$pb.TagNumber(3) + $core.String get groupName => $_getSZ(2); + @$pb.TagNumber(3) + set groupName($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasGroupName() => $_has(2); + @$pb.TagNumber(3) + void clearGroupName() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get deleteMessagesAfterMilliseconds => $_getI64(3); + @$pb.TagNumber(4) + set deleteMessagesAfterMilliseconds($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasDeleteMessagesAfterMilliseconds() => $_has(3); + @$pb.TagNumber(4) + void clearDeleteMessagesAfterMilliseconds() => clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get padding => $_getN(4); + @$pb.TagNumber(5) + set padding($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(5) + $core.bool hasPadding() => $_has(4); + @$pb.TagNumber(5) + void clearPadding() => clearField(5); +} + +class EncryptedGroupStateEnvelop extends $pb.GeneratedMessage { + factory EncryptedGroupStateEnvelop({ + $core.List<$core.int>? nonce, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? mac, + }) { + final $result = create(); + if (nonce != null) { + $result.nonce = nonce; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (mac != null) { + $result.mac = mac; + } + return $result; + } + EncryptedGroupStateEnvelop._() : super(); + factory EncryptedGroupStateEnvelop.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedGroupStateEnvelop.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedGroupStateEnvelop', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY, protoName: 'encryptedGroupState') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedGroupStateEnvelop clone() => EncryptedGroupStateEnvelop()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedGroupStateEnvelop copyWith(void Function(EncryptedGroupStateEnvelop) updates) => super.copyWith((message) => updates(message as EncryptedGroupStateEnvelop)) as EncryptedGroupStateEnvelop; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedGroupStateEnvelop create() => EncryptedGroupStateEnvelop._(); + EncryptedGroupStateEnvelop createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedGroupStateEnvelop getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedGroupStateEnvelop? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get nonce => $_getN(0); + @$pb.TagNumber(1) + set nonce($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(2) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedGroupState() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get mac => $_getN(2); + @$pb.TagNumber(3) + set mac($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasMac() => $_has(2); + @$pb.TagNumber(3) + void clearMac() => clearField(3); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/client/generated/groups.pbenum.dart b/lib/src/model/protobuf/client/generated/groups.pbenum.dart new file mode 100644 index 0000000..c03fb19 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbenum.dart @@ -0,0 +1,11 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + diff --git a/lib/src/model/protobuf/client/generated/groups.pbjson.dart b/lib/src/model/protobuf/client/generated/groups.pbjson.dart new file mode 100644 index 0000000..97fd3f8 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbjson.dart @@ -0,0 +1,54 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use encryptedGroupStateDescriptor instead') +const EncryptedGroupState$json = { + '1': 'EncryptedGroupState', + '2': [ + {'1': 'memberIds', '3': 1, '4': 3, '5': 3, '10': 'memberIds'}, + {'1': 'adminIds', '3': 2, '4': 3, '5': 3, '10': 'adminIds'}, + {'1': 'groupName', '3': 3, '4': 1, '5': 9, '10': 'groupName'}, + {'1': 'deleteMessagesAfterMilliseconds', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'deleteMessagesAfterMilliseconds', '17': true}, + {'1': 'padding', '3': 5, '4': 1, '5': 12, '10': 'padding'}, + ], + '8': [ + {'1': '_deleteMessagesAfterMilliseconds'}, + ], +}; + +/// Descriptor for `EncryptedGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedGroupStateDescriptor = $convert.base64Decode( + 'ChNFbmNyeXB0ZWRHcm91cFN0YXRlEhwKCW1lbWJlcklkcxgBIAMoA1IJbWVtYmVySWRzEhoKCG' + 'FkbWluSWRzGAIgAygDUghhZG1pbklkcxIcCglncm91cE5hbWUYAyABKAlSCWdyb3VwTmFtZRJN' + 'Ch9kZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgASgDSABSH2RlbGV0ZU1lc3NhZ2' + 'VzQWZ0ZXJNaWxsaXNlY29uZHOIAQESGAoHcGFkZGluZxgFIAEoDFIHcGFkZGluZ0IiCiBfZGVs' + 'ZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcw=='); + +@$core.Deprecated('Use encryptedGroupStateEnvelopDescriptor instead') +const EncryptedGroupStateEnvelop$json = { + '1': 'EncryptedGroupStateEnvelop', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 12, '10': 'nonce'}, + {'1': 'encryptedGroupState', '3': 2, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'mac', '3': 3, '4': 1, '5': 12, '10': 'mac'}, + ], +}; + +/// Descriptor for `EncryptedGroupStateEnvelop`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedGroupStateEnvelopDescriptor = $convert.base64Decode( + 'ChpFbmNyeXB0ZWRHcm91cFN0YXRlRW52ZWxvcBIUCgVub25jZRgBIAEoDFIFbm9uY2USMAoTZW' + '5jcnlwdGVkR3JvdXBTdGF0ZRgCIAEoDFITZW5jcnlwdGVkR3JvdXBTdGF0ZRIQCgNtYWMYAyAB' + 'KAxSA21hYw=='); + diff --git a/lib/src/model/protobuf/client/generated/groups.pbserver.dart b/lib/src/model/protobuf/client/generated/groups.pbserver.dart new file mode 100644 index 0000000..087f85c --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'groups.pb.dart'; + diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 6a41b3f..029b405 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -214,6 +214,200 @@ class PlaintextContent extends $pb.GeneratedMessage { PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0); } +class EncryptedContent_GroupCreate extends $pb.GeneratedMessage { + factory EncryptedContent_GroupCreate({ + $core.List<$core.int>? stateKey, + $core.List<$core.int>? groupPublicKey, + }) { + final $result = create(); + if (stateKey != null) { + $result.stateKey = stateKey; + } + if (groupPublicKey != null) { + $result.groupPublicKey = groupPublicKey; + } + return $result; + } + EncryptedContent_GroupCreate._() : super(); + factory EncryptedContent_GroupCreate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupCreate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupCreate', createEmptyInstance: create) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY, protoName: 'stateKey') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupCreate clone() => EncryptedContent_GroupCreate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupCreate copyWith(void Function(EncryptedContent_GroupCreate) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupCreate)) as EncryptedContent_GroupCreate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupCreate create() => EncryptedContent_GroupCreate._(); + EncryptedContent_GroupCreate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupCreate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupCreate? _defaultInstance; + + /// key for the state stored on the server + @$pb.TagNumber(3) + $core.List<$core.int> get stateKey => $_getN(0); + @$pb.TagNumber(3) + set stateKey($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(3) + $core.bool hasStateKey() => $_has(0); + @$pb.TagNumber(3) + void clearStateKey() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get groupPublicKey => $_getN(1); + @$pb.TagNumber(4) + set groupPublicKey($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(4) + $core.bool hasGroupPublicKey() => $_has(1); + @$pb.TagNumber(4) + void clearGroupPublicKey() => clearField(4); +} + +class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { + factory EncryptedContent_GroupJoin({ + $core.List<$core.int>? groupPublicKey, + }) { + final $result = create(); + if (groupPublicKey != null) { + $result.groupPublicKey = groupPublicKey; + } + return $result; + } + EncryptedContent_GroupJoin._() : super(); + factory EncryptedContent_GroupJoin.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupJoin.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupJoin', createEmptyInstance: create) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupJoin clone() => EncryptedContent_GroupJoin()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupJoin copyWith(void Function(EncryptedContent_GroupJoin) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupJoin)) as EncryptedContent_GroupJoin; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupJoin create() => EncryptedContent_GroupJoin._(); + EncryptedContent_GroupJoin createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupJoin getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupJoin? _defaultInstance; + + /// key for the state stored on the server + @$pb.TagNumber(4) + $core.List<$core.int> get groupPublicKey => $_getN(0); + @$pb.TagNumber(4) + set groupPublicKey($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(4) + $core.bool hasGroupPublicKey() => $_has(0); + @$pb.TagNumber(4) + void clearGroupPublicKey() => clearField(4); +} + +class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_GroupUpdate({ + $core.String? groupActionType, + $fixnum.Int64? affectedContactId, + $core.String? newGroupName, + }) { + final $result = create(); + if (groupActionType != null) { + $result.groupActionType = groupActionType; + } + if (affectedContactId != null) { + $result.affectedContactId = affectedContactId; + } + if (newGroupName != null) { + $result.newGroupName = newGroupName; + } + return $result; + } + EncryptedContent_GroupUpdate._() : super(); + factory EncryptedContent_GroupUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupUpdate', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'groupActionType', protoName: 'groupActionType') + ..aInt64(2, _omitFieldNames ? '' : 'affectedContactId', protoName: 'affectedContactId') + ..aOS(3, _omitFieldNames ? '' : 'newGroupName', protoName: 'newGroupName') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupUpdate clone() => EncryptedContent_GroupUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupUpdate copyWith(void Function(EncryptedContent_GroupUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupUpdate)) as EncryptedContent_GroupUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupUpdate create() => EncryptedContent_GroupUpdate._(); + EncryptedContent_GroupUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupUpdate? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get groupActionType => $_getSZ(0); + @$pb.TagNumber(1) + set groupActionType($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasGroupActionType() => $_has(0); + @$pb.TagNumber(1) + void clearGroupActionType() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get affectedContactId => $_getI64(1); + @$pb.TagNumber(2) + set affectedContactId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasAffectedContactId() => $_has(1); + @$pb.TagNumber(2) + void clearAffectedContactId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get newGroupName => $_getSZ(2); + @$pb.TagNumber(3) + set newGroupName($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasNewGroupName() => $_has(2); + @$pb.TagNumber(3) + void clearNewGroupName() => clearField(3); +} + class EncryptedContent_TextMessage extends $pb.GeneratedMessage { factory EncryptedContent_TextMessage({ $core.String? senderMessageId, @@ -1036,6 +1230,9 @@ class EncryptedContent extends $pb.GeneratedMessage { EncryptedContent_PushKeys? pushKeys, EncryptedContent_Reaction? reaction, EncryptedContent_TextMessage? textMessage, + EncryptedContent_GroupCreate? groupCreate, + EncryptedContent_GroupJoin? groupJoin, + EncryptedContent_GroupUpdate? groupUpdate, }) { final $result = create(); if (groupId != null) { @@ -1074,6 +1271,15 @@ class EncryptedContent extends $pb.GeneratedMessage { if (textMessage != null) { $result.textMessage = textMessage; } + if (groupCreate != null) { + $result.groupCreate = groupCreate; + } + if (groupJoin != null) { + $result.groupJoin = groupJoin; + } + if (groupUpdate != null) { + $result.groupUpdate = groupUpdate; + } return $result; } EncryptedContent._() : super(); @@ -1093,6 +1299,9 @@ class EncryptedContent extends $pb.GeneratedMessage { ..aOM(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create) ..aOM(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create) ..aOM(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) + ..aOM(14, _omitFieldNames ? '' : 'groupCreate', protoName: 'groupCreate', subBuilder: EncryptedContent_GroupCreate.create) + ..aOM(15, _omitFieldNames ? '' : 'groupJoin', protoName: 'groupJoin', subBuilder: EncryptedContent_GroupJoin.create) + ..aOM(16, _omitFieldNames ? '' : 'groupUpdate', protoName: 'groupUpdate', subBuilder: EncryptedContent_GroupUpdate.create) ..hasRequiredFields = false ; @@ -1243,6 +1452,39 @@ class EncryptedContent extends $pb.GeneratedMessage { void clearTextMessage() => clearField(13); @$pb.TagNumber(13) EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11); + + @$pb.TagNumber(14) + EncryptedContent_GroupCreate get groupCreate => $_getN(12); + @$pb.TagNumber(14) + set groupCreate(EncryptedContent_GroupCreate v) { setField(14, v); } + @$pb.TagNumber(14) + $core.bool hasGroupCreate() => $_has(12); + @$pb.TagNumber(14) + void clearGroupCreate() => clearField(14); + @$pb.TagNumber(14) + EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(12); + + @$pb.TagNumber(15) + EncryptedContent_GroupJoin get groupJoin => $_getN(13); + @$pb.TagNumber(15) + set groupJoin(EncryptedContent_GroupJoin v) { setField(15, v); } + @$pb.TagNumber(15) + $core.bool hasGroupJoin() => $_has(13); + @$pb.TagNumber(15) + void clearGroupJoin() => clearField(15); + @$pb.TagNumber(15) + EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(13); + + @$pb.TagNumber(16) + EncryptedContent_GroupUpdate get groupUpdate => $_getN(14); + @$pb.TagNumber(16) + set groupUpdate(EncryptedContent_GroupUpdate v) { setField(16, v); } + @$pb.TagNumber(16) + $core.bool hasGroupUpdate() => $_has(14); + @$pb.TagNumber(16) + void clearGroupUpdate() => clearField(16); + @$pb.TagNumber(16) + EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14); } diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 4300965..1896c58 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -106,8 +106,11 @@ const EncryptedContent$json = { {'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true}, {'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true}, {'1': 'textMessage', '3': 13, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 11, '10': 'textMessage', '17': true}, + {'1': 'groupCreate', '3': 14, '4': 1, '5': 11, '6': '.EncryptedContent.GroupCreate', '9': 12, '10': 'groupCreate', '17': true}, + {'1': 'groupJoin', '3': 15, '4': 1, '5': 11, '6': '.EncryptedContent.GroupJoin', '9': 13, '10': 'groupJoin', '17': true}, + {'1': 'groupUpdate', '3': 16, '4': 1, '5': 11, '6': '.EncryptedContent.GroupUpdate', '9': 14, '10': 'groupUpdate', '17': true}, ], - '3': [EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], + '3': [EncryptedContent_GroupCreate$json, EncryptedContent_GroupJoin$json, EncryptedContent_GroupUpdate$json, EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], '8': [ {'1': '_groupId'}, {'1': '_isDirectChat'}, @@ -121,6 +124,40 @@ const EncryptedContent$json = { {'1': '_pushKeys'}, {'1': '_reaction'}, {'1': '_textMessage'}, + {'1': '_groupCreate'}, + {'1': '_groupJoin'}, + {'1': '_groupUpdate'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupCreate$json = { + '1': 'GroupCreate', + '2': [ + {'1': 'stateKey', '3': 3, '4': 1, '5': 12, '10': 'stateKey'}, + {'1': 'groupPublicKey', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupJoin$json = { + '1': 'GroupJoin', + '2': [ + {'1': 'groupPublicKey', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupUpdate$json = { + '1': 'GroupUpdate', + '2': [ + {'1': 'groupActionType', '3': 1, '4': 1, '5': 9, '10': 'groupActionType'}, + {'1': 'affectedContactId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'affectedContactId', '17': true}, + {'1': 'newGroupName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'newGroupName', '17': true}, + ], + '8': [ + {'1': '_affectedContactId'}, + {'1': '_newGroupName'}, ], }; @@ -326,47 +363,57 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0' 'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh' 'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2' - '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z' - 'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX' - 'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ' - 'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg' - '90YXJnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIF' - 'ZW1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZR' - 'gBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3Nl' - 'bmRlck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYX' - 'JnZXRNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgE' - 'IAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCg' - 'oGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJ' - 'ZEIHCgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZX' - 'NzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlw' - 'ZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk' - '1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJl' - 'c0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTW' - 'Vzc2FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByAB' - 'KAxIAlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cH' - 'Rpb25LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0K' - 'D2VuY3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCg' - 'hSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxp' - 'bWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQh' - 'AKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2Ua' - 'pwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVX' - 'BkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdl' - 'SWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1' - 'IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5D' - 'b250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVB' - 'ABEgoKBkFDQ0VQVBACGvABCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0' - 'ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2' - 'VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESJQoLZGlzcGxheU5hbWUYAyABKAlI' - 'AVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2' - 'F2YXRhclN2Z0NvbXByZXNzZWRCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBl' - 'GAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGA' - 'IgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQg' - 'ASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICg' - 'Zfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3Vu' - 'dGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1' - 'IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5k' - 'QgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQh' - 'AKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RV' - 'cGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV' - '9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2U='); + '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBEkQKC2dyb3VwQ3JlYXRlGA4gASgL' + 'Mh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cENyZWF0ZUgMUgtncm91cENyZWF0ZYgBARI+Cglncm' + '91cEpvaW4YDyABKAsyGy5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgNUglncm91cEpvaW6I' + 'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA' + '5SC2dyb3VwVXBkYXRliAEBGlEKC0dyb3VwQ3JlYXRlEhoKCHN0YXRlS2V5GAMgASgMUghzdGF0' + 'ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQdWJsaWNLZXkaMwoJR3JvdXBKb2' + 'luEiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRq6AQoLR3JvdXBVcGRh' + 'dGUSKAoPZ3JvdXBBY3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZW' + 'RDb250YWN0SWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1l' + 'GAMgASgJSAFSDG5ld0dyb3VwTmFtZYgBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0' + 'dyb3VwTmFtZRqpAQoLVGV4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5k' + 'ZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbW' + 'VzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9f' + 'cXVvdGVNZXNzYWdlSWQaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YX' + 'JnZXRNZXNzYWdlSWQSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVt' + 'b3ZlGrcCCg1NZXNzYWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk' + '1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2Vu' + 'ZGVyTWVzc2FnZUlkiAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdG' + 'lwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3Rh' + 'bXAYBSABKANSCXRpbWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEg' + 'oKBk9QRU5FRBACQhIKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQajAUKBU1lZGlhEigKD3Nl' + 'bmRlck1lc3NhZ2VJZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5Fbm' + 'NyeXB0ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNl' + 'Y29uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZX' + 'NBdXRoZW50aWNhdGlvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3Rh' + 'bXAYBSABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3' + 'NhZ2VJZIgBARIpCg1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoN' + 'ZW5jcnlwdGlvbktleRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYW' + 'MYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIP' + 'ZW5jcnlwdGlvbk5vbmNliAEBIjMKBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBV' + 'ZJREVPEAISBwoDR0lGEANCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW90' + 'ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5jcn' + 'lwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlGqcBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEg' + 'ASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigKD3RhcmdldE' + '1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIK' + 'CgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdH' + 'lwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIr' + 'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrwAQoNQ29udGFjdF' + 'VwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5' + 'cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcm' + 'Vzc2VkiAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUS' + 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQg4KDF9kaX' + 'NwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQu' + 'UHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAy' + 'ABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlw' + 'ZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZW' + 'RBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYK' + 'Fmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHg' + 'oKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RD' + 'aGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaW' + 'FCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIM' + 'CgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg' + '4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZQ=='); diff --git a/lib/src/model/protobuf/client/groups.proto b/lib/src/model/protobuf/client/groups.proto index 140d754..813563c 100644 --- a/lib/src/model/protobuf/client/groups.proto +++ b/lib/src/model/protobuf/client/groups.proto @@ -1,10 +1,16 @@ syntax = "proto3"; // Stored encrypted on the server in the members columns. -message GroupState { +message EncryptedGroupState { repeated int64 memberIds = 1; repeated int64 adminIds = 2; string groupName = 3; optional int64 deleteMessagesAfterMilliseconds = 4; - bytes _padding = 5; + bytes padding = 5; +} + +message EncryptedGroupStateEnvelop { + bytes nonce = 1; + bytes encryptedGroupState = 2; + bytes mac = 3; } \ No newline at end of file diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 29dc4e1..a4ab602 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -44,31 +44,26 @@ message EncryptedContent { optional PushKeys pushKeys = 11; optional Reaction reaction = 12; optional TextMessage textMessage = 13; - optional NewGroup newGroup = 14; - optional JoinGroup joinGroup = 15; + optional GroupCreate groupCreate = 14; + optional GroupJoin groupJoin = 15; optional GroupUpdate groupUpdate = 16; - message NewGroup { + message GroupCreate { // key for the state stored on the server - string groupId = 1; - string stateId = 2; bytes stateKey = 3; bytes groupPublicKey = 4; } - message JoinGroup { + message GroupJoin { // key for the state stored on the server - string groupId = 1; bytes groupPublicKey = 4; } message GroupUpdate { - optional int64 removedAdmin = 1; - optional int64 removedUser = 2; - optional int64 groupName = 3; - optional int64 deleteMessagesAfterMilliseconds = 4; - bytes stateKey = 5; + string groupActionType = 1; // GroupActionType.name + optional int64 affectedContactId = 2; + optional string newGroupName = 3; } message TextMessage { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 7de2f52..4c40c3b 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:ui' as ui; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; @@ -283,6 +284,10 @@ class ApiService { request.v0.seq = seq; final requestBytes = request.writeToBuffer(); + Log.info( + 'Sending ${requestBytes.length} bytes to the server via WebSocket.', + ); + if (ensureRetransmission) { await addToRetransmissionBuffer(seq, requestBytes); } @@ -467,6 +472,7 @@ class ApiService { ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekeySignature = signedPreKey.signature ..signedPrekeyId = Int64(signedPreKey.id) + ..langCode = ui.PlatformDispatcher.instance.locale.languageCode ..isIos = Platform.isIOS; if (inviteCode != null && inviteCode != '') { diff --git a/lib/src/services/api/server_messages/contact.server_messages.dart b/lib/src/services/api/client2client/contact.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/contact.server_messages.dart rename to lib/src/services/api/client2client/contact.c2c.dart diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart new file mode 100644 index 0000000..0f7a040 --- /dev/null +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleGroupCreate( + int fromUserId, + String groupId, + EncryptedContent_GroupCreate newGroup, +) async { + // 1. Store the new group -> e.g. store the stateKey and groupPublicKey + // 2. Call function that should fetch all jobs + // 1. This function is also called in the main function, in case the state stored on the server could not be loaded + // 2. This function will also send the GroupJoin to all members -> so they get there public key + // 3. Finished + + final myGroupKey = generateIdentityKeyPair(); + + // Group state is joinedGroup -> As the current state has not yet been downloaded. + final group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + stateVersionId: const Value(1), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + ), + ); + + if (group == null) { + Log.error( + 'Could not create new group. Probably because the group already existed.', + ); + return; + } + + final user = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (user == null) { + // Only contacts can invite other contacts, so this can (via the UI) not happen. + Log.error( + 'User is not a contact. Aborting.', + ); + return; + } + + await twonlyDB.groupsDao.insertGroupMember( + GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(fromUserId), + memberState: const Value( + MemberState.admin, // is the group creator, so must be admin... + ), + groupPublicKey: Value(Uint8List.fromList(newGroup.groupPublicKey)), + ), + ); + + // can be done in the background -> websocket message can be ACK + unawaited(fetchGroupStatesForUnjoinedGroups()); +} + +Future handleGroupUpdate( + int fromUserId, + String groupId, + EncryptedContent_GroupUpdate update, +) async {} + +Future handleGroupJoin( + int fromUserId, + String groupId, + EncryptedContent_GroupJoin join, +) async {} diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/client2client/media.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/media.server_messages.dart rename to lib/src/services/api/client2client/media.c2c.dart diff --git a/lib/src/services/api/server_messages/messages.server_messages.dart b/lib/src/services/api/client2client/messages.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/messages.server_messages.dart rename to lib/src/services/api/client2client/messages.c2c.dart diff --git a/lib/src/services/api/server_messages/prekeys.server_messages.dart b/lib/src/services/api/client2client/prekeys.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/prekeys.server_messages.dart rename to lib/src/services/api/client2client/prekeys.c2c.dart diff --git a/lib/src/services/api/server_messages/pushkeys.server_messages.dart b/lib/src/services/api/client2client/pushkeys.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/pushkeys.server_messages.dart rename to lib/src/services/api/client2client/pushkeys.c2c.dart diff --git a/lib/src/services/api/server_messages/reaction.server_message.dart b/lib/src/services/api/client2client/reaction.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/reaction.server_message.dart rename to lib/src/services/api/client2client/reaction.c2c.dart diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/client2client/text_message.c2c.dart similarity index 100% rename from lib/src/services/api/server_messages/text_message.server_messages.dart rename to lib/src/services/api/client2client/text_message.c2c.dart diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index e8e6ad6..e9c8ddc 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -11,14 +11,15 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/api/server_messages/contact.server_messages.dart'; -import 'package:twonly/src/services/api/server_messages/media.server_messages.dart'; -import 'package:twonly/src/services/api/server_messages/messages.server_messages.dart'; -import 'package:twonly/src/services/api/server_messages/prekeys.server_messages.dart'; -import 'package:twonly/src/services/api/server_messages/pushkeys.server_messages.dart'; -import 'package:twonly/src/services/api/server_messages/reaction.server_message.dart'; -import 'package:twonly/src/services/api/server_messages/text_message.server_messages.dart'; +import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; +import 'package:twonly/src/services/api/client2client/media.c2c.dart'; +import 'package:twonly/src/services/api/client2client/messages.c2c.dart'; +import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart'; +import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart'; +import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; +import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -36,7 +37,7 @@ Future handleServerMessage(server.ServerToClient msg) async { } else if (msg.v0.hasNewMessage()) { final body = Uint8List.fromList(msg.v0.newMessage.body); final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - await handleNewMessage(fromUserId, body); + await handleClient2ClientMessage(fromUserId, body); } else { Log.error('Unknown server message: $msg'); } @@ -55,7 +56,7 @@ DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); Mutex protectReceiptCheck = Mutex(); -Future handleNewMessage(int fromUserId, Uint8List body) async { +Future handleClient2ClientMessage(int fromUserId, Uint8List body) async { final message = Message.fromBuffer(body); final receiptId = message.receiptId; @@ -205,6 +206,33 @@ Future handleEncryptedMessage( return null; } + if (content.hasGroupUpdate()) { + await handleGroupUpdate( + fromUserId, + content.groupId, + content.groupUpdate, + ); + return null; + } + + if (content.hasGroupCreate()) { + await handleGroupCreate( + fromUserId, + content.groupId, + content.groupCreate, + ); + return null; + } + + if (content.hasGroupJoin()) { + await handleGroupJoin( + fromUserId, + content.groupId, + content.groupJoin, + ); + return null; + } + /// Verify that the user is (still) in that group... if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) { diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart new file mode 100644 index 0000000..c144208 --- /dev/null +++ b/lib/src/services/group.services.dart @@ -0,0 +1,179 @@ +import 'dart:math'; +import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:fixnum/fixnum.dart'; +import 'package:hashlib/random.dart'; +import 'package:http/http.dart' as http; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +String getGroupStateUrl() { + return 'http${apiService.apiSecure}://${apiService.apiHost}/api/group/state'; +} + +Future createNewGroup(String groupName, List members) async { + // First: Upload new State to the server..... + // if (groupName) return; + + final groupId = uuid.v4(); + + final memberIds = members.map((x) => Int64(x.userId)).toList(); + + final groupState = EncryptedGroupState( + memberIds: memberIds, + adminIds: [Int64(gUser.userId)], + groupName: groupName, + deleteMessagesAfterMilliseconds: + Int64(defaultDeleteMessagesAfterMilliseconds), + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + final stateEncryptionKey = getRandomUint8List(32); + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final secretBox = await chacha20.encrypt( + groupState.writeToBuffer(), + secretKey: SecretKey(stateEncryptionKey), + nonce: encryptionNonce, + ); + + final encryptedGroupState = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + final myGroupKey = generateIdentityKeyPair(); + + { + // Upload the group state, if this fails, the group can not be created. + + final newGroupState = NewGroupState( + groupId: groupId, + versionId: Int64(1), + encryptedGroupState: encryptedGroupState.writeToBuffer(), + publicKey: myGroupKey.getPublicKey().serialize(), + ); + + final response = await http + .post( + Uri.parse(getGroupStateUrl()), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not upload group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + + final group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + groupName: Value(groupName), + isGroupAdmin: const Value(true), + stateEncryptionKey: Value(stateEncryptionKey), + stateVersionId: const Value(1), + myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + ), + ); + + if (group == null) { + Log.error('Could not insert group into database.'); + return false; + } + + Log.info('Created new group: ${group.groupId}'); + + for (final member in members) { + await twonlyDB.groupsDao.insertGroupMember( + GroupMembersCompanion( + groupId: Value(group.groupId), + contactId: Value(member.userId), + memberState: const Value(MemberState.normal), + ), + ); + } + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: const Value(GroupActionType.createdGroup), + ), + ); + + // Notify members about the new group :) + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupCreate: EncryptedContent_GroupCreate( + stateKey: stateEncryptionKey, + groupPublicKey: myGroupKey.getPublicKey().serialize(), + ), + ), + null, + ); + + return true; +} + +Future fetchGroupStatesForUnjoinedGroups() async { + final groups = await twonlyDB.groupsDao.getAllNotJoinedGroups(); + + for (final group in groups) {} +} + +Future fetchGroupState(Group group) async { + try { + final response = await http + .get( + Uri.parse('${getGroupStateUrl()}/${group.groupId}'), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not load group state. Got status code ${response.statusCode} from server.', + ); + return null; + } + + final groupStateServer = GroupState.fromBuffer(response.bodyBytes); + final envelope = EncryptedGroupStateEnvelop.fromBuffer( + groupStateServer.encryptedGroupState); + final chacha20 = FlutterChacha20.poly1305Aead(); + + final secretBox = SecretBox(envelope.encryptedGroupState, + nonce: envelope.nonce, mac: Mac(envelope.mac)); + + final encryptedGroupStateRaw = await chacha20.decrypt(secretBox, + secretKey: SecretKey(group.stateEncryptionKey!)); + + final encryptedGroupState = + EncryptedGroupState.fromBuffer(encryptedGroupStateRaw); + + encryptedGroupState.adminIds; + encryptedGroupState.memberIds; + encryptedGroupState.groupName; + encryptedGroupState.deleteMessagesAfterMilliseconds; + encryptedGroupState.deleteMessagesAfterMilliseconds; + groupStateServer.versionId; + } catch (e) { + Log.error(e); + return null; + } +} diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index 7ab04b5..c24ffec 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -277,9 +277,8 @@ class ContactsListView extends StatelessWidget { itemCount: contacts.length, itemBuilder: (context, index) { final contact = contacts[index]; - final displayName = getContactDisplayName(contact); return ListTile( - title: Text(displayName), + title: Text(substringBy(contact.username, 25)), leading: AvatarIcon(contact: contact), trailing: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index c1670a6..046e492 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart'; +import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; class StartNewChatView extends StatefulWidget { const StartNewChatView({super.key}); @@ -116,11 +117,10 @@ class UserList extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( restorationId: 'new_message_users_list', - itemCount: users.length + 2, + itemCount: users.length + 3, itemBuilder: (BuildContext context, int i) { - if (i == 0) { + if (i == 1) { return ListTile( - key: const Key('add_new_contact'), title: Text(context.lang.startNewChatNewContact), leading: const CircleAvatar( child: FaIcon( @@ -138,10 +138,29 @@ class UserList extends StatelessWidget { }, ); } - if (i == 1) { + if (i == 0) { + return ListTile( + title: Text(context.lang.newGroup), + leading: const CircleAvatar( + child: FaIcon( + FontAwesomeIcons.userGroup, + size: 13, + ), + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const GroupCreateSelectMembersView(), + ), + ); + }, + ); + } + if (i == 2) { return const Divider(); } - final user = users[i - 2]; + final user = users[i - 3]; return UserContextMenu( key: Key(user.userId.toString()), contact: user, diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart new file mode 100644 index 0000000..7c4b81c --- /dev/null +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; + +class GroupCreateSelectGroupNameView extends StatefulWidget { + const GroupCreateSelectGroupNameView({ + required this.selectedUsers, + super.key, + }); + + final List selectedUsers; + + @override + State createState() => + _GroupCreateSelectGroupNameViewState(); +} + +class _GroupCreateSelectGroupNameViewState + extends State { + final TextEditingController textFieldGroupName = TextEditingController(); + + bool _isLoading = false; + + Future _createNewGroup() async { + setState(() { + _isLoading = true; + }); + + final wasSuccess = + await createNewGroup(textFieldGroupName.text, widget.selectedUsers); + if (wasSuccess) { + // POP + } + + setState(() { + _isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: Text(context.lang.selectGroupName), + ), + floatingActionButton: FilledButton.icon( + onPressed: (textFieldGroupName.text.isEmpty || _isLoading) + ? null + : _createNewGroup, + label: Text(context.lang.createGroup), + icon: _isLoading + ? const SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + : const FaIcon(FontAwesomeIcons.penToSquare), + ), + body: SafeArea( + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + setState(() {}); + }, + autofocus: true, + controller: textFieldGroupName, + decoration: getInputDecoration( + context, + context.lang.groupNameInput, + ), + ), + ), + const SizedBox(height: 10), + ListTile( + title: Text(context.lang.groupMembers), + ), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: widget.selectedUsers.length, + itemBuilder: (BuildContext context, int i) { + final user = widget.selectedUsers[i]; + return UserContextMenu( + key: GlobalKey(), + contact: user, + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(user)), + FlameCounterWidget( + contactId: user.userId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + contact: user, + fontSize: 13, + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart new file mode 100644 index 0000000..2340746 --- /dev/null +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -0,0 +1,250 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; +import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart'; + +class GroupCreateSelectMembersView extends StatefulWidget { + const GroupCreateSelectMembersView({super.key}); + @override + State createState() => _StartNewChatView(); +} + +class _StartNewChatView extends State { + List contacts = []; + List allContacts = []; + final TextEditingController searchUserName = TextEditingController(); + late StreamSubscription> contactSub; + + final HashSet selectedUsers = HashSet(); + + @override + void initState() { + super.initState(); + + final stream = twonlyDB.contactsDao.watchAllAcceptedContacts(); + + contactSub = stream.listen((update) async { + update.sort( + (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)), + ); + setState(() { + allContacts = update; + }); + await filterUsers(); + }); + } + + @override + void dispose() { + unawaited(contactSub.cancel()); + super.dispose(); + } + + Future filterUsers() async { + if (searchUserName.value.text.isEmpty) { + setState(() { + contacts = allContacts; + }); + return; + } + final usersFiltered = allContacts + .where( + (user) => getContactDisplayName(user) + .toLowerCase() + .contains(searchUserName.value.text.toLowerCase()), + ) + .toList(); + setState(() { + contacts = usersFiltered; + }); + } + + void toggleSelectedUser(int userId) { + if (!selectedUsers.contains(userId)) { + selectedUsers.add(userId); + } else { + selectedUsers.remove(userId); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: Text(context.lang.selectMembers), + ), + floatingActionButton: FilledButton.icon( + onPressed: selectedUsers.isEmpty + ? null + : () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectGroupNameView( + selectedUsers: allContacts + .where((t) => selectedUsers.contains(t.userId)) + .toList(), + ), + ), + ); + }, + label: Text(context.lang.next), + icon: const FaIcon(FontAwesomeIcons.penToSquare), + ), + body: SafeArea( + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + await filterUsers(); + }, + controller: searchUserName, + decoration: getInputDecoration( + context, + context.lang.shareImageSearchAllContacts, + ), + ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: + contacts.length + (selectedUsers.isEmpty ? 0 : 2), + itemBuilder: (BuildContext context, int i) { + if (selectedUsers.isNotEmpty) { + final selected = selectedUsers.toList(); + if (i == 0) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 18), + constraints: const BoxConstraints( + maxHeight: 150, + ), + child: SingleChildScrollView( + child: LayoutBuilder( + builder: (context, constraints) { + // Wrap will use the available width from constraints.maxWidth + return Wrap( + spacing: 8, + children: selected.map((w) { + return _Chip( + contact: allContacts + .firstWhere((t) => t.userId == w), + onTap: toggleSelectedUser, + ); + }).toList(), + ); + }), + ), + ); + } + if (i == 1) { + return const Divider(); + } + i -= 2; + } + final user = contacts[i]; + return UserContextMenu( + key: GlobalKey(), + contact: user, + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(user)), + FlameCounterWidget( + contactId: user.userId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + contact: user, + fontSize: 13, + ), + trailing: Checkbox( + value: selectedUsers.contains(user.userId), + side: WidgetStateBorderSide.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return const BorderSide(width: 0); + } + return BorderSide( + color: Theme.of(context).colorScheme.outline, + ); + }, + ), + onChanged: (bool? value) { + toggleSelectedUser(user.userId); + }, + ), + onTap: () { + toggleSelectedUser(user.userId); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _Chip extends StatelessWidget { + const _Chip({ + required this.contact, + required this.onTap, + }); + final Contact contact; + final void Function(int) onTap; + + @override + Widget build(BuildContext context) { + return Chip( + key: GlobalKey(), + avatar: AvatarIcon( + contact: contact, + fontSize: 10, + ), + label: GestureDetector( + onTap: () => onTap(contact.userId), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getContactDisplayName(contact), + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(width: 15), + const FaIcon( + FontAwesomeIcons.xmark, + color: Colors.grey, + size: 12, + ) + ], + ), + ), + ); + } +} diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index 3824b77..b842220 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -13,20 +13,19 @@ CLIENT_DIR="./lib/src/model/protobuf/client/" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "backup.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "messages.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "groups.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" -exit - # Definitions for the Server API -SRC_DIR="../twonly-server/twonly/src/" +SRC_DIR="../twonly-server/twonly-api/src/" DST_DIR="$(pwd)/lib/src/model/protobuf/" -mkdir $DST_DIR +mkdir $DST_DIR &>/dev/null ORIGINAL_DIR=$(pwd) @@ -37,9 +36,7 @@ cd "$SRC_DIR" || { for proto_file in "api/"**/*.proto; do if [[ -f "$proto_file" ]]; then - # Run the protoc command protoc --proto_path="." --dart_out="$DST_DIR" "$proto_file" - echo "Processed: $proto_file" else echo "No .proto files found in $SRC_DIR" fi @@ -50,4 +47,4 @@ cd "$ORIGINAL_DIR" || { exit 1 } -echo "Finished processing .proto files." \ No newline at end of file +echo "Finished processing .proto files :)" \ No newline at end of file From 98a19b174b3aaf38192fa79f001642975ece49dd Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Nov 2025 01:47:05 +0100 Subject: [PATCH 41/76] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02eac22..b6229df 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository contains the complete source code of the [twonly](https://twonly - Developed by humans not by AI or Vibe Coding - No email or phone number required to register - Privacy friendly - Everything is stored on the device -- Backend is exclusively hosted in European +- The backend is hosted exclusively in Europe ## Planned From 72dca4d4b431f27065bde90ffc73a56bb62c7213 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Nov 2025 14:56:55 +0100 Subject: [PATCH 42/76] creation of groups works --- lib/src/database/daos/contacts.dao.dart | 5 + lib/src/database/daos/groups.dao.dart | 26 ++- lib/src/database/daos/messages.dao.dart | 32 ++++ lib/src/database/tables/groups.table.dart | 2 +- .../client/generated/messages.pb.dart | 48 +++++ .../client/generated/messages.pbjson.dart | 19 +- lib/src/model/protobuf/client/messages.proto | 3 + lib/src/services/api.service.dart | 11 +- .../api/client2client/contact.c2c.dart | 28 +-- .../api/client2client/groups.c2c.dart | 33 +++- lib/src/services/api/messages.dart | 10 +- lib/src/services/api/server_messages.dart | 75 +++++--- lib/src/services/group.services.dart | 145 +++++++++++++-- lib/src/views/chats/chat_messages.view.dart | 169 ++++++++++++------ .../all_reactions.bottom_sheet.dart | 1 - .../chat_list_entry.dart | 33 +++- .../chat_media_entry.dart | 1 - .../message_context_menu.dart | 3 - lib/src/views/chats/media_viewer.view.dart | 1 - .../emoji_reactions_row.component.dart | 1 - lib/src/views/chats/message_info.view.dart | 49 ++--- .../components/avatar_icon.component.dart | 77 ++++++-- lib/src/views/components/flame.dart | 8 +- .../group_create_select_group_name.view.dart | 2 + 24 files changed, 615 insertions(+), 167 deletions(-) diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 07a730e..7435688 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -37,6 +37,11 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { return select(contacts)..where((t) => t.userId.equals(userId)); } + Future getContactById(int userId) async { + return (select(contacts)..where((t) => t.userId.equals(userId))) + .getSingleOrNull(); + } + Future> getContactsByUsername(String username) async { return (select(contacts)..where((t) => t.username.equals(username))).get(); } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b28d460..52a837c 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -8,7 +8,13 @@ import 'package:twonly/src/utils/misc.dart'; part 'groups.dao.g.dart'; -@DriftAccessor(tables: [Groups, GroupMembers, GroupHistories]) +@DriftAccessor( + tables: [ + Groups, + GroupMembers, + GroupHistories, + ], +) class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { // this constructor is required so that the main database can create an instance // of this object. @@ -54,6 +60,24 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { await into(groupHistories).insert(insertAction); } + Future updateMember( + String groupId, + int contactId, + GroupMembersCompanion updates, + ) async { + await (update(groupMembers) + ..where( + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId))) + .write(updates); + } + + Future removeMember(String groupId, int contactId) async { + await (delete(groupMembers) + ..where( + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId))) + .go(); + } + Future createNewDirectChat( int contactId, GroupsCompanion group, diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 85b2928..f68163e 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -399,6 +399,38 @@ 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 deleteMessagesByContactId(int contactId) { // return (delete(messages) // ..where( diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index e745572..68d248a 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -49,7 +49,7 @@ class Groups extends Table { Set get primaryKey => {groupId}; } -enum MemberState { normal, admin } +enum MemberState { normal, admin, leftGroup } @DataClassName('GroupMember') class GroupMembers extends Table { diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 029b405..756be8b 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -112,6 +112,38 @@ class Message extends $pb.GeneratedMessage { PlaintextContent ensurePlaintextContent() => $_ensure(3); } +class PlaintextContent_RetryErrorMessage extends $pb.GeneratedMessage { + factory PlaintextContent_RetryErrorMessage() => create(); + PlaintextContent_RetryErrorMessage._() : super(); + factory PlaintextContent_RetryErrorMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent_RetryErrorMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent.RetryErrorMessage', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent_RetryErrorMessage clone() => PlaintextContent_RetryErrorMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent_RetryErrorMessage copyWith(void Function(PlaintextContent_RetryErrorMessage) updates) => super.copyWith((message) => updates(message as PlaintextContent_RetryErrorMessage)) as PlaintextContent_RetryErrorMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent_RetryErrorMessage create() => PlaintextContent_RetryErrorMessage._(); + PlaintextContent_RetryErrorMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent_RetryErrorMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent_RetryErrorMessage? _defaultInstance; +} + class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage { factory PlaintextContent_DecryptionErrorMessage({ PlaintextContent_DecryptionErrorMessage_Type? type, @@ -165,11 +197,15 @@ class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage { class PlaintextContent extends $pb.GeneratedMessage { factory PlaintextContent({ PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage, + PlaintextContent_RetryErrorMessage? retryControlError, }) { final $result = create(); if (decryptionErrorMessage != null) { $result.decryptionErrorMessage = decryptionErrorMessage; } + if (retryControlError != null) { + $result.retryControlError = retryControlError; + } return $result; } PlaintextContent._() : super(); @@ -178,6 +214,7 @@ class PlaintextContent extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent', createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'decryptionErrorMessage', protoName: 'decryptionErrorMessage', subBuilder: PlaintextContent_DecryptionErrorMessage.create) + ..aOM(2, _omitFieldNames ? '' : 'retryControlError', protoName: 'retryControlError', subBuilder: PlaintextContent_RetryErrorMessage.create) ..hasRequiredFields = false ; @@ -212,6 +249,17 @@ class PlaintextContent extends $pb.GeneratedMessage { void clearDecryptionErrorMessage() => clearField(1); @$pb.TagNumber(1) PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0); + + @$pb.TagNumber(2) + PlaintextContent_RetryErrorMessage get retryControlError => $_getN(1); + @$pb.TagNumber(2) + set retryControlError(PlaintextContent_RetryErrorMessage v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRetryControlError() => $_has(1); + @$pb.TagNumber(2) + void clearRetryControlError() => clearField(2); + @$pb.TagNumber(2) + PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1); } class EncryptedContent_GroupCreate extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 1896c58..82a3e7a 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -56,13 +56,20 @@ const PlaintextContent$json = { '1': 'PlaintextContent', '2': [ {'1': 'decryptionErrorMessage', '3': 1, '4': 1, '5': 11, '6': '.PlaintextContent.DecryptionErrorMessage', '9': 0, '10': 'decryptionErrorMessage', '17': true}, + {'1': 'retryControlError', '3': 2, '4': 1, '5': 11, '6': '.PlaintextContent.RetryErrorMessage', '9': 1, '10': 'retryControlError', '17': true}, ], - '3': [PlaintextContent_DecryptionErrorMessage$json], + '3': [PlaintextContent_RetryErrorMessage$json, PlaintextContent_DecryptionErrorMessage$json], '8': [ {'1': '_decryptionErrorMessage'}, + {'1': '_retryControlError'}, ], }; +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_RetryErrorMessage$json = { + '1': 'RetryErrorMessage', +}; + @$core.Deprecated('Use plaintextContentDescriptor instead') const PlaintextContent_DecryptionErrorMessage$json = { '1': 'DecryptionErrorMessage', @@ -85,10 +92,12 @@ const PlaintextContent_DecryptionErrorMessage_Type$json = { final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode( 'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG' 'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y' - 'TWVzc2FnZYgBARqEAQoWRGVjcnlwdGlvbkVycm9yTWVzc2FnZRJBCgR0eXBlGAEgASgOMi0uUG' - 'xhaW50ZXh0Q29udGVudC5EZWNyeXB0aW9uRXJyb3JNZXNzYWdlLlR5cGVSBHR5cGUiJwoEVHlw' - 'ZRILCgdVTktOT1dOEAASEgoOUFJFS0VZX1VOS05PV04QAUIZChdfZGVjcnlwdGlvbkVycm9yTW' - 'Vzc2FnZQ=='); + 'TWVzc2FnZYgBARJWChFyZXRyeUNvbnRyb2xFcnJvchgCIAEoCzIjLlBsYWludGV4dENvbnRlbn' + 'QuUmV0cnlFcnJvck1lc3NhZ2VIAVIRcmV0cnlDb250cm9sRXJyb3KIAQEaEwoRUmV0cnlFcnJv' + 'ck1lc3NhZ2UahAEKFkRlY3J5cHRpb25FcnJvck1lc3NhZ2USQQoEdHlwZRgBIAEoDjItLlBsYW' + 'ludGV4dENvbnRlbnQuRGVjcnlwdGlvbkVycm9yTWVzc2FnZS5UeXBlUgR0eXBlIicKBFR5cGUS' + 'CwoHVU5LTk9XThAAEhIKDlBSRUtFWV9VTktOT1dOEAFCGQoXX2RlY3J5cHRpb25FcnJvck1lc3' + 'NhZ2VCFAoSX3JldHJ5Q29udHJvbEVycm9y'); @$core.Deprecated('Use encryptedContentDescriptor instead') const EncryptedContent$json = { diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index a4ab602..e47999f 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -16,6 +16,9 @@ message Message { message PlaintextContent { optional DecryptionErrorMessage decryptionErrorMessage = 1; + optional RetryErrorMessage retryControlError = 2; + + message RetryErrorMessage { } message DecryptionErrorMessage { enum Type { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 4c40c3b..35debca 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -485,11 +485,18 @@ class ApiService { return sendRequestSync(req); } - Future getUsername(int userId) async { + Future getUserById(int userId) async { final get = ApplicationData_GetUserById()..userId = Int64(userId); final appData = ApplicationData()..getuserbyid = get; final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req, contactId: userId); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasUserdata()) { + return ok.userdata; + } + } + return null; } Future downloadDone(List token) async { diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 8e5aaa3..a961536 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -13,7 +13,7 @@ import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -Future handleContactRequest( +Future handleContactRequest( int fromUserId, EncryptedContent_ContactRequest contactRequest, ) async { @@ -34,24 +34,23 @@ Future handleContactRequest( ), ), ); - return; + return true; } } // Request the username by the server so an attacker can not // forge the displayed username in the contact request - final username = await apiService.getUsername(fromUserId); - if (username.isSuccess) { - // ignore: avoid_dynamic_calls - final name = username.value.userdata.username as Uint8List; - await twonlyDB.contactsDao.insertOnConflictUpdate( - ContactsCompanion( - username: Value(utf8.decode(name)), - userId: Value(fromUserId), - requested: const Value(true), - deletedByUser: const Value(false), - ), - ); + final user = await apiService.getUserById(fromUserId); + if (user == null) { + return false; } + await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(user.username)), + userId: Value(fromUserId), + requested: const Value(true), + deletedByUser: const Value(false), + ), + ); await setupNotificationWithUsers(); case EncryptedContent_ContactRequest_Type.ACCEPT: Log.info('Got a contact accept from $fromUserId'); @@ -82,6 +81,7 @@ Future handleContactRequest( ), ); } + return true; } Future handleContactUpdate( diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 0f7a040..6f81859 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -6,6 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/utils/log.dart'; @@ -26,9 +27,11 @@ Future handleGroupCreate( final group = await twonlyDB.groupsDao.createNewGroup( GroupsCompanion( groupId: Value(groupId), - stateVersionId: const Value(1), + stateVersionId: const Value(0), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), ), ); @@ -63,6 +66,15 @@ Future handleGroupCreate( // can be done in the background -> websocket message can be ACK unawaited(fetchGroupStatesForUnjoinedGroups()); + + await sendCipherTextToGroup( + groupId, + EncryptedContent( + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: myGroupKey.getPublicKey().serialize(), + ), + ), + ); } Future handleGroupUpdate( @@ -71,8 +83,23 @@ Future handleGroupUpdate( EncryptedContent_GroupUpdate update, ) async {} -Future handleGroupJoin( +Future handleGroupJoin( int fromUserId, String groupId, EncryptedContent_GroupJoin join, -) async {} +) async { + if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { + if (!await addNewHiddenContact(fromUserId)) { + Log.error('Got group join, but could not load contact.'); + return false; + } + } + await twonlyDB.groupsDao.updateMember( + groupId, + fromUserId, + GroupMembersCompanion( + groupPublicKey: Value(Uint8List.fromList(join.groupPublicKey)), + ), + ); + return true; +} diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 2345e02..4b72807 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -187,14 +187,18 @@ Future insertAndSendTextMessage( encryptedContent.textMessage.quoteMessageId = quotesMessageId; } - await sendCipherTextToGroup(groupId, encryptedContent, message.messageId); + await sendCipherTextToGroup( + groupId, + encryptedContent, + messageId: message.messageId, + ); } Future sendCipherTextToGroup( String groupId, - pb.EncryptedContent encryptedContent, + pb.EncryptedContent encryptedContent, { String? messageId, -) async { +}) async { final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index e9c8ddc..344a419 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -74,11 +74,23 @@ Future handleClient2ClientMessage(int fromUserId, Uint8List body) async { await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); case Message_Type.PLAINTEXT_CONTENT: - if (message.hasPlaintextContent() && - message.plaintextContent.hasDecryptionErrorMessage()) { - Log.info( - 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', - ); + var retry = false; + if (message.hasPlaintextContent()) { + if (message.plaintextContent.hasDecryptionErrorMessage()) { + Log.info( + 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', + ); + retry = true; + } + if (message.plaintextContent.hasRetryControlError()) { + Log.info( + 'Got access control error for $receiptId. Resending message.', + ); + retry = true; + } + } + + if (retry) { final newReceiptId = uuid.v4(); await twonlyDB.receiptsDao.updateReceipt( receiptId, @@ -162,7 +174,10 @@ Future handleEncryptedMessage( final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); if (content.hasContactRequest()) { - await handleContactRequest(fromUserId, content.contactRequest); + if (!await handleContactRequest(fromUserId, content.contactRequest)) { + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); + } return null; } @@ -206,15 +221,6 @@ Future handleEncryptedMessage( return null; } - if (content.hasGroupUpdate()) { - await handleGroupUpdate( - fromUserId, - content.groupId, - content.groupUpdate, - ); - return null; - } - if (content.hasGroupCreate()) { await handleGroupCreate( fromUserId, @@ -224,15 +230,6 @@ Future handleEncryptedMessage( return null; } - if (content.hasGroupJoin()) { - await handleGroupJoin( - fromUserId, - content.groupId, - content.groupJoin, - ); - return null; - } - /// Verify that the user is (still) in that group... if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) { @@ -255,11 +252,41 @@ Future handleEncryptedMessage( ), ); } else { + if (content.hasGroupJoin()) { + Log.error( + 'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.', + ); + // In case the group join was received before the GroupCreate the sender should send it later again. + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); + } + Log.error('User $fromUserId tried to access group ${content.groupId}.'); return null; } } + if (content.hasGroupUpdate()) { + await handleGroupUpdate( + fromUserId, + content.groupId, + content.groupUpdate, + ); + return null; + } + + if (content.hasGroupJoin()) { + if (!await handleGroupJoin( + fromUserId, + content.groupId, + content.groupJoin, + )) { + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); + } + return null; + } + if (content.hasTextMessage()) { await handleTextMessage( fromUserId, diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index c144208..201430a 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart' show Value; @@ -13,6 +15,7 @@ import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -29,7 +32,7 @@ Future createNewGroup(String groupName, List members) async { final memberIds = members.map((x) => Int64(x.userId)).toList(); final groupState = EncryptedGroupState( - memberIds: memberIds, + memberIds: [Int64(gUser.userId)] + memberIds, adminIds: [Int64(gUser.userId)], groupName: groupName, deleteMessagesAfterMilliseconds: @@ -88,6 +91,7 @@ Future createNewGroup(String groupName, List members) async { stateEncryptionKey: Value(stateEncryptionKey), stateVersionId: const Value(1), myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + joinedGroup: const Value(true), ), ); @@ -125,7 +129,6 @@ Future createNewGroup(String groupName, List members) async { groupPublicKey: myGroupKey.getPublicKey().serialize(), ), ), - null, ); return true; @@ -134,11 +137,15 @@ Future createNewGroup(String groupName, List members) async { Future fetchGroupStatesForUnjoinedGroups() async { final groups = await twonlyDB.groupsDao.getAllNotJoinedGroups(); - for (final group in groups) {} + for (final group in groups) { + await fetchGroupState(group); + } } -Future fetchGroupState(Group group) async { +Future fetchGroupState(Group group) async { try { + var isSuccess = true; + final response = await http .get( Uri.parse('${getGroupStateUrl()}/${group.groupId}'), @@ -149,7 +156,7 @@ Future fetchGroupState(Group group) async { Log.error( 'Could not load group state. Got status code ${response.statusCode} from server.', ); - return null; + return false; } final groupStateServer = GroupState.fromBuffer(response.bodyBytes); @@ -166,14 +173,128 @@ Future fetchGroupState(Group group) async { final encryptedGroupState = EncryptedGroupState.fromBuffer(encryptedGroupStateRaw); - encryptedGroupState.adminIds; - encryptedGroupState.memberIds; - encryptedGroupState.groupName; - encryptedGroupState.deleteMessagesAfterMilliseconds; - encryptedGroupState.deleteMessagesAfterMilliseconds; - groupStateServer.versionId; + if (group.stateVersionId >= groupStateServer.versionId.toInt()) { + Log.error('Group ${group.groupId} has newest group state'); + return false; + } + + final isGroupAdmin = encryptedGroupState.adminIds + .firstWhereOrNull((t) => t.toInt() == gUser.userId) != + null; + + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + groupName: Value(encryptedGroupState.groupName), + deleteMessagesAfterMilliseconds: Value( + encryptedGroupState.deleteMessagesAfterMilliseconds.toInt(), + ), + isGroupAdmin: Value(isGroupAdmin), + ), + ); + + var currentGroupMembers = + await twonlyDB.groupsDao.getGroupMembers(group.groupId); + + // First find and insert NEW members + for (final memberId in encryptedGroupState.memberIds) { + if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) { + // User is already in the database + continue; + } + Log.info('New member in the GROUP state: $memberId'); + + var inContacts = true; + + if (await twonlyDB.contactsDao.getContactById(memberId.toInt()) == null) { + // User is not yet in the contacts, add him in the hidden. So he is not in the contact list / needs to be + // requested separately. + if (!await addNewHiddenContact(memberId.toInt())) { + Log.error('Could not request member ID will retry later.'); + isSuccess = false; + inContacts = false; + } + } + if (inContacts) { + // User is already a contact, so just add him to the group members list + await twonlyDB.groupsDao.insertGroupMember( + GroupMembersCompanion( + groupId: Value(group.groupId), + contactId: Value(memberId.toInt()), + memberState: const Value(MemberState.normal), + ), + ); + } + } + + // check if there is a member which is not in the server list... + + // update the current members list + currentGroupMembers = + await twonlyDB.groupsDao.getGroupMembers(group.groupId); + + for (final member in currentGroupMembers) { + // Member is not any more in the members list + if (!encryptedGroupState.memberIds.contains(Int64(member.contactId))) { + await twonlyDB.groupsDao.removeMember(group.groupId, member.contactId); + continue; + } + + MemberState? newMemberState; + + if (encryptedGroupState.adminIds.contains(Int64(member.contactId))) { + if (member.memberState == MemberState.normal) { + // user was promoted + newMemberState = MemberState.admin; + } + } else if (member.memberState == MemberState.admin) { + // user was demoted + newMemberState = MemberState.normal; + } + + if (newMemberState != null) { + await twonlyDB.groupsDao.updateMember( + group.groupId, + member.contactId, + GroupMembersCompanion( + memberState: Value(newMemberState), + ), + ); + } + } + + if (isSuccess) { + // in case not all members could be loaded from the server, + // this will ensure it will be tried again later + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + stateVersionId: Value(groupStateServer.versionId.toInt()), + joinedGroup: const Value(true), + ), + ); + } + return true; } catch (e) { Log.error(e); - return null; + return false; } } + +Future addNewHiddenContact(int contactId) async { + final userData = await apiService.getUserById(contactId); + if (userData == null) { + Log.error('Could not load contact informations'); + return false; + } + await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(userData.username)), + userId: Value(contactId), + deletedByUser: + const Value(true), // this will hide the contact in the contact list + ), + ); + await createNewSignalSession(userData); + return true; +} diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index e7191dd..f753800 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -30,17 +30,22 @@ Color getMessageColor(Message message) { } class ChatItem { - const ChatItem._({this.message, this.date}); + const ChatItem._({this.message, this.date, this.lastOpenedPosition}); factory ChatItem.date(DateTime date) { return ChatItem._(date: date); } factory ChatItem.message(Message message) { return ChatItem._(message: message); } + factory ChatItem.lastOpenedPosition(List contacts) { + return ChatItem._(lastOpenedPosition: contacts); + } final Message? message; final DateTime? date; + final List? lastOpenedPosition; bool get isMessage => message != null; bool get isDate => date != null; + bool get isLastOpenedPosition => lastOpenedPosition != null; } /// Displays detailed information about a SampleItem. @@ -60,7 +65,12 @@ class _ChatMessagesViewState extends State { String currentInputText = ''; late StreamSubscription userSub; late StreamSubscription> messageSub; + late StreamSubscription>>? + lastOpenedMessageByContactSub; + List messages = []; + List allMessages = []; + List<(Message, Contact)> lastOpenedMessageByContact = []; List galleryItems = []; Message? quotesMessage; GlobalKey verifyShieldKey = GlobalKey(); @@ -87,6 +97,7 @@ class _ChatMessagesViewState extends State { void dispose() { userSub.cancel(); messageSub.cancel(); + lastOpenedMessageByContactSub?.cancel(); tutorial?.cancel(); textFieldFocus.dispose(); super.dispose(); @@ -103,66 +114,110 @@ 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); + }); + } + final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); - messageSub = msgStream.listen((newMessages) async { + 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; + // return; } await protectMessageUpdating.protect(() async { - await flutterLocalNotificationsPlugin.cancelAll(); - - final chatItems = []; - final storedMediaFiles = []; - - DateTime? lastDate; - - final openedMessages = >{}; - - for (final msg in newMessages) { - if (msg.type == MessageType.text && - msg.senderId != null && - msg.openedAt == null) { - if (openedMessages[msg.senderId!] == null) { - openedMessages[msg.senderId!] = []; - } - openedMessages[msg.senderId!]!.add(msg.messageId); - } - - if (msg.type == MessageType.media && msg.mediaStored) { - storedMediaFiles.add(msg); - } - - if (lastDate == null || - msg.createdAt.day != lastDate.day || - msg.createdAt.month != lastDate.month || - msg.createdAt.year != lastDate.year) { - chatItems.add(ChatItem.date(msg.createdAt)); - lastDate = msg.createdAt; - } - chatItems.add(ChatItem.message(msg)); - } - - for (final contactId in openedMessages.keys) { - await notifyContactAboutOpeningMessage( - contactId, - openedMessages[contactId]!, - ); - } - - if (!mounted) return; - setState(() { - messages = chatItems.reversed.toList(); - }); - - final items = await MemoryItem.convertFromMessages(storedMediaFiles); - galleryItems = items.values.toList(); - setState(() {}); + await setMessages(update, lastOpenedMessageByContact); }); }); } + Future setMessages( + List newMessages, + List<(Message, Contact)> lastOpenedMessageByContact, + ) async { + await flutterLocalNotificationsPlugin.cancelAll(); + + final chatItems = []; + final storedMediaFiles = []; + + 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; + + for (final msg in newMessages) { + index += 1; + if (msg.type == MessageType.text && + msg.senderId != null && + msg.openedAt == null) { + if (openedMessages[msg.senderId!] == null) { + openedMessages[msg.senderId!] = []; + } + openedMessages[msg.senderId!]!.add(msg.messageId); + } + + if (msg.type == MessageType.media && msg.mediaStored) { + storedMediaFiles.add(msg); + } + + if (lastDate == null || + msg.createdAt.day != lastDate.day || + msg.createdAt.month != lastDate.month || + msg.createdAt.year != lastDate.year) { + chatItems.add(ChatItem.date(msg.createdAt)); + 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]!, + ), + ); + } + } + } + + for (final contactId in openedMessages.keys) { + await notifyContactAboutOpeningMessage( + contactId, + openedMessages[contactId]!, + ); + } + + if (!mounted) return; + setState(() { + messages = chatItems.reversed.toList(); + }); + + final items = await MemoryItem.convertFromMessages(storedMediaFiles); + galleryItems = items.values.toList(); + setState(() {}); + } + Future _sendMessage() async { if (newMessageController.text == '') return; @@ -275,6 +330,18 @@ 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( + key: GlobalKey(), + contact: w, + fontSize: 12, + ); + }).toList(), + ); } else { final chatMessage = messages[i].message!; return Transform.translate( diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 9d25e2d..8853f02 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -63,7 +63,6 @@ class _AllReactionsViewState extends State { remove: true, ), ), - null, ); if (mounted) Navigator.pop(context); } diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 4b8fd27..646609e 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry. import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; class ChatListEntry extends StatefulWidget { const ChatListEntry({ @@ -86,7 +87,7 @@ class _ChatListEntryState extends State { Widget build(BuildContext context) { final right = widget.message.senderId == null; - final (padding, borderRadius) = getMessageLayout( + final (padding, borderRadius, hideContactAvatar) = getMessageLayout( widget.message, widget.prevMessage, widget.nextMessage, @@ -172,19 +173,36 @@ class _ChatListEntryState extends State { return Align( alignment: right ? Alignment.centerRight : Alignment.centerLeft, - child: Padding(padding: padding, child: child), + child: Padding( + padding: padding, + child: Row( + mainAxisAlignment: + right ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + if (!right && !widget.group.isDirectChat) + hideContactAvatar + ? const SizedBox(width: 24) + : AvatarIcon( + contactId: widget.message.senderId, + fontSize: 12, + ), + child, + ], + ), + ), ); } } -(EdgeInsetsGeometry, BorderRadius) getMessageLayout( +(EdgeInsetsGeometry, BorderRadius, bool) getMessageLayout( Message message, Message? prevMessage, Message? nextMessage, bool hasReactions, ) { - var bottom = 20.0; - var top = 0.0; + var bottom = 10.0; + var top = 10.0; + var hideContactAvatar = false; var topLeft = 12.0; var topRight = 12.0; @@ -209,6 +227,7 @@ class _ChatListEntryState extends State { if (combinesWidthNext) { bottom = 0; bottomLeft = 5.0; + hideContactAvatar = true; } if (message.senderId == null) { @@ -219,6 +238,7 @@ class _ChatListEntryState extends State { final tmp2 = bottomLeft; bottomLeft = bottomRight; bottomRight = tmp2; + hideContactAvatar = true; } return ( @@ -228,6 +248,7 @@ class _ChatListEntryState extends State { topRight: Radius.circular(topRight), bottomRight: Radius.circular(bottomRight), bottomLeft: Radius.circular(bottomLeft), - ) + ), + hideContactAvatar ); } diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index bb53503..96104b4 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -76,7 +76,6 @@ class _ChatMediaEntryState extends State { targetMessageId: widget.message.messageId, ), ), - null, ); await twonlyDB.messagesDao.updateMessageId( widget.message.messageId, diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 8fbfb26..3a95cc1 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -65,7 +65,6 @@ class MessageContextMenu extends StatelessWidget { remove: false, ), ), - null, ); }, icon: FontAwesomeIcons.faceLaugh, @@ -124,7 +123,6 @@ class MessageContextMenu extends StatelessWidget { senderMessageId: message.messageId, ), ), - null, ); } else { await twonlyDB.messagesDao @@ -225,7 +223,6 @@ Future editTextMessage(BuildContext context, Message message) async { ), ), ), - null, ); } if (!context.mounted) return; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index c290f5f..41dc818 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -290,7 +290,6 @@ class _MediaViewerViewState extends State { targetMessageId: currentMessage!.messageId, ), ), - null, ); setState(() { imageSaved = true; diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart index 5ff1e3c..d6fc8d3 100644 --- a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -47,7 +47,6 @@ class _EmojiReactionWidgetState extends State { remove: false, ), ), - null, ); setState(() { diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart index e2b5acd..c4d81b6 100644 --- a/lib/src/views/chats/message_info.view.dart +++ b/lib/src/views/chats/message_info.view.dart @@ -122,34 +122,37 @@ class _MessageInfoViewState extends State { } columns.add( - Row( - children: [ - AvatarIcon( - contact: groupMember.$2, - fontSize: 15, - ), - const SizedBox(width: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + AvatarIcon( + contact: groupMember.$2, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getContactDisplayName(groupMember.$2), + style: const TextStyle(fontSize: 17), + ), + ], + ), + ), + Column( children: [ Text( - getContactDisplayName(groupMember.$2), - style: const TextStyle(fontSize: 17), + friendlyDateTime(context, actionAt), + style: const TextStyle(fontSize: 12), ), + Text(actionTypeText), ], ), - ), - Column( - children: [ - Text( - friendlyDateTime(context, actionAt), - style: const TextStyle(fontSize: 12), - ), - Text(actionTypeText), - ], - ), - ], + ], + ), ), ); } diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index 8b9924a..944cdba 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -3,7 +3,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; class AvatarIcon extends StatefulWidget { @@ -11,12 +10,14 @@ class AvatarIcon extends StatefulWidget { super.key, this.group, this.contact, + this.contactId, this.userData, this.fontSize = 20, this.color, }); final Group? group; final Contact? contact; + final int? contactId; final UserData? userData; final double? fontSize; final Color? color; @@ -54,6 +55,12 @@ class _AvatarIconState extends State { _avatarSVGs.add(widget.userData!.avatarSvg!); } else if (widget.contact?.avatarSvgCompressed != null) { _avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); + } else if (widget.contactId != null) { + final contact = + await twonlyDB.contactsDao.getContactById(widget.contactId!); + if (contact != null && contact.avatarSvgCompressed != null) { + _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); + } } if (mounted) setState(() {}); } @@ -62,6 +69,62 @@ class _AvatarIconState extends State { Widget build(BuildContext context) { final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2); + Widget avatars = SvgPicture.asset('assets/images/default_avatar.svg'); + + if (_avatarSVGs.length == 1) { + avatars = SvgPicture.string( + _avatarSVGs.first, + errorBuilder: (a, b, c) => avatars, + ); + } else if (_avatarSVGs.length >= 2) { + final a = SvgPicture.string( + _avatarSVGs.first, + errorBuilder: (a, b, c) => avatars, + ); + final b = SvgPicture.string( + _avatarSVGs[1], + errorBuilder: (a, b, c) => avatars, + ); + if (_avatarSVGs.length >= 3) { + final c = SvgPicture.string( + _avatarSVGs[2], + errorBuilder: (a, b, c) => avatars, + ); + avatars = Stack( + children: [ + Transform.translate( + offset: const Offset(-15, 5), + child: Transform.scale( + scale: 0.8, + child: c, + ), + ), + Transform.translate( + offset: const Offset(15, 5), + child: Transform.scale( + scale: 0.8, + child: b, + ), + ), + a, + ], + ); + } else { + avatars = Stack( + children: [ + Transform.translate( + offset: const Offset(-10, 5), + child: Transform.scale( + scale: 0.8, + child: b, + ), + ), + Transform.translate(offset: const Offset(10, 0), child: a), + ], + ); + } + } + return Container( constraints: BoxConstraints( minHeight: 2 * (widget.fontSize ?? 20), @@ -76,17 +139,7 @@ class _AvatarIconState extends State { height: proSize as double, width: proSize, color: widget.color, - child: Center( - child: _avatarSVGs.isEmpty - ? SvgPicture.asset('assets/images/default_avatar.svg') - : SvgPicture.string( - _avatarSVGs.first, - errorBuilder: (context, error, stackTrace) { - Log.error('$error'); - return Container(); - }, - ), - ), + child: Center(child: avatars), ), ), ), diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 7512489..122d014 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -46,9 +46,11 @@ class _FlameCounterWidgetState extends State { isBestFriend = gUser.myBestFriendGroupId == groupId; final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); flameCounterSub = stream.listen((counter) { - setState(() { - flameCounter = counter; - }); + if (mounted) { + setState(() { + flameCounter = counter; + }); + } }); } } diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart index 7c4b81c..8b97054 100644 --- a/lib/src/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -36,6 +36,8 @@ class _GroupCreateSelectGroupNameViewState await createNewGroup(textFieldGroupName.text, widget.selectedUsers); if (wasSuccess) { // POP + Navigator.popUntil(context, (route) => route.isFirst); + return; } setState(() { From 9eea69b3dd0157bb65923f9a5a69cba9e3b40d33 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Nov 2025 14:58:02 +0100 Subject: [PATCH 43/76] fixing analyzer --- lib/src/database/daos/groups.dao.dart | 6 ++-- lib/src/database/twonly.db.dart | 2 +- lib/src/services/api/server_messages.dart | 5 ++-- lib/src/services/group.services.dart | 16 ++++++---- .../group_create_select_group_name.view.dart | 4 ++- .../group_create_select_members.view.dart | 29 ++++++++++--------- 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 52a837c..60e0d37 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -67,14 +67,16 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ) async { await (update(groupMembers) ..where( - (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId))) + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), + )) .write(updates); } Future removeMember(String groupId, int contactId) async { await (delete(groupMembers) ..where( - (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId))) + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), + )) .go(); } diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 6b02b5c..aa8390e 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -44,7 +44,7 @@ part 'twonly.db.g.dart'; SignalContactPreKeys, SignalContactSignedPreKeys, MessageActions, - GroupHistories + GroupHistories, ], daos: [ MessagesDao, diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 344a419..b072b30 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:mutex/mutex.dart'; @@ -11,15 +12,15 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; -import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; +import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; import 'package:twonly/src/services/api/client2client/media.c2c.dart'; import 'package:twonly/src/services/api/client2client/messages.c2c.dart'; import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart'; import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart'; import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; +import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 201430a..4168dbe 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -161,14 +161,20 @@ Future fetchGroupState(Group group) async { final groupStateServer = GroupState.fromBuffer(response.bodyBytes); final envelope = EncryptedGroupStateEnvelop.fromBuffer( - groupStateServer.encryptedGroupState); + groupStateServer.encryptedGroupState, + ); final chacha20 = FlutterChacha20.poly1305Aead(); - final secretBox = SecretBox(envelope.encryptedGroupState, - nonce: envelope.nonce, mac: Mac(envelope.mac)); + final secretBox = SecretBox( + envelope.encryptedGroupState, + nonce: envelope.nonce, + mac: Mac(envelope.mac), + ); - final encryptedGroupStateRaw = await chacha20.decrypt(secretBox, - secretKey: SecretKey(group.stateEncryptionKey!)); + final encryptedGroupStateRaw = await chacha20.decrypt( + secretBox, + secretKey: SecretKey(group.stateEncryptionKey!), + ); final encryptedGroupState = EncryptedGroupState.fromBuffer(encryptedGroupStateRaw); diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart index 8b97054..e6cd4b2 100644 --- a/lib/src/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -36,7 +36,9 @@ class _GroupCreateSelectGroupNameViewState await createNewGroup(textFieldGroupName.text, widget.selectedUsers); if (wasSuccess) { // POP - Navigator.popUntil(context, (route) => route.isFirst); + if (mounted) { + Navigator.popUntil(context, (route) => route.isFirst); + } return; } diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index 2340746..103bdde 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -138,19 +138,20 @@ class _StartNewChatView extends State { ), child: SingleChildScrollView( child: LayoutBuilder( - builder: (context, constraints) { - // Wrap will use the available width from constraints.maxWidth - return Wrap( - spacing: 8, - children: selected.map((w) { - return _Chip( - contact: allContacts - .firstWhere((t) => t.userId == w), - onTap: toggleSelectedUser, - ); - }).toList(), - ); - }), + builder: (context, constraints) { + // Wrap will use the available width from constraints.maxWidth + return Wrap( + spacing: 8, + children: selected.map((w) { + return _Chip( + contact: allContacts + .firstWhere((t) => t.userId == w), + onTap: toggleSelectedUser, + ); + }).toList(), + ); + }, + ), ), ); } @@ -241,7 +242,7 @@ class _Chip extends StatelessWidget { FontAwesomeIcons.xmark, color: Colors.grey, size: 12, - ) + ), ], ), ), From 30086c2475dc1afe4f82e3b091dd29938dc04c38 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 1 Nov 2025 23:45:37 +0100 Subject: [PATCH 44/76] change group name possible #227 --- lib/src/database/daos/groups.dao.dart | 20 ++ lib/src/database/tables/groups.table.dart | 10 + lib/src/database/twonly.db.g.dart | 229 +++++++++++------- lib/src/localization/app_de.arb | 9 +- lib/src/localization/app_en.arb | 9 +- .../generated/app_localizations.dart | 42 ++++ .../generated/app_localizations_de.dart | 21 ++ .../generated/app_localizations_en.dart | 21 ++ .../api/client2client/groups.c2c.dart | 51 +++- lib/src/services/group.services.dart | 135 ++++++++++- .../chat_list_components/group_list_item.dart | 24 +- lib/src/views/chats/chat_messages.view.dart | 56 ++++- .../chat_group_action.dart | 84 +++++++ .../components/avatar_icon.component.dart | 1 + .../views/components/better_list_title.dart | 21 +- .../components/context_menu.component.dart | 4 +- lib/src/views/groups/group.view.dart | 218 ++++++++++++++++- .../views/groups/group_member.context.dart | 83 +++++++ 18 files changed, 923 insertions(+), 115 deletions(-) create mode 100644 lib/src/views/chats/chat_messages_components/chat_group_action.dart create mode 100644 lib/src/views/groups/group_member.context.dart diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 60e0d37..b1da33f 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -60,6 +60,13 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { await into(groupHistories).insert(insertAction); } + Stream> watchGroupActions(String groupId) { + return (select(groupHistories) + ..where((t) => t.groupId.equals(groupId)) + ..orderBy([(t) => OrderingTerm.asc(t.actionAt)])) + .watch(); + } + Future updateMember( String groupId, int contactId, @@ -139,6 +146,19 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return query.map((row) => row.readTable(contacts)).watch(); } + Stream> watchGroupMembers(String groupId) { + final query = + (select(groupMembers)..where((t) => t.groupId.equals(groupId))).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(groupMembers.contactId), + ), + ]); + return query + .map((row) => (row.readTable(contacts), row.readTable(groupMembers))) + .watch(); + } + Stream> watchGroups() { return select(groups).watch(); } diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 68d248a..ea25e46 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -82,6 +82,9 @@ class GroupHistories extends Table { TextColumn get groupId => text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); + IntColumn get contactId => + integer().nullable().references(Contacts, #userId)(); + IntColumn get affectedContactId => integer().nullable().references(Contacts, #userId)(); @@ -95,3 +98,10 @@ class GroupHistories extends Table { @override Set get primaryKey => {groupHistoryId}; } + +GroupActionType? groupActionTypeFromString(String name) { + for (final v in GroupActionType.values) { + if (v.name == name) return v; + } + return null; +} diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 76dd93a..f1589fe 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -6880,6 +6880,15 @@ class $GroupHistoriesTable extends GroupHistories requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); static const VerificationMeta _affectedContactIdMeta = const VerificationMeta('affectedContactId'); @override @@ -6918,6 +6927,7 @@ class $GroupHistoriesTable extends GroupHistories List get $columns => [ groupHistoryId, groupId, + contactId, affectedContactId, oldGroupName, newGroupName, @@ -6948,6 +6958,10 @@ class $GroupHistoriesTable extends GroupHistories } else if (isInserting) { context.missing(_groupIdMeta); } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } if (data.containsKey('affected_contact_id')) { context.handle( _affectedContactIdMeta, @@ -6983,6 +6997,8 @@ class $GroupHistoriesTable extends GroupHistories DriftSqlType.string, data['${effectivePrefix}group_history_id'])!, groupId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), affectedContactId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}affected_contact_id']), oldGroupName: attachedDatabase.typeMapping @@ -7009,6 +7025,7 @@ class $GroupHistoriesTable extends GroupHistories class GroupHistory extends DataClass implements Insertable { final String groupHistoryId; final String groupId; + final int? contactId; final int? affectedContactId; final String? oldGroupName; final String? newGroupName; @@ -7017,6 +7034,7 @@ class GroupHistory extends DataClass implements Insertable { const GroupHistory( {required this.groupHistoryId, required this.groupId, + this.contactId, this.affectedContactId, this.oldGroupName, this.newGroupName, @@ -7027,6 +7045,9 @@ class GroupHistory extends DataClass implements Insertable { final map = {}; map['group_history_id'] = Variable(groupHistoryId); map['group_id'] = Variable(groupId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } if (!nullToAbsent || affectedContactId != null) { map['affected_contact_id'] = Variable(affectedContactId); } @@ -7048,6 +7069,9 @@ class GroupHistory extends DataClass implements Insertable { return GroupHistoriesCompanion( groupHistoryId: Value(groupHistoryId), groupId: Value(groupId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), affectedContactId: affectedContactId == null && nullToAbsent ? const Value.absent() : Value(affectedContactId), @@ -7068,6 +7092,7 @@ class GroupHistory extends DataClass implements Insertable { return GroupHistory( groupHistoryId: serializer.fromJson(json['groupHistoryId']), groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), affectedContactId: serializer.fromJson(json['affectedContactId']), oldGroupName: serializer.fromJson(json['oldGroupName']), newGroupName: serializer.fromJson(json['newGroupName']), @@ -7082,6 +7107,7 @@ class GroupHistory extends DataClass implements Insertable { return { 'groupHistoryId': serializer.toJson(groupHistoryId), 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), 'affectedContactId': serializer.toJson(affectedContactId), 'oldGroupName': serializer.toJson(oldGroupName), 'newGroupName': serializer.toJson(newGroupName), @@ -7094,6 +7120,7 @@ class GroupHistory extends DataClass implements Insertable { GroupHistory copyWith( {String? groupHistoryId, String? groupId, + Value contactId = const Value.absent(), Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), @@ -7102,6 +7129,7 @@ class GroupHistory extends DataClass implements Insertable { GroupHistory( groupHistoryId: groupHistoryId ?? this.groupHistoryId, groupId: groupId ?? this.groupId, + contactId: contactId.present ? contactId.value : this.contactId, affectedContactId: affectedContactId.present ? affectedContactId.value : this.affectedContactId, @@ -7118,6 +7146,7 @@ class GroupHistory extends DataClass implements Insertable { ? data.groupHistoryId.value : this.groupHistoryId, groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, affectedContactId: data.affectedContactId.present ? data.affectedContactId.value : this.affectedContactId, @@ -7137,6 +7166,7 @@ class GroupHistory extends DataClass implements Insertable { return (StringBuffer('GroupHistory(') ..write('groupHistoryId: $groupHistoryId, ') ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') ..write('affectedContactId: $affectedContactId, ') ..write('oldGroupName: $oldGroupName, ') ..write('newGroupName: $newGroupName, ') @@ -7147,14 +7177,15 @@ class GroupHistory extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(groupHistoryId, groupId, affectedContactId, - oldGroupName, newGroupName, type, actionAt); + int get hashCode => Object.hash(groupHistoryId, groupId, contactId, + affectedContactId, oldGroupName, newGroupName, type, actionAt); @override bool operator ==(Object other) => identical(this, other) || (other is GroupHistory && other.groupHistoryId == this.groupHistoryId && other.groupId == this.groupId && + other.contactId == this.contactId && other.affectedContactId == this.affectedContactId && other.oldGroupName == this.oldGroupName && other.newGroupName == this.newGroupName && @@ -7165,6 +7196,7 @@ class GroupHistory extends DataClass implements Insertable { class GroupHistoriesCompanion extends UpdateCompanion { final Value groupHistoryId; final Value groupId; + final Value contactId; final Value affectedContactId; final Value oldGroupName; final Value newGroupName; @@ -7174,6 +7206,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { const GroupHistoriesCompanion({ this.groupHistoryId = const Value.absent(), this.groupId = const Value.absent(), + this.contactId = const Value.absent(), this.affectedContactId = const Value.absent(), this.oldGroupName = const Value.absent(), this.newGroupName = const Value.absent(), @@ -7184,6 +7217,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { GroupHistoriesCompanion.insert({ required String groupHistoryId, required String groupId, + this.contactId = const Value.absent(), this.affectedContactId = const Value.absent(), this.oldGroupName = const Value.absent(), this.newGroupName = const Value.absent(), @@ -7196,6 +7230,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { static Insertable custom({ Expression? groupHistoryId, Expression? groupId, + Expression? contactId, Expression? affectedContactId, Expression? oldGroupName, Expression? newGroupName, @@ -7206,6 +7241,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { return RawValuesInsertable({ if (groupHistoryId != null) 'group_history_id': groupHistoryId, if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, if (affectedContactId != null) 'affected_contact_id': affectedContactId, if (oldGroupName != null) 'old_group_name': oldGroupName, if (newGroupName != null) 'new_group_name': newGroupName, @@ -7218,6 +7254,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { GroupHistoriesCompanion copyWith( {Value? groupHistoryId, Value? groupId, + Value? contactId, Value? affectedContactId, Value? oldGroupName, Value? newGroupName, @@ -7227,6 +7264,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { return GroupHistoriesCompanion( groupHistoryId: groupHistoryId ?? this.groupHistoryId, groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, affectedContactId: affectedContactId ?? this.affectedContactId, oldGroupName: oldGroupName ?? this.oldGroupName, newGroupName: newGroupName ?? this.newGroupName, @@ -7245,6 +7283,9 @@ class GroupHistoriesCompanion extends UpdateCompanion { if (groupId.present) { map['group_id'] = Variable(groupId.value); } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } if (affectedContactId.present) { map['affected_contact_id'] = Variable(affectedContactId.value); } @@ -7272,6 +7313,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { return (StringBuffer('GroupHistoriesCompanion(') ..write('groupHistoryId: $groupHistoryId, ') ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') ..write('affectedContactId: $affectedContactId, ') ..write('oldGroupName: $oldGroupName, ') ..write('newGroupName: $newGroupName, ') @@ -7568,22 +7610,6 @@ final class $$ContactsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } - - static MultiTypedResultKey<$GroupHistoriesTable, List> - _groupHistoriesRefsTable(_$TwonlyDB db) => - MultiTypedResultKey.fromTable(db.groupHistories, - aliasName: $_aliasNameGenerator( - db.contacts.userId, db.groupHistories.affectedContactId)); - - $$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs { - final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories) - .filter((f) => f.affectedContactId.userId - .sqlEquals($_itemColumn('user_id')!)); - - final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } } class $$ContactsTableFilterComposer @@ -7766,27 +7792,6 @@ class $$ContactsTableFilterComposer )); return f(composer); } - - Expression groupHistoriesRefs( - Expression Function($$GroupHistoriesTableFilterComposer f) f) { - final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.userId, - referencedTable: $db.groupHistories, - getReferencedColumn: (t) => t.affectedContactId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$GroupHistoriesTableFilterComposer( - $db: $db, - $table: $db.groupHistories, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } } class $$ContactsTableOrderingComposer @@ -8020,27 +8025,6 @@ class $$ContactsTableAnnotationComposer )); return f(composer); } - - Expression groupHistoriesRefs( - Expression Function($$GroupHistoriesTableAnnotationComposer a) f) { - final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.userId, - referencedTable: $db.groupHistories, - getReferencedColumn: (t) => t.affectedContactId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$GroupHistoriesTableAnnotationComposer( - $db: $db, - $table: $db.groupHistories, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } } class $$ContactsTableTableManager extends RootTableManager< @@ -8060,8 +8044,7 @@ class $$ContactsTableTableManager extends RootTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs, - bool groupHistoriesRefs})> { + bool signalContactSignedPreKeysRefs})> { $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) : super(TableManagerState( db: db, @@ -8142,8 +8125,7 @@ class $$ContactsTableTableManager extends RootTableManager< groupMembersRefs = false, receiptsRefs = false, signalContactPreKeysRefs = false, - signalContactSignedPreKeysRefs = false, - groupHistoriesRefs = false}) { + signalContactSignedPreKeysRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ @@ -8153,8 +8135,7 @@ class $$ContactsTableTableManager extends RootTableManager< if (receiptsRefs) db.receipts, if (signalContactPreKeysRefs) db.signalContactPreKeys, if (signalContactSignedPreKeysRefs) - db.signalContactSignedPreKeys, - if (groupHistoriesRefs) db.groupHistories + db.signalContactSignedPreKeys ], addJoins: null, getPrefetchedDataCallback: (items) async { @@ -8234,19 +8215,6 @@ class $$ContactsTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.contactId == item.userId), - typedResults: items), - if (groupHistoriesRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$ContactsTableReferences - ._groupHistoriesRefsTable(db), - managerFromTypedResult: (p0) => - $$ContactsTableReferences(db, table, p0) - .groupHistoriesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.affectedContactId == item.userId), typedResults: items) ]; }, @@ -8272,8 +8240,7 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< bool groupMembersRefs, bool receiptsRefs, bool signalContactPreKeysRefs, - bool signalContactSignedPreKeysRefs, - bool groupHistoriesRefs})>; + bool signalContactSignedPreKeysRefs})>; typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ required String groupId, Value isGroupAdmin, @@ -13213,6 +13180,7 @@ typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion Function({ required String groupHistoryId, required String groupId, + Value contactId, Value affectedContactId, Value oldGroupName, Value newGroupName, @@ -13224,6 +13192,7 @@ typedef $$GroupHistoriesTableUpdateCompanionBuilder = GroupHistoriesCompanion Function({ Value groupHistoryId, Value groupId, + Value contactId, Value affectedContactId, Value oldGroupName, Value newGroupName, @@ -13251,6 +13220,21 @@ final class $$GroupHistoriesTableReferences manager.$state.copyWith(prefetchedData: [item])); } + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.groupHistories.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get contactId { + final $_column = $_itemColumn('contact_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + static $ContactsTable _affectedContactIdTable(_$TwonlyDB db) => db.contacts.createAlias($_aliasNameGenerator( db.groupHistories.affectedContactId, db.contacts.userId)); @@ -13314,6 +13298,26 @@ class $$GroupHistoriesTableFilterComposer return composer; } + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableFilterComposer get affectedContactId { final $$ContactsTableFilterComposer composer = $composerBuilder( composer: this, @@ -13382,6 +13386,26 @@ class $$GroupHistoriesTableOrderingComposer return composer; } + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableOrderingComposer get affectedContactId { final $$ContactsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -13447,6 +13471,26 @@ class $$GroupHistoriesTableAnnotationComposer return composer; } + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ContactsTableAnnotationComposer get affectedContactId { final $$ContactsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -13479,7 +13523,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< $$GroupHistoriesTableUpdateCompanionBuilder, (GroupHistory, $$GroupHistoriesTableReferences), GroupHistory, - PrefetchHooks Function({bool groupId, bool affectedContactId})> { + PrefetchHooks Function( + {bool groupId, bool contactId, bool affectedContactId})> { $$GroupHistoriesTableTableManager(_$TwonlyDB db, $GroupHistoriesTable table) : super(TableManagerState( db: db, @@ -13493,6 +13538,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< updateCompanionCallback: ({ Value groupHistoryId = const Value.absent(), Value groupId = const Value.absent(), + Value contactId = const Value.absent(), Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), @@ -13503,6 +13549,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< GroupHistoriesCompanion( groupHistoryId: groupHistoryId, groupId: groupId, + contactId: contactId, affectedContactId: affectedContactId, oldGroupName: oldGroupName, newGroupName: newGroupName, @@ -13513,6 +13560,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< createCompanionCallback: ({ required String groupHistoryId, required String groupId, + Value contactId = const Value.absent(), Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), @@ -13523,6 +13571,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< GroupHistoriesCompanion.insert( groupHistoryId: groupHistoryId, groupId: groupId, + contactId: contactId, affectedContactId: affectedContactId, oldGroupName: oldGroupName, newGroupName: newGroupName, @@ -13537,7 +13586,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< )) .toList(), prefetchHooksCallback: ( - {groupId = false, affectedContactId = false}) { + {groupId = false, contactId = false, affectedContactId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -13565,6 +13614,17 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< .groupId, ) as T; } + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$GroupHistoriesTableReferences._contactIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } if (affectedContactId) { state = state.withJoin( currentTable: table, @@ -13598,7 +13658,8 @@ typedef $$GroupHistoriesTableProcessedTableManager = ProcessedTableManager< $$GroupHistoriesTableUpdateCompanionBuilder, (GroupHistory, $$GroupHistoriesTableReferences), GroupHistory, - PrefetchHooks Function({bool groupId, bool affectedContactId})>; + PrefetchHooks Function( + {bool groupId, bool contactId, bool affectedContactId})>; class $TwonlyDBManager { final _$TwonlyDB _db; diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 3c84c47..5a3c614 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -365,5 +365,12 @@ "selectGroupName": "Gruppennamen wählen", "groupNameInput": "Gruppennamen", "groupMembers": "Mitglieder", - "createGroup": "Gruppe erstellen" + "createGroup": "Gruppe erstellen", + "addMember": "Mitglied hinzufügen", + "leaveGroup": "Gruppe verlassen", + "createContactRequest": "Kontaktanfrage erstellen", + "makeAdmin": "Zum Admin machen", + "removeAdmin": "Als Admin entfernen", + "removeFromGroup": "Aus Gruppe entfernen", + "admin": "Admin" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 489cb1c..eb1cc6c 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -521,5 +521,12 @@ "selectGroupName": "Select group name", "groupNameInput": "Group name", "groupMembers": "Members", - "createGroup": "Create group" + "addMember": "Add member", + "createGroup": "Create group", + "leaveGroup": "Leave group", + "createContactRequest": "Create contact request", + "makeAdmin": "Make admin", + "removeAdmin": "Remove as admin", + "removeFromGroup": "Remove from group", + "admin": "Admin" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 3466610..cda4669 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2228,11 +2228,53 @@ abstract class AppLocalizations { /// **'Members'** String get groupMembers; + /// No description provided for @addMember. + /// + /// In en, this message translates to: + /// **'Add member'** + String get addMember; + /// No description provided for @createGroup. /// /// In en, this message translates to: /// **'Create group'** String get createGroup; + + /// No description provided for @leaveGroup. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroup; + + /// No description provided for @createContactRequest. + /// + /// In en, this message translates to: + /// **'Create contact request'** + String get createContactRequest; + + /// No description provided for @makeAdmin. + /// + /// In en, this message translates to: + /// **'Make admin'** + String get makeAdmin; + + /// No description provided for @removeAdmin. + /// + /// In en, this message translates to: + /// **'Remove as admin'** + String get removeAdmin; + + /// No description provided for @removeFromGroup. + /// + /// In en, this message translates to: + /// **'Remove from group'** + String get removeFromGroup; + + /// No description provided for @admin. + /// + /// In en, this message translates to: + /// **'Admin'** + String get admin; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 3d16a0d..f06b19a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1181,6 +1181,27 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groupMembers => 'Mitglieder'; + @override + String get addMember => 'Mitglied hinzufügen'; + @override String get createGroup => 'Gruppe erstellen'; + + @override + String get leaveGroup => 'Gruppe verlassen'; + + @override + String get createContactRequest => 'Kontaktanfrage erstellen'; + + @override + String get makeAdmin => 'Zum Admin machen'; + + @override + String get removeAdmin => 'Als Admin entfernen'; + + @override + String get removeFromGroup => 'Aus Gruppe entfernen'; + + @override + String get admin => 'Admin'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 1c61b1f..7ab19e8 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1174,6 +1174,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groupMembers => 'Members'; + @override + String get addMember => 'Add member'; + @override String get createGroup => 'Create group'; + + @override + String get leaveGroup => 'Leave group'; + + @override + String get createContactRequest => 'Create contact request'; + + @override + String get makeAdmin => 'Make admin'; + + @override + String get removeAdmin => 'Remove as admin'; + + @override + String get removeFromGroup => 'Remove from group'; + + @override + String get admin => 'Admin'; } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 6f81859..5e18672 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; @@ -29,7 +28,7 @@ Future handleGroupCreate( groupId: Value(groupId), stateVersionId: const Value(0), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), - myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + myGroupPrivateKey: Value(myGroupKey.serialize()), groupName: const Value(''), joinedGroup: const Value(false), ), @@ -81,7 +80,53 @@ Future handleGroupUpdate( int fromUserId, String groupId, EncryptedContent_GroupUpdate update, -) async {} +) async { + Log.info('Got group update for $groupId from $fromUserId'); + + final actionType = groupActionTypeFromString(update.groupActionType); + if (actionType == null) { + Log.error('Group action ${update.groupActionType} is unknown ignoring.'); + return; + } + + final group = (await twonlyDB.groupsDao.getGroup(groupId))!; + + switch (actionType) { + case GroupActionType.updatedGroupName: + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + oldGroupName: Value(group.groupName), + newGroupName: Value(update.newGroupName), + contactId: Value(fromUserId), + ), + ); + case GroupActionType.removedMember: + case GroupActionType.addMember: + case GroupActionType.leftGroup: + case GroupActionType.promoteToAdmin: + case GroupActionType.demoteToMember: + int? affectedContactId = update.affectedContactId.toInt(); + + if (affectedContactId == gUser.userId) { + affectedContactId = null; + } + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + affectedContactId: Value(affectedContactId), + contactId: Value(fromUserId), + ), + ); + case GroupActionType.createdGroup: + break; + } + + unawaited(fetchGroupState(group)); +} Future handleGroupJoin( int fromUserId, diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 4168dbe..5c76bbc 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; + import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -8,6 +9,8 @@ import 'package:fixnum/fixnum.dart'; import 'package:hashlib/random.dart'; import 'package:http/http.dart' as http; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +// ignore: implementation_imports +import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -23,6 +26,10 @@ String getGroupStateUrl() { return 'http${apiService.apiSecure}://${apiService.apiHost}/api/group/state'; } +String getGroupChallengeUrl() { + return 'http${apiService.apiSecure}://${apiService.apiHost}/api/group/challenge'; +} + Future createNewGroup(String groupName, List members) async { // First: Upload new State to the server..... // if (groupName) return; @@ -90,7 +97,7 @@ Future createNewGroup(String groupName, List members) async { isGroupAdmin: const Value(true), stateEncryptionKey: Value(stateEncryptionKey), stateVersionId: const Value(1), - myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()), + myGroupPrivateKey: Value(myGroupKey.serialize()), joinedGroup: const Value(true), ), ); @@ -142,7 +149,7 @@ Future fetchGroupStatesForUnjoinedGroups() async { } } -Future fetchGroupState(Group group) async { +Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { try { var isSuccess = true; @@ -156,7 +163,7 @@ Future fetchGroupState(Group group) async { Log.error( 'Could not load group state. Got status code ${response.statusCode} from server.', ); - return false; + return null; } final groupStateServer = GroupState.fromBuffer(response.bodyBytes); @@ -180,8 +187,10 @@ Future fetchGroupState(Group group) async { EncryptedGroupState.fromBuffer(encryptedGroupStateRaw); if (group.stateVersionId >= groupStateServer.versionId.toInt()) { - Log.error('Group ${group.groupId} has newest group state'); - return false; + Log.info( + 'Group ${group.groupId} has already newest group state from the server!', + ); + return (groupStateServer.versionId.toInt(), encryptedGroupState); } final isGroupAdmin = encryptedGroupState.adminIds @@ -204,6 +213,9 @@ Future fetchGroupState(Group group) async { // First find and insert NEW members for (final memberId in encryptedGroupState.memberIds) { + if (memberId == Int64(gUser.userId)) { + continue; + } if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) { // User is already in the database continue; @@ -280,10 +292,10 @@ Future fetchGroupState(Group group) async { ), ); } - return true; + return (groupStateServer.versionId.toInt(), encryptedGroupState); } catch (e) { Log.error(e); - return false; + return null; } } @@ -304,3 +316,112 @@ Future addNewHiddenContact(int contactId) async { await createNewSignalSession(userData); return true; } + +Future updateGroupState(Group group, EncryptedGroupState state) async { + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final secretBox = await chacha20.encrypt( + state.writeToBuffer(), + secretKey: SecretKey(group.stateEncryptionKey!), + nonce: encryptionNonce, + ); + + final encryptedGroupState = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + { + // Upload the group state, if this fails, the group can not be created. + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final publicKey = uint8ListToHex(keyPair.getPublicKey().serialize()); + + final responseNonce = await http + .get( + Uri.parse('${getGroupChallengeUrl()}/$publicKey'), + ) + .timeout(const Duration(seconds: 10)); + + if (responseNonce.statusCode != 200) { + Log.error( + 'Could not load nonce. Got status code ${responseNonce.statusCode} from server.', + ); + return false; + } + + final updateTBS = UpdateGroupState_UpdateTBS( + versionId: Int64(group.stateVersionId + 1), + encryptedGroupState: encryptedGroupState.writeToBuffer(), + publicKey: keyPair.getPublicKey().serialize(), + nonce: responseNonce.bodyBytes, + ); + + final random = getRandomUint8List(32); + final signature = sign( + keyPair.getPrivateKey().serialize(), + updateTBS.writeToBuffer(), + random, + ); + + final newGroupState = UpdateGroupState( + update: updateTBS, + signature: signature, + ); + + final response = await http + .patch( + Uri.parse(getGroupStateUrl()), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not patch group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + + // Update database to the newest state + return (await fetchGroupState(group)) != null; +} + +Future updateGroupeName(Group group, String groupName) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + state.groupName = groupName; + + // send new state to the server + if (!await updateGroupState(group, state)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.updatedGroupName.name, + newGroupName: groupName, + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.updatedGroupName), + oldGroupName: Value(group.groupName), + newGroupName: Value(groupName), + ), + ); + + return true; +} diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 729ba02..c903069 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -17,6 +17,8 @@ import 'package:twonly/src/views/chats/media_viewer.view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/group_context_menu.component.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; class GroupListItem extends StatefulWidget { const GroupListItem({ @@ -232,7 +234,27 @@ class _UserListItem extends State { ), ], ), - leading: AvatarIcon(group: widget.group), + leading: GestureDetector( + onTap: () async { + Widget pushWidget = GroupView(widget.group); + + if (widget.group.isDirectChat) { + final contacts = await twonlyDB.groupsDao + .getGroupContact(widget.group.groupId); + pushWidget = ContactView(contacts.first.userId); + } + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return pushWidget; + }, + ), + ); + }, + child: AvatarIcon(group: widget.group), + ), trailing: IconButton( onPressed: () { Navigator.push( diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index f753800..94bc7fc 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -14,6 +14,7 @@ import 'package:twonly/src/services/notifications/background.notifications.dart' import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; @@ -30,7 +31,12 @@ Color getMessageColor(Message message) { } class ChatItem { - const ChatItem._({this.message, this.date, this.lastOpenedPosition}); + const ChatItem._({ + this.message, + this.date, + this.lastOpenedPosition, + this.groupAction, + }); factory ChatItem.date(DateTime date) { return ChatItem._(date: date); } @@ -40,11 +46,16 @@ class ChatItem { 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; } @@ -65,12 +76,14 @@ class _ChatMessagesViewState extends State { String currentInputText = ''; late StreamSubscription userSub; late StreamSubscription> messageSub; + late StreamSubscription>? groupActionsSub; late StreamSubscription>>? lastOpenedMessageByContactSub; List messages = []; List allMessages = []; List<(Message, Contact)> lastOpenedMessageByContact = []; + List groupActions = []; List galleryItems = []; Message? quotesMessage; GlobalKey verifyShieldKey = GlobalKey(); @@ -97,6 +110,7 @@ class _ChatMessagesViewState extends State { void dispose() { userSub.cancel(); messageSub.cancel(); + groupActionsSub?.cancel(); lastOpenedMessageByContactSub?.cancel(); tutorial?.cancel(); textFieldFocus.dispose(); @@ -121,7 +135,13 @@ class _ChatMessagesViewState extends State { lastOpenedStream.listen((lastActionsFuture) async { final update = await lastActionsFuture; lastOpenedMessageByContact = update; - await setMessages(allMessages, 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); }); } @@ -135,7 +155,7 @@ class _ChatMessagesViewState extends State { // return; } await protectMessageUpdating.protect(() async { - await setMessages(update, lastOpenedMessageByContact); + await setMessages(update, lastOpenedMessageByContact, groupActions); }); }); } @@ -143,6 +163,7 @@ class _ChatMessagesViewState extends State { Future setMessages( List newMessages, List<(Message, Contact)> lastOpenedMessageByContact, + List groupActions, ) async { await flutterLocalNotificationsPlugin.cancelAll(); @@ -165,8 +186,20 @@ class _ChatMessagesViewState extends State { } } var index = 0; + var groupHistoryIndex = 0; for (final msg in newMessages) { + if (groupHistoryIndex < groupActions.length) { + for (; groupHistoryIndex < groupActions.length; groupHistoryIndex++) { + if (msg.createdAt.isAfter(groupActions[groupHistoryIndex].actionAt)) { + chatItems + .add(ChatItem.groupAction(groupActions[groupHistoryIndex])); + // groupHistoryIndex++; + } else { + break; + } + } + } index += 1; if (msg.type == MessageType.text && msg.senderId != null && @@ -200,6 +233,11 @@ class _ChatMessagesViewState extends State { } } } + if (groupHistoryIndex < groupActions.length) { + for (var i = groupHistoryIndex; i < groupActions.length; i++) { + chatItems.add(ChatItem.groupAction(groupActions[i])); + } + } for (final contactId in openedMessages.keys) { await notifyContactAboutOpeningMessage( @@ -262,9 +300,9 @@ class _ChatMessagesViewState extends State { appBar: AppBar( title: GestureDetector( onTap: () async { - if (widget.group.isDirectChat) { - final member = await twonlyDB.groupsDao - .getGroupMembers(widget.group.groupId); + if (group.isDirectChat) { + final member = + await twonlyDB.groupsDao.getGroupMembers(group.groupId); if (!context.mounted) return; await Navigator.push( context, @@ -279,7 +317,7 @@ class _ChatMessagesViewState extends State { context, MaterialPageRoute( builder: (context) { - return GroupView(widget.group); + return GroupView(group); }, ), ); @@ -298,7 +336,7 @@ class _ChatMessagesViewState extends State { child: Row( children: [ Text( - substringBy(widget.group.groupName, 20), + substringBy(group.groupName, 20), ), const SizedBox(width: 10), VerifiedShield(key: verifyShieldKey, group: group), @@ -342,6 +380,8 @@ class _ChatMessagesViewState extends State { ); }).toList(), ); + } else if (messages[i].isGroupAction) { + return ChatGroupAction(action: messages[i].groupAction!); } else { final chatMessage = messages[i].message!; return Transform.translate( diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart new file mode 100644 index 0000000..0f7fa9d --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +class ChatGroupAction extends StatefulWidget { + const ChatGroupAction({ + required this.action, + super.key, + }); + + final GroupHistory action; + + @override + State createState() => _ChatGroupActionState(); +} + +class _ChatGroupActionState extends State { + Contact? contact; + Contact? affectedContact; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + if (widget.action.contactId == null) return; + contact = + await twonlyDB.contactsDao.getContactById(widget.action.contactId!); + + if (widget.action.affectedContactId == null) return; + affectedContact = await twonlyDB.contactsDao + .getContactById(widget.action.affectedContactId!); + + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + var text = ''; + + if (widget.action.type == GroupActionType.updatedGroupName) { + if (contact == null) { + text = + 'You have changed the group name to "${widget.action.newGroupName}".'; + } else { + text = + '${getContactDisplayName(contact!)} has changed the group name to "${widget.action.newGroupName}".'; + } + } + + if (text == '') return Container(); + + return Padding( + padding: const EdgeInsets.all(8), + child: Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + const WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: FaIcon( + FontAwesomeIcons.pencil, + size: 10, + color: Colors.grey, + ), + ), + const WidgetSpan(child: SizedBox(width: 8)), + TextSpan( + text: text, + style: const TextStyle(color: Colors.grey), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index 944cdba..2bab8f5 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -126,6 +126,7 @@ class _AvatarIconState extends State { } return Container( + key: GlobalKey(), constraints: BoxConstraints( minHeight: 2 * (widget.fontSize ?? 20), minWidth: 2 * (widget.fontSize ?? 20), diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index c83eca2..50fb542 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -3,16 +3,20 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class BetterListTile extends StatelessWidget { const BetterListTile({ - required this.icon, required this.text, required this.onTap, + this.icon, + this.leading, super.key, this.color, this.subtitle, + this.trailing, this.iconSize = 20, this.padding, }); - final IconData icon; + final IconData? icon; + final Widget? leading; + final Widget? trailing; final String text; final Widget? subtitle; final Color? color; @@ -30,12 +34,15 @@ class BetterListTile extends StatelessWidget { left: 19, ) : padding!, - child: FaIcon( - icon, - size: iconSize, - color: color, - ), + child: (leading != null) + ? leading + : FaIcon( + icon, + size: iconSize, + color: color, + ), ), + trailing: trailing, title: Text( text, style: TextStyle(color: color), diff --git a/lib/src/views/components/context_menu.component.dart b/lib/src/views/components/context_menu.component.dart index c2bd12e..5e6fd62 100644 --- a/lib/src/views/components/context_menu.component.dart +++ b/lib/src/views/components/context_menu.component.dart @@ -47,7 +47,7 @@ class _ContextMenuState extends State { elevation: 1, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), // corner radius + borderRadius: BorderRadius.circular(12), ), popUpAnimationStyle: const AnimationStyle( duration: Duration.zero, @@ -56,7 +56,7 @@ class _ContextMenuState extends State { items: >[ ...widget.items.map( (item) => PopupMenuItem( - padding: EdgeInsets.zero, + padding: const EdgeInsets.only(right: 4), child: ListTile( title: Text(item.title), onTap: () async { diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 85aa283..57c47ec 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -1,5 +1,18 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/components/verified_shield.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group_member.context.dart'; +import 'package:twonly/src/views/settings/profile/profile.view.dart'; class GroupView extends StatefulWidget { const GroupView(this.group, {super.key}); @@ -11,8 +24,211 @@ class GroupView extends StatefulWidget { } class _GroupViewState extends State { + late Group group; + + List<(Contact, GroupMember)> members = []; + + late StreamSubscription groupSub; + late StreamSubscription> membersSub; + + @override + void initState() { + group = widget.group; + initAsync(); + super.initState(); + } + + @override + void dispose() { + groupSub.cancel(); + membersSub.cancel(); + super.dispose(); + } + + Future initAsync() async { + final groupStream = twonlyDB.groupsDao.watchGroup(widget.group.groupId); + groupSub = groupStream.listen((update) { + if (update != null) { + setState(() { + group = update; + }); + } + }); + final membersStream = + twonlyDB.groupsDao.watchGroupMembers(widget.group.groupId); + membersSub = membersStream.listen((update) { + setState(() { + members = update; + members.sort( + (b, a) => a.$2.memberState!.index.compareTo(b.$2.memberState!.index), + ); + }); + }); + } + + Future _updateGroupName() async { + final newGroupName = await showGroupNameChangeDialog(context, group); + + if (context.mounted && + newGroupName != null && + newGroupName != '' && + newGroupName != group.groupName) { + if (!await updateGroupeName(group, newGroupName)) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Network issue. Try again later.'), + duration: Duration(seconds: 3), + ), + ); + } + } + } + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: AvatarIcon( + group: group, + fontSize: 30, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: VerifiedShield(key: GlobalKey(), group: group), + ), + Text( + substringBy(group.groupName, 25), + style: const TextStyle(fontSize: 20), + ), + ], + ), + const SizedBox(height: 50), + if (group.isGroupAdmin) + BetterListTile( + icon: FontAwesomeIcons.pencil, + text: context.lang.groupNameInput, + onTap: _updateGroupName, + ), + const Divider(), + ListTile( + title: Padding( + padding: const EdgeInsets.only(left: 17), + child: Text( + '${members.length + 1} ${context.lang.groupMembers}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + if (group.isGroupAdmin) + BetterListTile( + icon: FontAwesomeIcons.plus, + text: context.lang.addMember, + onTap: () => {}, + ), + BetterListTile( + padding: const EdgeInsets.only(left: 13), + leading: AvatarIcon( + userData: gUser, + fontSize: 16, + ), + text: context.lang.you, + trailing: (group.isGroupAdmin) ? Text(context.lang.admin) : null, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileView(), + ), + ); + }, + ), + ...members.map((member) { + return GroupMemberContextMenu( + group: widget.group, + contact: member.$1, + member: member.$2, + child: BetterListTile( + padding: const EdgeInsets.only(left: 13), + leading: AvatarIcon( + contact: member.$1, + fontSize: 16, + ), + text: getContactDisplayName(member.$1, maxLength: 25), + trailing: (member.$2.memberState == MemberState.admin) + ? Text(context.lang.admin) + : null, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ContactView(member.$1.userId), + ), + ); + }, + ), + ); + }), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + BetterListTile( + icon: FontAwesomeIcons.rightFromBracket, + color: Colors.red, + text: context.lang.leaveGroup, + onTap: () => {}, + ), + ], + ), + ); } } + +Future showGroupNameChangeDialog( + BuildContext context, + Group group, +) { + final controller = TextEditingController(text: group.groupName); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(context.lang.groupNameInput), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration(hintText: context.lang.groupNameInput), + ), + actions: [ + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(context.lang.ok), + onPressed: () { + Navigator.of(context).pop(controller.text); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart new file mode 100644 index 0000000..4807e2f --- /dev/null +++ b/lib/src/views/groups/group_member.context.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; + +class GroupMemberContextMenu extends StatelessWidget { + const GroupMemberContextMenu({ + required this.contact, + required this.member, + required this.child, + required this.group, + super.key, + }); + final Contact contact; + final GroupMember member; + final Group group; + final Widget child; + + @override + Widget build(BuildContext context) { + return ContextMenu( + items: [ + if (contact.accepted) + ContextMenuItem( + title: context.lang.contextMenuOpenChat, + onTap: () async { + final directChat = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + if (directChat == null) { + // create + return; + } + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatMessagesView(directChat), + ), + ); + }, + icon: FontAwesomeIcons.message, + ), + if (!contact.accepted) + ContextMenuItem( + title: context.lang.createContactRequest, + onTap: () async { + // onResponseTriggered(); + }, + icon: FontAwesomeIcons.userPlus, + ), + if (group.isGroupAdmin && member.memberState == MemberState.normal) + ContextMenuItem( + title: context.lang.makeAdmin, + onTap: () async { + // onResponseTriggered(); + }, + icon: FontAwesomeIcons.key, + ), + if (group.isGroupAdmin && member.memberState == MemberState.admin) + ContextMenuItem( + title: context.lang.removeAdmin, + onTap: () async { + // onResponseTriggered(); + }, + icon: FontAwesomeIcons.key, + ), + if (group.isGroupAdmin) + ContextMenuItem( + title: context.lang.removeFromGroup, + onTap: () async { + // onResponseTriggered(); + }, + icon: FontAwesomeIcons.rightFromBracket, + ), + ], + child: child, + ); + } +} From 8cf57224ce597deecbdab46272fc01f054cb24d6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 14:09:18 +0100 Subject: [PATCH 45/76] make and remove admin rights --- lib/src/localization/app_de.arb | 7 +- lib/src/localization/app_en.arb | 7 +- .../generated/app_localizations.dart | 30 +++++++ .../generated/app_localizations_de.dart | 21 +++++ .../generated/app_localizations_en.dart | 21 +++++ lib/src/services/group.services.dart | 83 ++++++++++++++++++- .../chat_group_action.dart | 46 ++++++---- lib/src/views/groups/group.view.dart | 20 +++-- .../views/groups/group_member.context.dart | 53 +++++++++++- 9 files changed, 260 insertions(+), 28 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 5a3c614..16565cd 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -372,5 +372,10 @@ "makeAdmin": "Zum Admin machen", "removeAdmin": "Als Admin entfernen", "removeFromGroup": "Aus Gruppe entfernen", - "admin": "Admin" + "admin": "Admin", + "revokeAdminRightsTitle": "Adminrechte von {username} entfernen?", + "revokeAdminRightsOkBtn": "Als Admin entfernen", + "makeAdminRightsTitle": "{username} zum Admin machen?", + "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", + "makeAdminRightsOkBtn": "Zum Admin machen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index eb1cc6c..e0e3870 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -528,5 +528,10 @@ "makeAdmin": "Make admin", "removeAdmin": "Remove as admin", "removeFromGroup": "Remove from group", - "admin": "Admin" + "admin": "Admin", + "revokeAdminRightsTitle": "Revoke {username}'s admin rights?", + "revokeAdminRightsOkBtn": "Remove as admin", + "makeAdminRightsTitle": "Make {username} an admin?", + "makeAdminRightsBody": "{username} will be able to edit this group and its members.", + "makeAdminRightsOkBtn": "Make admin" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index cda4669..7bac4bb 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2275,6 +2275,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Admin'** String get admin; + + /// No description provided for @revokeAdminRightsTitle. + /// + /// In en, this message translates to: + /// **'Revoke {username}\'s admin rights?'** + String revokeAdminRightsTitle(Object username); + + /// No description provided for @revokeAdminRightsOkBtn. + /// + /// In en, this message translates to: + /// **'Remove as admin'** + String get revokeAdminRightsOkBtn; + + /// No description provided for @makeAdminRightsTitle. + /// + /// In en, this message translates to: + /// **'Make {username} an admin?'** + String makeAdminRightsTitle(Object username); + + /// No description provided for @makeAdminRightsBody. + /// + /// In en, this message translates to: + /// **'{username} will be able to edit this group and its members.'** + String makeAdminRightsBody(Object username); + + /// No description provided for @makeAdminRightsOkBtn. + /// + /// In en, this message translates to: + /// **'Make admin'** + String get makeAdminRightsOkBtn; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index f06b19a..cb7374a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1204,4 +1204,25 @@ class AppLocalizationsDe extends AppLocalizations { @override String get admin => 'Admin'; + + @override + String revokeAdminRightsTitle(Object username) { + return 'Adminrechte von $username entfernen?'; + } + + @override + String get revokeAdminRightsOkBtn => 'Als Admin entfernen'; + + @override + String makeAdminRightsTitle(Object username) { + return '$username zum Admin machen?'; + } + + @override + String makeAdminRightsBody(Object username) { + return '$username wird diese Gruppe und ihre Mitglieder bearbeiten können.'; + } + + @override + String get makeAdminRightsOkBtn => 'Zum Admin machen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 7ab19e8..99ccfc3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1197,4 +1197,25 @@ class AppLocalizationsEn extends AppLocalizations { @override String get admin => 'Admin'; + + @override + String revokeAdminRightsTitle(Object username) { + return 'Revoke $username\'s admin rights?'; + } + + @override + String get revokeAdminRightsOkBtn => 'Remove as admin'; + + @override + String makeAdminRightsTitle(Object username) { + return 'Make $username an admin?'; + } + + @override + String makeAdminRightsBody(Object username) { + return '$username will be able to edit this group and its members.'; + } + + @override + String get makeAdminRightsOkBtn => 'Make admin'; } diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 5c76bbc..9f7d25a 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; @@ -317,7 +318,12 @@ Future addNewHiddenContact(int contactId) async { return true; } -Future updateGroupState(Group group, EncryptedGroupState state) async { +Future updateGroupState( + Group group, + EncryptedGroupState state, { + Uint8List? addAdmin, + Uint8List? removeAdmin, +}) async { final chacha20 = FlutterChacha20.poly1305Aead(); final encryptionNonce = chacha20.newNonce(); @@ -358,6 +364,8 @@ Future updateGroupState(Group group, EncryptedGroupState state) async { encryptedGroupState: encryptedGroupState.writeToBuffer(), publicKey: keyPair.getPublicKey().serialize(), nonce: responseNonce.bodyBytes, + addAdmin: addAdmin, + removeAdmin: removeAdmin, ); final random = getRandomUint8List(32); @@ -391,6 +399,79 @@ Future updateGroupState(Group group, EncryptedGroupState state) async { return (await fetchGroupState(group)) != null; } +Future manageAdminState( + Group group, + GroupMember member, + int contactId, + bool remove, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + final userId = Int64(contactId); + + Uint8List? addAdmin; + Uint8List? removeAdmin; + + if (remove) { + if (state.adminIds.contains(userId)) { + state.adminIds.remove(userId); + removeAdmin = member.groupPublicKey; + } else { + Log.info('User was already removed as admin.'); + return true; + } + } else { + if (!state.adminIds.contains(userId)) { + state.adminIds.add(userId); + addAdmin = member.groupPublicKey; + } else { + Log.info('User is already admin.'); + return true; + } + } + + if (addAdmin == null && removeAdmin == null) { + Log.info('User does not have a group public key.'); + return false; + } + + // send new state to the server + if (!await updateGroupState( + group, + state, + addAdmin: addAdmin, + removeAdmin: removeAdmin, + )) { + return false; + } + + final groupActionType = + remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin; + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: groupActionType.name, + affectedContactId: Int64(contactId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: Value(groupActionType), + affectedContactId: Value(contactId), + ), + ); + + return true; +} + Future updateGroupeName(Group group, String groupName) async { // ensure the latest state is used final currentState = await fetchGroupState(group); diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index 0f7fa9d..f748f7e 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -28,13 +28,15 @@ class _ChatGroupActionState extends State { } Future initAsync() async { - if (widget.action.contactId == null) return; - contact = - await twonlyDB.contactsDao.getContactById(widget.action.contactId!); + if (widget.action.contactId != null) { + contact = + await twonlyDB.contactsDao.getContactById(widget.action.contactId!); + } - if (widget.action.affectedContactId == null) return; - affectedContact = await twonlyDB.contactsDao - .getContactById(widget.action.affectedContactId!); + if (widget.action.affectedContactId != null) { + affectedContact = await twonlyDB.contactsDao + .getContactById(widget.action.affectedContactId!); + } if (mounted) setState(() {}); } @@ -43,14 +45,30 @@ class _ChatGroupActionState extends State { Widget build(BuildContext context) { var text = ''; - if (widget.action.type == GroupActionType.updatedGroupName) { - if (contact == null) { - text = - 'You have changed the group name to "${widget.action.newGroupName}".'; - } else { - text = - '${getContactDisplayName(contact!)} has changed the group name to "${widget.action.newGroupName}".'; - } + final affected = (affectedContact == null) + ? 'you' + : "${getContactDisplayName(affectedContact!)}'s"; + final affectedR = (affectedContact == null) ? 'your' : affected; + final maker = (contact == null) ? '' : getContactDisplayName(contact!); + + switch (widget.action.type) { + case GroupActionType.updatedGroupName: + text = (contact == null) + ? 'You have changed the group name to "${widget.action.newGroupName}".' + : '$maker has changed the group name to "${widget.action.newGroupName}".'; + case GroupActionType.createdGroup: + case GroupActionType.removedMember: + case GroupActionType.addMember: + case GroupActionType.leftGroup: + break; + case GroupActionType.promoteToAdmin: + text = (contact == null) + ? 'You made $affected an admin.' + : '$maker made $affected an admin.'; + case GroupActionType.demoteToMember: + text = (contact == null) + ? 'You revoked $affected admin rights.' + : '$maker revoked $affectedR admin rights.'; } if (text == '') return Container(); diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 57c47ec..87bb0b4 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -75,12 +75,7 @@ class _GroupViewState extends State { newGroupName != group.groupName) { if (!await updateGroupeName(group, newGroupName)) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Network issue. Try again later.'), - duration: Duration(seconds: 3), - ), - ); + showNetworkIssue(context); } } } @@ -143,6 +138,7 @@ class _GroupViewState extends State { BetterListTile( padding: const EdgeInsets.only(left: 13), leading: AvatarIcon( + key: GlobalKey(), userData: gUser, fontSize: 16, ), @@ -159,12 +155,13 @@ class _GroupViewState extends State { ), ...members.map((member) { return GroupMemberContextMenu( - group: widget.group, + group: group, contact: member.$1, member: member.$2, child: BetterListTile( padding: const EdgeInsets.only(left: 13), leading: AvatarIcon( + key: GlobalKey(), contact: member.$1, fontSize: 16, ), @@ -232,3 +229,12 @@ Future showGroupNameChangeDialog( }, ); } + +void showNetworkIssue(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Network issue. Try again later.'), + duration: Duration(seconds: 3), + ), + ); +} diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart index 4807e2f..1b39a69 100644 --- a/lib/src/views/groups/group_member.context.dart +++ b/lib/src/views/groups/group_member.context.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/context_menu.component.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; class GroupMemberContextMenu extends StatelessWidget { const GroupMemberContextMenu({ @@ -52,19 +56,60 @@ class GroupMemberContextMenu extends StatelessWidget { }, icon: FontAwesomeIcons.userPlus, ), - if (group.isGroupAdmin && member.memberState == MemberState.normal) + if (member.groupPublicKey != null && + group.isGroupAdmin && + member.memberState == MemberState.normal) ContextMenuItem( title: context.lang.makeAdmin, onTap: () async { - // onResponseTriggered(); + final ok = await showAlertDialog( + context, + context.lang + .makeAdminRightsTitle(getContactDisplayName(contact)), + context.lang + .makeAdminRightsBody(getContactDisplayName(contact)), + customOk: context.lang.makeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + false, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } }, icon: FontAwesomeIcons.key, ), - if (group.isGroupAdmin && member.memberState == MemberState.admin) + if (member.groupPublicKey != null && + group.isGroupAdmin && + member.memberState == MemberState.admin) ContextMenuItem( title: context.lang.removeAdmin, onTap: () async { - // onResponseTriggered(); + final ok = await showAlertDialog( + context, + context.lang + .revokeAdminRightsTitle(getContactDisplayName(contact)), + '', + customOk: context.lang.revokeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + true, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } }, icon: FontAwesomeIcons.key, ), From d616e08dec1ddb24c879ea306397e9d7fbfd786e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 14:29:09 +0100 Subject: [PATCH 46/76] store group creation action --- lib/src/services/api.service.dart | 2 ++ .../api/client2client/groups.c2c.dart | 29 ++++++++++++------- lib/src/views/chats/chat_messages.view.dart | 5 +++- .../chat_group_action.dart | 18 ++++++++++-- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 35debca..05d8d88 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -30,6 +30,7 @@ import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; @@ -99,6 +100,7 @@ class ApiService { unawaited(syncFlameCounters()); unawaited(setupNotificationWithUsers()); unawaited(signalHandleNewServerConnection()); + unawaited(fetchGroupStatesForUnjoinedGroups()); } } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 5e18672..2bba307 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -14,6 +14,17 @@ Future handleGroupCreate( String groupId, EncryptedContent_GroupCreate newGroup, ) async { + final user = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (user == null) { + // Only contacts can invite other contacts, so this can (via the UI) not happen. + Log.error( + 'User is not a contact. Aborting.', + ); + return; + } + // 1. Store the new group -> e.g. store the stateKey and groupPublicKey // 2. Call function that should fetch all jobs // 1. This function is also called in the main function, in case the state stored on the server could not be loaded @@ -41,16 +52,14 @@ Future handleGroupCreate( return; } - final user = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (user == null) { - // Only contacts can invite other contacts, so this can (via the UI) not happen. - Log.error( - 'User is not a contact. Aborting.', - ); - return; - } + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + contactId: Value(fromUserId), + affectedContactId: const Value(null), + type: const Value(GroupActionType.addMember), + ), + ); await twonlyDB.groupsDao.insertGroupMember( GroupMembersCompanion( diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 94bc7fc..240ac51 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -381,7 +381,10 @@ class _ChatMessagesViewState extends State { }).toList(), ); } else if (messages[i].isGroupAction) { - return ChatGroupAction(action: messages[i].groupAction!); + return ChatGroupAction( + key: Key(messages[i].groupAction!.groupHistoryId), + action: messages[i].groupAction!, + ); } else { final chatMessage = messages[i].message!; return Transform.translate( diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index f748f7e..0576654 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -44,6 +44,7 @@ class _ChatGroupActionState extends State { @override Widget build(BuildContext context) { var text = ''; + IconData? icon; final affected = (affectedContact == null) ? 'you' @@ -56,22 +57,33 @@ class _ChatGroupActionState extends State { text = (contact == null) ? 'You have changed the group name to "${widget.action.newGroupName}".' : '$maker has changed the group name to "${widget.action.newGroupName}".'; + icon = FontAwesomeIcons.pencil; case GroupActionType.createdGroup: + icon = FontAwesomeIcons.penToSquare; + text = (contact == null) + ? 'You have created the group.' + : '$maker has created the group.'; case GroupActionType.removedMember: case GroupActionType.addMember: + icon = FontAwesomeIcons.userPlus; + text = (contact == null) + ? 'You have added $affected to the group.' + : '$maker has added $affected to the group.'; case GroupActionType.leftGroup: break; case GroupActionType.promoteToAdmin: + icon = FontAwesomeIcons.key; text = (contact == null) ? 'You made $affected an admin.' : '$maker made $affected an admin.'; case GroupActionType.demoteToMember: + icon = FontAwesomeIcons.key; text = (contact == null) ? 'You revoked $affected admin rights.' : '$maker revoked $affectedR admin rights.'; } - if (text == '') return Container(); + if (text == '' || icon == null) return Container(); return Padding( padding: const EdgeInsets.all(8), @@ -80,10 +92,10 @@ class _ChatGroupActionState extends State { textAlign: TextAlign.center, text: TextSpan( children: [ - const WidgetSpan( + WidgetSpan( alignment: PlaceholderAlignment.middle, child: FaIcon( - FontAwesomeIcons.pencil, + icon, size: 10, color: Colors.grey, ), From 4bc7db75e9c1ef01d45e0a967a335a08d45754d9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 15:48:50 +0100 Subject: [PATCH 47/76] remove and add members #277 --- lib/src/database/daos/groups.dao.dart | 8 +- lib/src/localization/app_de.arb | 5 +- lib/src/localization/app_en.arb | 5 +- .../generated/app_localizations.dart | 18 ++ .../generated/app_localizations_de.dart | 11 ++ .../generated/app_localizations_en.dart | 11 ++ .../api/client2client/groups.c2c.dart | 44 +++-- lib/src/services/group.services.dart | 171 ++++++++++++++++-- lib/src/views/camera/share_image_view.dart | 3 +- .../chat_list_components/group_list_item.dart | 42 +++-- lib/src/views/chats/chat_messages.view.dart | 99 +++++----- .../chat_group_action.dart | 10 +- lib/src/views/groups/group.view.dart | 18 +- .../group_create_select_members.view.dart | 84 ++++++--- .../views/groups/group_member.context.dart | 110 ++++++----- 15 files changed, 463 insertions(+), 176 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b1da33f..96ed572 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -46,8 +46,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return _insertGroup(group); } - Future insertGroupMember(GroupMembersCompanion members) async { - await into(groupMembers).insert(members); + Future insertOrUpdateGroupMember(GroupMembersCompanion members) async { + await into(groupMembers).insertOnConflictUpdate(members); } Future insertGroupAction(GroupHistoriesCompanion action) async { @@ -159,8 +159,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .watch(); } - Stream> watchGroups() { - return select(groups).watch(); + Stream> watchGroupsForShareImage() { + return (select(groups)..where((g) => g.leftGroup.equals(false))).watch(); } Stream watchGroup(String groupId) { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 16565cd..e667679 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -377,5 +377,8 @@ "revokeAdminRightsOkBtn": "Als Admin entfernen", "makeAdminRightsTitle": "{username} zum Admin machen?", "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", - "makeAdminRightsOkBtn": "Zum Admin machen" + "makeAdminRightsOkBtn": "Zum Admin machen", + "updateGroup": "Gruppe aktualisieren", + "alreadyInGroup": "Bereits Mitglied", + "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index e0e3870..c663ba8 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -533,5 +533,8 @@ "revokeAdminRightsOkBtn": "Remove as admin", "makeAdminRightsTitle": "Make {username} an admin?", "makeAdminRightsBody": "{username} will be able to edit this group and its members.", - "makeAdminRightsOkBtn": "Make admin" + "makeAdminRightsOkBtn": "Make admin", + "updateGroup": "Update group", + "alreadyInGroup": "Already in Group", + "removeContactFromGroupTitle": "Remove {username} from this group?" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 7bac4bb..ec7eb68 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2305,6 +2305,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Make admin'** String get makeAdminRightsOkBtn; + + /// No description provided for @updateGroup. + /// + /// In en, this message translates to: + /// **'Update group'** + String get updateGroup; + + /// No description provided for @alreadyInGroup. + /// + /// In en, this message translates to: + /// **'Already in Group'** + String get alreadyInGroup; + + /// No description provided for @removeContactFromGroupTitle. + /// + /// In en, this message translates to: + /// **'Remove {username} from this group?'** + String removeContactFromGroupTitle(Object username); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index cb7374a..04e5cd3 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1225,4 +1225,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get makeAdminRightsOkBtn => 'Zum Admin machen'; + + @override + String get updateGroup => 'Gruppe aktualisieren'; + + @override + String get alreadyInGroup => 'Bereits Mitglied'; + + @override + String removeContactFromGroupTitle(Object username) { + return '$username aus dieser Gruppe entfernen?'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 99ccfc3..169b063 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1218,4 +1218,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get makeAdminRightsOkBtn => 'Make admin'; + + @override + String get updateGroup => 'Update group'; + + @override + String get alreadyInGroup => 'Already in Group'; + + @override + String removeContactFromGroupTitle(Object username) { + return 'Remove $username from this group?'; + } } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 2bba307..5eb5399 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -33,17 +33,33 @@ Future handleGroupCreate( final myGroupKey = generateIdentityKeyPair(); - // Group state is joinedGroup -> As the current state has not yet been downloaded. - final group = await twonlyDB.groupsDao.createNewGroup( - GroupsCompanion( - groupId: Value(groupId), - stateVersionId: const Value(0), - stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), - myGroupPrivateKey: Value(myGroupKey.serialize()), - groupName: const Value(''), - joinedGroup: const Value(false), - ), - ); + var group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) { + // Group state is joinedGroup -> As the current state has not yet been downloaded. + group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + ), + ); + } else { + // User was already in the group, so update leftGroup back to false + await twonlyDB.groupsDao.updateGroup( + groupId, + GroupsCompanion( + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + leftGroup: const Value(false), + ), + ); + } if (group == null) { Log.error( @@ -61,7 +77,7 @@ Future handleGroupCreate( ), ); - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(groupId), contactId: Value(fromUserId), @@ -120,6 +136,10 @@ Future handleGroupUpdate( if (affectedContactId == gUser.userId) { affectedContactId = null; + if (actionType == GroupActionType.removedMember) { + // Oh no, I just got removed from the group... + // This state is handle this case in the fetchGroupState.... + } } await twonlyDB.groupsDao.insertGroupAction( diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 9f7d25a..9d24c39 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; - import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -111,7 +110,7 @@ Future createNewGroup(String groupName, List members) async { Log.info('Created new group: ${group.groupId}'); for (final member in members) { - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(group.groupId), contactId: Value(member.userId), @@ -151,6 +150,12 @@ Future fetchGroupStatesForUnjoinedGroups() async { } Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { + if (group.leftGroup) { + Log.error( + 'Could not refresh group state, as user is no longer part of the group', + ); + return null; + } try { var isSuccess = true; @@ -191,6 +196,18 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { Log.info( 'Group ${group.groupId} has already newest group state from the server!', ); + // return (groupStateServer.versionId.toInt(), encryptedGroupState); + } + + if (!encryptedGroupState.memberIds.contains(Int64(gUser.userId))) { + // OH no, I am no longer a member of this group... + // -> + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + leftGroup: Value(true), + ), + ); return (groupStateServer.versionId.toInt(), encryptedGroupState); } @@ -236,7 +253,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { } if (inContacts) { // User is already a contact, so just add him to the group members list - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(group.groupId), contactId: Value(memberId.toInt()), @@ -318,7 +335,7 @@ Future addNewHiddenContact(int contactId) async { return true; } -Future updateGroupState( +Future _updateGroupState( Group group, EncryptedGroupState state, { Uint8List? addAdmin, @@ -394,9 +411,7 @@ Future updateGroupState( return false; } } - - // Update database to the newest state - return (await fetchGroupState(group)) != null; + return true; } Future manageAdminState( @@ -439,7 +454,7 @@ Future manageAdminState( } // send new state to the server - if (!await updateGroupState( + if (!await _updateGroupState( group, state, addAdmin: addAdmin, @@ -469,7 +484,8 @@ Future manageAdminState( ), ); - return true; + // Updates the memberState :) + return (await fetchGroupState(group)) != null; } Future updateGroupeName(Group group, String groupName) async { @@ -481,7 +497,7 @@ Future updateGroupeName(Group group, String groupName) async { state.groupName = groupName; // send new state to the server - if (!await updateGroupState(group, state)) { + if (!await _updateGroupState(group, state)) { return false; } @@ -504,5 +520,138 @@ Future updateGroupeName(Group group, String groupName) async { ), ); - return true; + // Updates the groupName :) + return (await fetchGroupState(group)) != null; +} + +Future addNewGroupMembers( + Group group, + List newGroupMemberIds, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + var memberIds = state.memberIds + newGroupMemberIds.map(Int64.new).toList(); + memberIds = memberIds.toSet().toList(); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: memberIds, + adminIds: state.adminIds, + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState)) { + return false; + } + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + for (final newMember in newGroupMemberIds) { + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.addMember.name, + affectedContactId: Int64(newMember), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.addMember), + affectedContactId: Value(newMember), + ), + ); + + await sendCipherText( + newMember, + EncryptedContent( + groupId: group.groupId, + groupCreate: EncryptedContent_GroupCreate( + stateKey: group.stateEncryptionKey, + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); + } + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; +} + +Future removeMemberFromGroup( + Group group, + GroupMember member, + int removeContactId, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + final contactId = Int64(removeContactId); + + final membersIdSet = state.memberIds.toSet(); + final adminIdSet = state.adminIds.toSet(); + Uint8List? removeAdmin; + if (!membersIdSet.contains(contactId)) { + Log.info('User was already removed from the group!'); + return true; + } + if (adminIdSet.contains(contactId)) { + if (member.groupPublicKey == null) { + // If the admin public key is not removed, that the user could potentially still update the group state. So only + // allow the user removal, if this key is known. It is better the users can not remove the other user, then + // the he can but the other user, could still update the group state. + Log.error( + 'Could not remove user. User is admin, but groupPublicKey is unknown.', + ); + return false; + } + removeAdmin = member.groupPublicKey; + } + + membersIdSet.remove(contactId); + adminIdSet.remove(contactId); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: membersIdSet.toList(), + adminIds: adminIdSet.toList(), + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState, removeAdmin: removeAdmin)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.removedMember.name, + affectedContactId: Int64(removeContactId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.removedMember), + affectedContactId: Value(removeContactId), + ), + ); + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; } diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 7acf01e..38fa15f 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -50,7 +50,8 @@ class _ShareImageView extends State { void initState() { super.initState(); - allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async { + allGroupSub = + twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async { setState(() { contacts = allGroups; }); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index c903069..4b1ce6c 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -255,28 +255,30 @@ class _UserListItem extends State { }, child: AvatarIcon(group: widget.group), ), - trailing: IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - if (_hasNonOpenedMediaFile) { - return ChatMessagesView(widget.group); - } else { - return CameraSendToView(widget.group); - } + trailing: (widget.group.leftGroup) + ? null + : IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + if (_hasNonOpenedMediaFile) { + return ChatMessagesView(widget.group); + } else { + return CameraSendToView(widget.group); + } + }, + ), + ); }, + icon: FaIcon( + _hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), ), - ); - }, - icon: FaIcon( - _hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), - ), - ), onTap: onTap, ), ); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 240ac51..b6f2d06 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -452,58 +452,59 @@ class _ChatMessagesViewState extends State { ], ), ), - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, + if (!group.leftGroup) + Padding( + padding: const EdgeInsets.only( + bottom: 30, + left: 20, + right: 20, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: inputTextMessageDeco(context), ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, ), - ], + if (currentInputText != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), ), - ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index 0576654..3e3add2 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -48,8 +48,8 @@ class _ChatGroupActionState extends State { final affected = (affectedContact == null) ? 'you' - : "${getContactDisplayName(affectedContact!)}'s"; - final affectedR = (affectedContact == null) ? 'your' : affected; + : getContactDisplayName(affectedContact!); + final affectedR = (affectedContact == null) ? 'your' : "$affected'"; final maker = (contact == null) ? '' : getContactDisplayName(contact!); switch (widget.action.type) { @@ -64,6 +64,10 @@ class _ChatGroupActionState extends State { ? 'You have created the group.' : '$maker has created the group.'; case GroupActionType.removedMember: + icon = FontAwesomeIcons.userMinus; + text = (contact == null) + ? 'You have removed $affected from the group.' + : '$maker has removed $affected from the group.'; case GroupActionType.addMember: icon = FontAwesomeIcons.userPlus; text = (contact == null) @@ -79,7 +83,7 @@ class _ChatGroupActionState extends State { case GroupActionType.demoteToMember: icon = FontAwesomeIcons.key; text = (contact == null) - ? 'You revoked $affected admin rights.' + ? 'You revoked $affectedR admin rights.' : '$maker revoked $affectedR admin rights.'; } diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 87bb0b4..3631f46 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/views/groups/group_member.context.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; @@ -81,6 +82,21 @@ class _GroupViewState extends State { } } + Future _addNewGroupMembers() async { + final selectedUserIds = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectMembersView(group: group), + ), + ) as List?; + if (selectedUserIds == null) return; + if (!await addNewGroupMembers(group, selectedUserIds)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -133,7 +149,7 @@ class _GroupViewState extends State { BetterListTile( icon: FontAwesomeIcons.plus, text: context.lang.addMember, - onTap: () => {}, + onTap: _addNewGroupMembers, ), BetterListTile( padding: const EdgeInsets.only(left: 13), diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index 103bdde..f4ffd37 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -12,7 +12,8 @@ import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart'; class GroupCreateSelectMembersView extends StatefulWidget { - const GroupCreateSelectMembersView({super.key}); + const GroupCreateSelectMembersView({this.group, super.key}); + final Group? group; @override State createState() => _StartNewChatView(); } @@ -24,6 +25,7 @@ class _StartNewChatView extends State { late StreamSubscription> contactSub; final HashSet selectedUsers = HashSet(); + final HashSet alreadyInGroup = HashSet(); @override void initState() { @@ -40,6 +42,18 @@ class _StartNewChatView extends State { }); await filterUsers(); }); + initAsync(); + } + + Future initAsync() async { + if (widget.group != null) { + final members = + await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); + for (final member in members) { + alreadyInGroup.add(member.userId); + } + if (mounted) setState(() {}); + } } @override @@ -68,6 +82,7 @@ class _StartNewChatView extends State { } void toggleSelectedUser(int userId) { + if (alreadyInGroup.contains(userId)) return; if (!selectedUsers.contains(userId)) { selectedUsers.add(userId); } else { @@ -76,30 +91,41 @@ class _StartNewChatView extends State { setState(() {}); } + Future submitChanges() async { + if (widget.group != null) { + Navigator.pop(context, selectedUsers.toList()); + return; + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectGroupNameView( + selectedUsers: allContacts + .where((t) => selectedUsers.contains(t.userId)) + .toList(), + ), + ), + ); + } + @override Widget build(BuildContext context) { return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( appBar: AppBar( - title: Text(context.lang.selectMembers), + title: Text( + widget.group == null + ? context.lang.selectMembers + : context.lang.addMember, + ), ), floatingActionButton: FilledButton.icon( - onPressed: selectedUsers.isEmpty - ? null - : () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupCreateSelectGroupNameView( - selectedUsers: allContacts - .where((t) => selectedUsers.contains(t.userId)) - .toList(), - ), - ), - ); - }, - label: Text(context.lang.next), + onPressed: selectedUsers.isEmpty ? null : submitChanges, + label: Text( + widget.group == null ? context.lang.next : context.lang.updateGroup, + ), icon: const FaIcon(FontAwesomeIcons.penToSquare), ), body: SafeArea( @@ -174,12 +200,16 @@ class _StartNewChatView extends State { ), ], ), + subtitle: (alreadyInGroup.contains(user.userId)) + ? Text(context.lang.alreadyInGroup) + : null, leading: AvatarIcon( contact: user, fontSize: 13, ), trailing: Checkbox( - value: selectedUsers.contains(user.userId), + value: selectedUsers.contains(user.userId) | + alreadyInGroup.contains(user.userId), side: WidgetStateBorderSide.resolveWith( (states) { if (states.contains(WidgetState.selected)) { @@ -221,15 +251,15 @@ class _Chip extends StatelessWidget { @override Widget build(BuildContext context) { - return Chip( - key: GlobalKey(), - avatar: AvatarIcon( - contact: contact, - fontSize: 10, - ), - label: GestureDetector( - onTap: () => onTap(contact.userId), - child: Row( + return GestureDetector( + onTap: () => onTap(contact.userId), + child: Chip( + key: GlobalKey(), + avatar: AvatarIcon( + contact: contact, + fontSize: 10, + ), + label: Row( mainAxisSize: MainAxisSize.min, children: [ Text( diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart index 1b39a69..d3b7b42 100644 --- a/lib/src/views/groups/group_member.context.dart +++ b/lib/src/views/groups/group_member.context.dart @@ -24,6 +24,67 @@ class GroupMemberContextMenu extends StatelessWidget { final Group group; final Widget child; + Future _makeContactAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.makeAdminRightsTitle(getContactDisplayName(contact)), + context.lang.makeAdminRightsBody(getContactDisplayName(contact)), + customOk: context.lang.makeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + false, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactAsAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.revokeAdminRightsTitle(getContactDisplayName(contact)), + '', + customOk: context.lang.revokeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + true, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactFromGroup(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.removeContactFromGroupTitle(getContactDisplayName(contact)), + '', + ); + if (ok) { + if (!await removeMemberFromGroup( + group, + member, + contact.userId, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + @override Widget build(BuildContext context) { return ContextMenu( @@ -61,28 +122,7 @@ class GroupMemberContextMenu extends StatelessWidget { member.memberState == MemberState.normal) ContextMenuItem( title: context.lang.makeAdmin, - onTap: () async { - final ok = await showAlertDialog( - context, - context.lang - .makeAdminRightsTitle(getContactDisplayName(contact)), - context.lang - .makeAdminRightsBody(getContactDisplayName(contact)), - customOk: context.lang.makeAdminRightsOkBtn, - ); - if (ok) { - if (!await manageAdminState( - group, - member, - contact.userId, - false, - )) { - if (context.mounted) { - showNetworkIssue(context); - } - } - } - }, + onTap: () => _makeContactAdmin(context), icon: FontAwesomeIcons.key, ), if (member.groupPublicKey != null && @@ -90,35 +130,13 @@ class GroupMemberContextMenu extends StatelessWidget { member.memberState == MemberState.admin) ContextMenuItem( title: context.lang.removeAdmin, - onTap: () async { - final ok = await showAlertDialog( - context, - context.lang - .revokeAdminRightsTitle(getContactDisplayName(contact)), - '', - customOk: context.lang.revokeAdminRightsOkBtn, - ); - if (ok) { - if (!await manageAdminState( - group, - member, - contact.userId, - true, - )) { - if (context.mounted) { - showNetworkIssue(context); - } - } - } - }, + onTap: () => _removeContactAsAdmin(context), icon: FontAwesomeIcons.key, ), if (group.isGroupAdmin) ContextMenuItem( title: context.lang.removeFromGroup, - onTap: () async { - // onResponseTriggered(); - }, + onTap: () => _removeContactFromGroup(context), icon: FontAwesomeIcons.rightFromBracket, ), ], From b06011dc4993fbaaf73e3e43235de63c10978891 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 16:06:25 +0100 Subject: [PATCH 48/76] translate group actions --- lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 18 +++- .../generated/app_localizations.dart | 96 +++++++++++++++++++ .../generated/app_localizations_de.dart | 72 ++++++++++++++ .../generated/app_localizations_en.dart | 72 ++++++++++++++ .../chat_group_action.dart | 76 +++++++++++---- 6 files changed, 319 insertions(+), 19 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index e667679..786d32c 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -380,5 +380,7 @@ "makeAdminRightsOkBtn": "Zum Admin machen", "updateGroup": "Gruppe aktualisieren", "alreadyInGroup": "Bereits Mitglied", - "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?" + "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?", + "groupActionYou": "dich", + "groupActionYour": "deine" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c663ba8..871d14d 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -536,5 +536,21 @@ "makeAdminRightsOkBtn": "Make admin", "updateGroup": "Update group", "alreadyInGroup": "Already in Group", - "removeContactFromGroupTitle": "Remove {username} from this group?" + "removeContactFromGroupTitle": "Remove {username} from this group?", + "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", + "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", + "youCreatedGroup": "Du hast die Gruppe erstellt.", + "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", + "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", + "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", + "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", + "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", + "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", + "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", + "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", + "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", + "youLeftGroup": "Du hast die Gruppe verlassen.", + "makerLeftGroup": "{maker} hat die Gruppe verlassen.", + "groupActionYou": "you", + "groupActionYour": "your" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index ec7eb68..38d0dbe 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2323,6 +2323,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Remove {username} from this group?'** String removeContactFromGroupTitle(Object username); + + /// No description provided for @youChangedGroupName. + /// + /// In en, this message translates to: + /// **'Du hast den Gruppennamen zu „{newGroupName}“ geändert.'** + String youChangedGroupName(Object newGroupName); + + /// No description provided for @makerChangedGroupName. + /// + /// In en, this message translates to: + /// **'{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.'** + String makerChangedGroupName(Object maker, Object newGroupName); + + /// No description provided for @youCreatedGroup. + /// + /// In en, this message translates to: + /// **'Du hast die Gruppe erstellt.'** + String get youCreatedGroup; + + /// No description provided for @makerCreatedGroup. + /// + /// In en, this message translates to: + /// **'{maker} hat die Gruppe erstellt.'** + String makerCreatedGroup(Object maker); + + /// No description provided for @youRemovedMember. + /// + /// In en, this message translates to: + /// **'Du hast {affected} aus der Gruppe entfernt.'** + String youRemovedMember(Object affected); + + /// No description provided for @makerRemovedMember. + /// + /// In en, this message translates to: + /// **'{maker} hat {affected} aus der Gruppe entfernt.'** + String makerRemovedMember(Object affected, Object maker); + + /// No description provided for @youAddedMember. + /// + /// In en, this message translates to: + /// **'Du hast {affected} zur Gruppe hinzugefügt.'** + String youAddedMember(Object affected); + + /// No description provided for @makerAddedMember. + /// + /// In en, this message translates to: + /// **'{maker} hat {affected} zur Gruppe hinzugefügt.'** + String makerAddedMember(Object affected, Object maker); + + /// No description provided for @youMadeAdmin. + /// + /// In en, this message translates to: + /// **'Du hast {affected} zum Administrator gemacht.'** + String youMadeAdmin(Object affected); + + /// No description provided for @makerMadeAdmin. + /// + /// In en, this message translates to: + /// **'{maker} hat {affected} zum Administrator gemacht.'** + String makerMadeAdmin(Object affected, Object maker); + + /// No description provided for @youRevokedAdminRights. + /// + /// In en, this message translates to: + /// **'Du hast {affectedR} die Administratorrechte entzogen.'** + String youRevokedAdminRights(Object affectedR); + + /// No description provided for @makerRevokedAdminRights. + /// + /// In en, this message translates to: + /// **'{maker} hat {affectedR} die Administratorrechte entzogen.'** + String makerRevokedAdminRights(Object affectedR, Object maker); + + /// No description provided for @youLeftGroup. + /// + /// In en, this message translates to: + /// **'Du hast die Gruppe verlassen.'** + String get youLeftGroup; + + /// No description provided for @makerLeftGroup. + /// + /// In en, this message translates to: + /// **'{maker} hat die Gruppe verlassen.'** + String makerLeftGroup(Object maker); + + /// No description provided for @groupActionYou. + /// + /// In en, this message translates to: + /// **'you'** + String get groupActionYou; + + /// No description provided for @groupActionYour. + /// + /// In en, this message translates to: + /// **'your'** + String get groupActionYour; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 04e5cd3..ceec42c 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1236,4 +1236,76 @@ class AppLocalizationsDe extends AppLocalizations { String removeContactFromGroupTitle(Object username) { return '$username aus dieser Gruppe entfernen?'; } + + @override + String youChangedGroupName(Object newGroupName) { + return 'Du hast den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String makerChangedGroupName(Object maker, Object newGroupName) { + return '$maker hat den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String get youCreatedGroup => 'Du hast die Gruppe erstellt.'; + + @override + String makerCreatedGroup(Object maker) { + return '$maker hat die Gruppe erstellt.'; + } + + @override + String youRemovedMember(Object affected) { + return 'Du hast $affected aus der Gruppe entfernt.'; + } + + @override + String makerRemovedMember(Object affected, Object maker) { + return '$maker hat $affected aus der Gruppe entfernt.'; + } + + @override + String youAddedMember(Object affected) { + return 'Du hast $affected zur Gruppe hinzugefügt.'; + } + + @override + String makerAddedMember(Object affected, Object maker) { + return '$maker hat $affected zur Gruppe hinzugefügt.'; + } + + @override + String youMadeAdmin(Object affected) { + return 'Du hast $affected zum Administrator gemacht.'; + } + + @override + String makerMadeAdmin(Object affected, Object maker) { + return '$maker hat $affected zum Administrator gemacht.'; + } + + @override + String youRevokedAdminRights(Object affectedR) { + return 'Du hast $affectedR die Administratorrechte entzogen.'; + } + + @override + String makerRevokedAdminRights(Object affectedR, Object maker) { + return '$maker hat $affectedR die Administratorrechte entzogen.'; + } + + @override + String get youLeftGroup => 'Du hast die Gruppe verlassen.'; + + @override + String makerLeftGroup(Object maker) { + return '$maker hat die Gruppe verlassen.'; + } + + @override + String get groupActionYou => 'dich'; + + @override + String get groupActionYour => 'deine'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 169b063..99f7695 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1229,4 +1229,76 @@ class AppLocalizationsEn extends AppLocalizations { String removeContactFromGroupTitle(Object username) { return 'Remove $username from this group?'; } + + @override + String youChangedGroupName(Object newGroupName) { + return 'Du hast den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String makerChangedGroupName(Object maker, Object newGroupName) { + return '$maker hat den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String get youCreatedGroup => 'Du hast die Gruppe erstellt.'; + + @override + String makerCreatedGroup(Object maker) { + return '$maker hat die Gruppe erstellt.'; + } + + @override + String youRemovedMember(Object affected) { + return 'Du hast $affected aus der Gruppe entfernt.'; + } + + @override + String makerRemovedMember(Object affected, Object maker) { + return '$maker hat $affected aus der Gruppe entfernt.'; + } + + @override + String youAddedMember(Object affected) { + return 'Du hast $affected zur Gruppe hinzugefügt.'; + } + + @override + String makerAddedMember(Object affected, Object maker) { + return '$maker hat $affected zur Gruppe hinzugefügt.'; + } + + @override + String youMadeAdmin(Object affected) { + return 'Du hast $affected zum Administrator gemacht.'; + } + + @override + String makerMadeAdmin(Object affected, Object maker) { + return '$maker hat $affected zum Administrator gemacht.'; + } + + @override + String youRevokedAdminRights(Object affectedR) { + return 'Du hast $affectedR die Administratorrechte entzogen.'; + } + + @override + String makerRevokedAdminRights(Object affectedR, Object maker) { + return '$maker hat $affectedR die Administratorrechte entzogen.'; + } + + @override + String get youLeftGroup => 'Du hast die Gruppe verlassen.'; + + @override + String makerLeftGroup(Object maker) { + return '$maker hat die Gruppe verlassen.'; + } + + @override + String get groupActionYou => 'you'; + + @override + String get groupActionYour => 'your'; } diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index 3e3add2..f2c2826 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -4,6 +4,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; class ChatGroupAction extends StatefulWidget { const ChatGroupAction({ @@ -47,47 +48,88 @@ class _ChatGroupActionState extends State { IconData? icon; final affected = (affectedContact == null) - ? 'you' + ? context.lang.groupActionYou : getContactDisplayName(affectedContact!); - final affectedR = (affectedContact == null) ? 'your' : "$affected'"; + final affectedR = + (affectedContact == null) ? context.lang.groupActionYour : affected; final maker = (contact == null) ? '' : getContactDisplayName(contact!); switch (widget.action.type) { case GroupActionType.updatedGroupName: text = (contact == null) - ? 'You have changed the group name to "${widget.action.newGroupName}".' - : '$maker has changed the group name to "${widget.action.newGroupName}".'; + ? context.lang.youChangedGroupName(widget.action.newGroupName!) + : context.lang + .makerChangedGroupName(maker, widget.action.newGroupName!); icon = FontAwesomeIcons.pencil; case GroupActionType.createdGroup: icon = FontAwesomeIcons.penToSquare; text = (contact == null) - ? 'You have created the group.' - : '$maker has created the group.'; + ? context.lang.youCreatedGroup + : context.lang.makerCreatedGroup(maker); case GroupActionType.removedMember: icon = FontAwesomeIcons.userMinus; text = (contact == null) - ? 'You have removed $affected from the group.' - : '$maker has removed $affected from the group.'; + ? context.lang.youRemovedMember(affected) + : context.lang.makerRemovedMember(affected, maker); case GroupActionType.addMember: icon = FontAwesomeIcons.userPlus; text = (contact == null) - ? 'You have added $affected to the group.' - : '$maker has added $affected to the group.'; - case GroupActionType.leftGroup: - break; + ? context.lang.youAddedMember(affected) + : context.lang.makerAddedMember(affected, maker); case GroupActionType.promoteToAdmin: icon = FontAwesomeIcons.key; text = (contact == null) - ? 'You made $affected an admin.' - : '$maker made $affected an admin.'; + ? context.lang.youMadeAdmin(affected) + : context.lang.makerMadeAdmin(affected, maker); case GroupActionType.demoteToMember: icon = FontAwesomeIcons.key; text = (contact == null) - ? 'You revoked $affectedR admin rights.' - : '$maker revoked $affectedR admin rights.'; + ? context.lang.youRevokedAdminRights(affected) + : context.lang.makerRevokedAdminRights(affectedR, maker); + case GroupActionType.leftGroup: + icon = FontAwesomeIcons.userMinus; + text = (contact == null) + ? context.lang.youLeftGroup + : context.lang.makerLeftGroup(maker); } - if (text == '' || icon == null) return Container(); + // switch (widget.action.type) { + // case GroupActionType.updatedGroupName: + // text = (contact == null) + // ? 'You have changed the group name to "${widget.action.newGroupName}".' + // : '$maker has changed the group name to "${widget.action.newGroupName}".'; + // icon = FontAwesomeIcons.pencil; + // case GroupActionType.createdGroup: + // icon = FontAwesomeIcons.penToSquare; + // text = (contact == null) + // ? 'You have created the group.' + // : '$maker has created the group.'; + // case GroupActionType.removedMember: + // icon = FontAwesomeIcons.userMinus; + // text = (contact == null) + // ? 'You have removed $affected from the group.' + // : '$maker has removed $affected from the group.'; + // case GroupActionType.addMember: + // icon = FontAwesomeIcons.userPlus; + // text = (contact == null) + // ? 'You have added $affected to the group.' + // : '$maker has added $affected to the group.'; + // case GroupActionType.promoteToAdmin: + // icon = FontAwesomeIcons.key; + // text = (contact == null) + // ? 'You made $affected an admin.' + // : '$maker made $affected an admin.'; + // case GroupActionType.demoteToMember: + // icon = FontAwesomeIcons.key; + // text = (contact == null) + // ? 'You revoked $affectedR admin rights.' + // : '$maker revoked $affectedR admin rights.'; + // case GroupActionType.leftGroup: + // icon = FontAwesomeIcons.userMinus; + // text = (contact == null) + // ? 'You have left the group.' + // : '$maker has left the group.'; + // } return Padding( padding: const EdgeInsets.all(8), From d8dda1a0d1c38779988fbb2e2e363d292f7360d8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 17:20:00 +0100 Subject: [PATCH 49/76] fixed translation --- lib/src/localization/app_de.arb | 14 ++++++++++++++ lib/src/localization/app_en.arb | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 786d32c..d129157 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -381,6 +381,20 @@ "updateGroup": "Gruppe aktualisieren", "alreadyInGroup": "Bereits Mitglied", "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?", + "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", + "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", + "youCreatedGroup": "Du hast die Gruppe erstellt.", + "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", + "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", + "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", + "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", + "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", + "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", + "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", + "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", + "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", + "youLeftGroup": "Du hast die Gruppe verlassen.", + "makerLeftGroup": "{maker} hat die Gruppe verlassen.", "groupActionYou": "dich", "groupActionYour": "deine" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 871d14d..a74ea19 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -537,20 +537,20 @@ "updateGroup": "Update group", "alreadyInGroup": "Already in Group", "removeContactFromGroupTitle": "Remove {username} from this group?", - "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", - "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", - "youCreatedGroup": "Du hast die Gruppe erstellt.", - "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", - "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", - "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", - "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", - "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", - "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", - "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", - "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", - "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", - "youLeftGroup": "Du hast die Gruppe verlassen.", - "makerLeftGroup": "{maker} hat die Gruppe verlassen.", + "youChangedGroupName": "You have changed the group name to \"{newGroupName}\".", + "makerChangedGroupName": "{maker} has changed the group name to \"{newGroupName}\".", + "youCreatedGroup": "You have created the group.", + "makerCreatedGroup": "{maker} has created the group.", + "youRemovedMember": "You have removed {affected} from the group.", + "makerRemovedMember": "{maker} has removed {affected} from the group.", + "youAddedMember": "You have added {affected} to the group.", + "makerAddedMember": "{maker} has added {affected} to the group.", + "youMadeAdmin": "You made {affected} an admin.", + "makerMadeAdmin": "{maker} made {affected} an admin.", + "youRevokedAdminRights": "You revoked {affectedR} admin rights.", + "makerRevokedAdminRights": "{maker} revoked {affectedR} admin rights.", + "youLeftGroup": "You have left the group.", + "makerLeftGroup": "{maker} has left the group.", "groupActionYou": "you", "groupActionYour": "your" } \ No newline at end of file From e1232f45b5f52981fb407954bfe64362cfc749f5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 17:28:26 +0100 Subject: [PATCH 50/76] add missing translation --- lib/src/localization/app_de.arb | 381 +++++++++++++++++++++++++++++++- 1 file changed, 379 insertions(+), 2 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index d129157..2e6e6e4 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -1,54 +1,101 @@ { "@@locale": "de", "registerTitle": "Willkommen bei twonly!", + "@registerTitle": {}, "registerSlogan": "twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.", + "@registerSlogan": {}, "onboardingWelcomeTitle": "Willkommen bei twonly!", + "@onboardingWelcomeTitle": {}, "onboardingWelcomeBody": "Erlebe eine private und sichere Möglichkeit mit Freunden in Kontakt zu bleiben, indem du spontane Bilder teilst.", + "@onboardingWelcomeBody": {}, "onboardingE2eTitle": "Unbekümmert teilen", + "@onboardingE2eTitle": {}, "onboardingE2eBody": "Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.", + "@onboardingE2eBody": {}, "onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten", + "@onboardingFocusTitle": {}, "onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.", + "@onboardingFocusBody": {}, "onboardingSendTwonliesTitle": "twonlies senden", + "@onboardingSendTwonliesTitle": {}, "onboardingSendTwonliesBody": "Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!", + "@onboardingSendTwonliesBody": {}, "onboardingNotProductTitle": "Du bist nicht das Produkt!", + "@onboardingNotProductTitle": {}, "onboardingNotProductBody": "twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.", + "@onboardingNotProductBody": {}, "onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei", + "@onboardingBuyOneGetTwoTitle": {}, "onboardingBuyOneGetTwoBody": "twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.", + "@onboardingBuyOneGetTwoBody": {}, "onboardingGetStartedTitle": "Auf geht's", + "@onboardingGetStartedTitle": {}, "onboardingGetStartedBody": "Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.", + "@onboardingGetStartedBody": {}, "onboardingTryForFree": "Jetzt registrieren", + "@onboardingTryForFree": {}, "registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!", + "@registerUsernameSlogan": {}, "registerUsernameDecoration": "Benutzername", + "@registerUsernameDecoration": {}, "registerUsernameLimits": "Der Benutzername muss mindestens 3 Zeichen lang sein.", + "@registerUsernameLimits": {}, "registerSubmitButton": "Jetzt registrieren!", + "@registerSubmitButton": {}, "registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!", + "@registerTwonlyCodeText": {}, "registerTwonlyCodeLabel": "twonly-Code", + "@registerTwonlyCodeLabel": {}, "newMessageTitle": "Neue Nachricht", + "@newMessageTitle": {}, "chatsTapToSend": "Klicke, um dein erstes Bild zu teilen.", + "@chatsTapToSend": {}, "cameraPreviewSendTo": "Senden an", + "@cameraPreviewSendTo": {}, "shareImageTitle": "Teilen mit", + "@shareImageTitle": {}, "shareImageBestFriends": "Beste Freunde", + "@shareImageBestFriends": {}, "shareImagePinnedContacts": "Angeheftet", + "@shareImagePinnedContacts": {}, "shareImagedEditorSendImage": "Senden", + "@shareImagedEditorSendImage": {}, "shareImagedEditorShareWith": "Teilen mit", + "@shareImagedEditorShareWith": {}, "shareImagedEditorSaveImage": "Speichern", + "@shareImagedEditorSaveImage": {}, "shareImagedEditorSavedImage": "Gespeichert", + "@shareImagedEditorSavedImage": {}, "shareImagedSelectAll": "Alle auswählen", + "@shareImagedSelectAll": {}, "shareImageAllUsers": "Alle Kontakte", + "@shareImageAllUsers": {}, "shareImageAllTwonlyWarning": "twonlies können nur an verifizierte Kontakte gesendet werden!", + "@shareImageAllTwonlyWarning": {}, "shareImageSearchAllContacts": "Alle Kontakte durchsuchen", "@shareImageSearchAllContacts": {}, "shareImageUserNotVerified": "Benutzer ist nicht verifiziert", + "@shareImageUserNotVerified": {}, "shareImageUserNotVerifiedDesc": "twonlies können nur an verifizierte Nutzer gesendet werden. Um einen Nutzer zu verifizieren, gehe auf sein Profil und auf „Sicherheitsnummer verifizieren“.", + "@shareImageUserNotVerifiedDesc": {}, "shareImageShowArchived": "Archivierte Benutzer anzeigen", + "@shareImageShowArchived": {}, "searchUsernameInput": "Benutzername", + "@searchUsernameInput": {}, "searchUsernameTitle": "Benutzernamen suchen", + "@searchUsernameTitle": {}, "searchUserNamePreview": "Um dich und andere twonly Benutzer vor Spam und Missbrauch zu schützen, ist es nicht möglich, im Preview-Modus nach anderen Personen zu suchen. Andere Benutzer können dich finden und deren Anfragen werden dann hier angezeigt!", + "@searchUserNamePreview": {}, "selectSubscription": "Abo auswählen", + "@selectSubscription": {}, "searchUsernameNotFound": "Benutzername nicht gefunden", + "@searchUsernameNotFound": {}, "searchUsernameNotFoundBody": "Es wurde kein Benutzer mit dem Benutzernamen \"{username}\" gefunden.", + "@searchUsernameNotFoundBody": {}, "searchUsernameNewFollowerTitle": "Folgeanfragen", + "@searchUsernameNewFollowerTitle": {}, "searchUsernameQrCodeBtn": "QR-Code scannen", + "@searchUsernameQrCodeBtn": {}, "searchUserNamePending": "Ausstehend", "@searchUserNamePending": {}, "searchUserNameBlockUserTooltip": "Benutzer ohne Benachrichtigung blockieren.", @@ -62,10 +109,15 @@ "userFoundBody": "Möchtest du eine Folgeanfrage stellen?", "@userFoundBody": {}, "chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!", + "@chatListViewSearchUserNameBtn": {}, "chatListViewSendFirstTwonly": "Sende dein erstes twonly!", + "@chatListViewSendFirstTwonly": {}, "chatListDetailInput": "Nachricht eingeben", + "@chatListDetailInput": {}, "userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.", + "@userDeletedAccount": {}, "contextMenuUserProfile": "Userprofil", + "@contextMenuUserProfile": {}, "contextMenuVerifyUser": "Verifizieren", "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archivieren", @@ -79,322 +131,647 @@ "startNewChatYourContacts": "Deine Kontakte", "@startNewChatYourContacts": {}, "contextMenuOpenChat": "Chat", + "@contextMenuOpenChat": {}, "contextMenuPin": "Anheften", + "@contextMenuPin": {}, "contextMenuUnpin": "Lösen", + "@contextMenuUnpin": {}, "mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!", + "@mediaViewerAuthReason": {}, "mediaViewerTwonlyTapToOpen": "Tippe um den twonly zu öffnen!", + "@mediaViewerTwonlyTapToOpen": {}, "messageSendState_Received": "Empfangen", + "@messageSendState_Received": {}, "messageSendState_Opened": "Geöffnet", + "@messageSendState_Opened": {}, "messageSendState_Send": "Gesendet", + "@messageSendState_Send": {}, "messageSendState_Sending": "Wird gesendet", + "@messageSendState_Sending": {}, "messageSendState_TapToLoad": "Tippe zum Laden", + "@messageSendState_TapToLoad": {}, "messageSendState_Loading": "Herunterladen", + "@messageSendState_Loading": {}, "messageStoredInGallery": "Gespeichert", + "@messageStoredInGallery": {}, "messageReopened": "Erneut geöffnet", "@messageReopened": {}, "imageEditorDrawOk": "Zeichnung machen", + "@imageEditorDrawOk": {}, "settingsTitle": "Einstellungen", + "@settingsTitle": {}, "settingsChats": "Chats", + "@settingsChats": {}, "settingsStorageData": "Daten und Speicher", + "@settingsStorageData": {}, "settingsStorageDataStoreInGTitle": "In der Galerie speichern", + "@settingsStorageDataStoreInGTitle": {}, "settingsStorageDataStoreInGSubtitle": "Speichere Bilder zusätzlich in der Systemgalerie.", + "@settingsStorageDataStoreInGSubtitle": {}, "settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload", + "@settingsStorageDataMediaAutoDownload": {}, "settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten", + "@settingsStorageDataAutoDownMobile": {}, "settingsStorageDataAutoDownWifi": "Bei Nutzung von WLAN", + "@settingsStorageDataAutoDownWifi": {}, "settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis", + "@settingsPreSelectedReactions": {}, "settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.", + "@settingsPreSelectedReactionsError": {}, "settingsProfile": "Profil", + "@settingsProfile": {}, "settingsProfileCustomizeAvatar": "Avatar anpassen", + "@settingsProfileCustomizeAvatar": {}, "settingsProfileEditDisplayName": "Anzeigename", + "@settingsProfileEditDisplayName": {}, "settingsProfileEditDisplayNameNew": "Neuer Anzeigename", + "@settingsProfileEditDisplayNameNew": {}, "settingsAccount": "Konto", + "@settingsAccount": {}, "settingsSubscription": "Abonnement", + "@settingsSubscription": {}, "settingsAppearance": "Erscheinungsbild", + "@settingsAppearance": {}, "settingsPrivacy": "Datenschutz", + "@settingsPrivacy": {}, "settingsPrivacyBlockUsers": "Benutzer blockieren", + "@settingsPrivacyBlockUsers": {}, "settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.", + "@settingsPrivacyBlockUsersDesc": {}, "settingsPrivacyBlockUsersCount": "{len} Kontakt(e)", + "@settingsPrivacyBlockUsersCount": {}, "settingsNotification": "Benachrichtigung", + "@settingsNotification": {}, "settingsNotifyTroubleshooting": "Fehlersuche", + "@settingsNotifyTroubleshooting": {}, "settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.", + "@settingsNotifyTroubleshootingDesc": {}, "settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt", + "@settingsNotifyTroubleshootingNoProblem": {}, "settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.", + "@settingsNotifyTroubleshootingNoProblemDesc": {}, "settingsHelp": "Hilfe", + "@settingsHelp": {}, "settingsHelpFAQ": "FAQ", + "@settingsHelpFAQ": {}, "feedbackTooltip": "Feedback zur Verbesserung von twonly geben.", + "@feedbackTooltip": {}, "settingsHelpContactUs": "Kontaktiere uns", + "@settingsHelpContactUs": {}, "contactUsFaq": "FAQ schon gelesen?", + "@contactUsFaq": {}, "contactUsEmojis": "Wie fühlst du dich? (optional)", + "@contactUsEmojis": {}, "contactUsSelectOption": "Bitte wähle eine Option", + "@contactUsSelectOption": {}, "contactUsReason": "Sag uns, warum du uns kontaktierst", + "@contactUsReason": {}, "contactUsMessage": "Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.", + "@contactUsMessage": {}, "contactUsYourMessage": "Deine Nachricht", + "@contactUsYourMessage": {}, "contactUsMessageTitle": "Erzähl uns, was los ist", + "@contactUsMessageTitle": {}, "contactUsReasonNotWorking": "Etwas funktioniert nicht", + "@contactUsReasonNotWorking": {}, "contactUsReasonFeatureRequest": "Funktionsanfrage", + "@contactUsReasonFeatureRequest": {}, "contactUsReasonQuestion": "Frage", + "@contactUsReasonQuestion": {}, "contactUsReasonFeedback": "Feedback", + "@contactUsReasonFeedback": {}, "contactUsReasonOther": "Sonstiges", + "@contactUsReasonOther": {}, "contactUsIncludeLog": "Debug-Protokoll anhängen.", + "@contactUsIncludeLog": {}, "contactUsWhatsThat": "Was ist das?", + "@contactUsWhatsThat": {}, "contactUsLastWarning": "Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.", + "@contactUsLastWarning": {}, "contactUsSuccess": "Feedback erfolgreich übermittelt!", + "@contactUsSuccess": {}, "contactUsShortcut": "Feedback-Symbol ausblenden", + "@contactUsShortcut": {}, "settingsHelpDiagnostics": "Diagnoseprotokoll", + "@settingsHelpDiagnostics": {}, "settingsHelpVersion": "Version", + "@settingsHelpVersion": {}, "settingsHelpLicenses": "Lizenzen (Source-Code)", + "@settingsHelpLicenses": {}, "settingsHelpCredits": "Lizenzen (Bilder)", + "@settingsHelpCredits": {}, "settingsHelpImprint": "Impressum & Datenschutzrichtlinie", + "@settingsHelpImprint": {}, "settingsHelpTerms": "Nutzungsbedingungen", + "@settingsHelpTerms": {}, "settingsAppearanceTheme": "Theme", + "@settingsAppearanceTheme": {}, "settingsAccountDeleteAccount": "Konto löschen", + "@settingsAccountDeleteAccount": {}, "settingsAccountDeleteAccountWithBallance": "Im nächsten Schritt kannst du auswählen, was du mit dem Restguthaben ({credit}) machen willst.", + "@settingsAccountDeleteAccountWithBallance": {}, "settingsAccountDeleteAccountNoInternet": "Zum Löschen deines Accounts ist eine Internetverbindung erforderlich.", + "@settingsAccountDeleteAccountNoInternet": {}, "settingsAccountDeleteAccountNoBallance": "Wenn du dein Konto gelöscht hast, gibt es keinen Weg zurück.", + "@settingsAccountDeleteAccountNoBallance": {}, "settingsAccountDeleteModalTitle": "Bist du sicher?", + "@settingsAccountDeleteModalTitle": {}, "settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.", + "@settingsAccountDeleteModalBody": {}, "contactVerifyNumberTitle": "Sicherheitsnummer verifizieren", + "@contactVerifyNumberTitle": {}, "contactVerifyNumberTapToScan": "Zum Scannen tippen", + "@contactVerifyNumberTapToScan": {}, "contactVerifyNumberMarkAsVerified": "Als verifiziert markieren", + "@contactVerifyNumberMarkAsVerified": {}, "contactVerifyNumberClearVerification": "Verifizierung aufheben", + "@contactVerifyNumberClearVerification": {}, "contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.", + "@contactVerifyNumberLongDesc": {}, "contactNickname": "Spitzname", + "@contactNickname": {}, "contactNicknameNew": "Neuer Spitzname", + "@contactNicknameNew": {}, "contactBlock": "Blockieren", + "@contactBlock": {}, "contactRemove": "Benutzer löschen", + "@contactRemove": {}, "contactRemoveTitle": "{username} löschen?", + "@contactRemoveTitle": {}, "contactRemoveBody": "Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.", + "@contactRemoveBody": {}, "deleteAllContactMessages": "Textnachrichten löschen", + "@deleteAllContactMessages": {}, "deleteAllContactMessagesBody": "Dadurch werden alle Nachrichten, ausgenommen gespeicherte Mediendateien, in deinem Chat mit {username} gelöscht. Dies löscht NICHT die auf dem Gerät von {username} gespeicherten Nachrichten!", + "@deleteAllContactMessagesBody": {}, "contactBlockTitle": "Blockiere {username}", + "@contactBlockTitle": {}, "contactBlockBody": "Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und sein Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.", + "@contactBlockBody": {}, "undo": "Rückgängig", + "@undo": {}, "redo": "Wiederholen", + "@redo": {}, "next": "Weiter", + "@next": {}, "submit": "Abschicken", + "@submit": {}, "close": "Schließen", + "@close": {}, "cancel": "Abbrechen", + "@cancel": {}, "edit": "Bearbeiten", + "@edit": {}, "ok": "Ok", + "@ok": {}, "now": "Jetzt", + "@now": {}, "you": "Du", + "@you": {}, "minutesShort": "Min.", + "@minutesShort": {}, "image": "Bild", + "@image": {}, "video": "Video", + "@video": {}, "react": "Reagieren", + "@react": {}, "reply": "Antworten", + "@reply": {}, "copy": "Kopieren", + "@copy": {}, "delete": "Löschen", + "@delete": {}, "info": "Info", + "@info": {}, "disable": "Deaktiviern", + "@disable": {}, "enable": "Aktivieren", + "@enable": {}, "switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.", + "@switchFrontAndBackCamera": {}, "addTextItem": "Text", + "@addTextItem": {}, "protectAsARealTwonly": "Als echtes twonly senden!", + "@protectAsARealTwonly": {}, "addDrawing": "Zeichnung", + "@addDrawing": {}, "addEmoji": "Emoji", + "@addEmoji": {}, "toggleFlashLight": "Taschenlampe umschalten", + "@toggleFlashLight": {}, "toggleHighQuality": "Bessere Auflösung umschalten", + "@toggleHighQuality": {}, "searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.", + "@searchUsernameNotFoundLong": {}, "errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.", + "@errorUnknown": {}, "errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.", + "@errorBadRequest": {}, "errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.", + "@errorTooManyRequests": {}, "errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.", + "@errorInternalError": {}, "errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.", + "@errorInvalidInvitationCode": {}, "errorUsernameAlreadyTaken": "Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.", + "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.", + "@errorSignatureNotValid": {}, "errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.", + "@errorUsernameNotFound": {}, "errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.", + "@errorUsernameNotValid": {}, "errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.", + "@errorInvalidPublicKey": {}, "errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.", + "@errorSessionAlreadyAuthenticated": {}, "errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.", + "@errorSessionNotAuthenticated": {}, "errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.", + "@errorOnlyOneSessionAllowed": {}, "upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.", + "@upgradeToPaidPlan": {}, "upgradeToPaidPlanButton": "Auf {planId} upgraden", + "@upgradeToPaidPlanButton": {}, "partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!", + "@partOfPaidPlanOf": {}, "errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.", + "@errorNotEnoughCredit": {}, "errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.", + "@errorPlanLimitReached": {}, "errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.", + "@errorPlanNotAllowed": {}, "errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.", + "@errorVoucherInvalid": {}, "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.", + "@errorPlanUpgradeNotYearly": {}, "proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", + "@proFeature1": {}, "proFeature2": "1 zusätzlicher Plus Benutzer", + "@proFeature2": {}, "proFeature3": "Zusatzfunktionen (coming-soon)", + "@proFeature3": {}, "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", + "@proFeature4": {}, "year": "year", + "@year": {}, "month": "month", + "@month": {}, "familyFeature1": "✓ Alles von Pro", + "@familyFeature1": {}, "familyFeature2": "4 zusätzliche Plus Benutzer", + "@familyFeature2": {}, "redeemUserInviteCode": "Oder löse einen twonly-Code ein.", + "@redeemUserInviteCode": {}, "freeFeature1": "10 Medien-Datei-Uploads pro Tag", + "@freeFeature1": {}, "plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", + "@plusFeature1": {}, "plusFeature2": "Zusatzfunktionen (coming-soon)", + "@plusFeature2": {}, "transactionHistory": "Transaktionshistorie", + "@transactionHistory": {}, "currentBalance": "Dein Guthaben", + "@currentBalance": {}, "manageAdditionalUsers": "Zusätzliche Benutzer verwalten", + "@manageAdditionalUsers": {}, "manageSubscription": "Abonnement verwalten", + "@manageSubscription": {}, "nextPayment": "Nächste Zahlung", + "@nextPayment": {}, "open": "Offene", + "@open": {}, "buy": "Kaufen", + "@buy": {}, "createOrRedeemVoucher": "Gutschein erstellen oder einlösen", + "@createOrRedeemVoucher": {}, "subscriptionRefund": "Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von {refund} für dein aktuelles Abonnement.", + "@subscriptionRefund": {}, "createVoucher": "Gutschein kaufen", + "@createVoucher": {}, "createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.", + "@createVoucherDesc": {}, "redeemVoucher": "Gutschein einlösen", + "@redeemVoucher": {}, "redeemUserInviteCodeTitle": "twonly-Code einlösen", + "@redeemUserInviteCodeTitle": {}, "redeemUserInviteCodeSuccess": "Dein Plan wurde erfolgreich angepasst.", + "@redeemUserInviteCodeSuccess": {}, "voucherCreated": "Gutschein wurde erstellt", + "@voucherCreated": {}, "openVouchers": "Offene Gutscheine", + "@openVouchers": {}, "enterVoucherCode": "Gutschein Code eingeben", + "@enterVoucherCode": {}, "voucherRedeemed": "Gutschein eingelöst", + "@voucherRedeemed": {}, "requestedVouchers": "Beantragte Gutscheine", + "@requestedVouchers": {}, "redeemedVouchers": "Eingelöste Gutscheine", + "@redeemedVouchers": {}, "transactionCash": "Bargeldtransaktion", + "@transactionCash": {}, "transactionPlanUpgrade": "Planupgrade", + "@transactionPlanUpgrade": {}, "transactionRefund": "Rückerstattung", + "@transactionRefund": {}, "transactionAutoRenewal": "Automatische Verlängerung", + "@transactionAutoRenewal": {}, "refund": "Rückerstattung", + "@refund": {}, "transactionThanksForTesting": "Danke fürs Testen", + "@transactionThanksForTesting": {}, "transactionUnknown": "Unbekannte Transaktion", + "@transactionUnknown": {}, "transactionVoucherCreated": "Gutschein erstellt", + "@transactionVoucherCreated": {}, "transactionVoucherRedeemed": "Gutschein eingelöst", + "@transactionVoucherRedeemed": {}, "checkoutOptions": "Optionen", + "@checkoutOptions": {}, "checkoutPayYearly": "Jährlich bezahlen", + "@checkoutPayYearly": {}, "checkoutTotal": "Gesamt", + "@checkoutTotal": {}, "selectPaymentMethod": "Zahlungsmethode auswählen", + "@selectPaymentMethod": {}, "twonlyCredit": "twonly-Guthaben", + "@twonlyCredit": {}, "notEnoughCredit": "Du hast nicht genügend Guthaben!", + "@notEnoughCredit": {}, "chargeCredit": "Guthaben aufladen", + "@chargeCredit": {}, "autoRenewal": "Automatische Verlängerung", + "@autoRenewal": {}, "autoRenewalDesc": "Du kannst dies jederzeit ändern.", + "@autoRenewalDesc": {}, "autoRenewalLongDesc": "Wenn dein Abonnement ausläuft, wirst du automatisch auf den Preview-Plan zurückgestuft. Wenn du die automatische Verlängerung aktivierst, vergewissere dich bitte, dass du über genügend Guthaben für die automatische Erneuerung verfügst. Wir werden dich rechtzeitig vor der automatischen Erneuerung benachrichtigen.", + "@autoRenewalLongDesc": {}, "planSuccessUpgraded": "Dein Plan wurde erfolgreich aktualisiert.", + "@planSuccessUpgraded": {}, "checkoutSubmit": "Kostenpflichtig bestellen", + "@checkoutSubmit": {}, "additionalUsersList": "Ihre zusätzlichen Benutzer", + "@additionalUsersList": {}, "additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer", + "@additionalUsersPlusTokens": {}, "additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer", + "@additionalUsersFreeTokens": {}, "planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", + "@planNotAllowed": {}, "planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", + "@planLimitReached": {}, "galleryDelete": "Datei löschen", + "@galleryDelete": {}, "galleryExport": "In Galerie exportieren", + "@galleryExport": {}, "galleryExportSuccess": "Erfolgreich in der Gallery gespeichert.", + "@galleryExportSuccess": {}, "galleryDetails": "Details anzeigen", + "@galleryDetails": {}, "settingsResetTutorials": "Tutorials erneut anzeigen", + "@settingsResetTutorials": {}, "settingsResetTutorialsDesc": "Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.", + "@settingsResetTutorialsDesc": {}, "settingsResetTutorialsSuccess": "Tutorials werden erneut angezeigt.", + "@settingsResetTutorialsSuccess": {}, "tutorialChatListSearchUsersTitle": "Freunde finden und Freundschaftsanfragen verwalten", + "@tutorialChatListSearchUsersTitle": {}, "tutorialChatListSearchUsersDesc": "Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.", + "@tutorialChatListSearchUsersDesc": {}, "tutorialChatListContextMenuTitle": "Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.", + "@tutorialChatListContextMenuTitle": {}, "tutorialChatListContextMenuDesc": "Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.", + "@tutorialChatListContextMenuDesc": {}, "tutorialChatMessagesVerifyShieldTitle": "Verifiziere deine Kontakte!", + "@tutorialChatMessagesVerifyShieldTitle": {}, "tutorialChatMessagesVerifyShieldDesc": "twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.", + "@tutorialChatMessagesVerifyShieldDesc": {}, "tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen", + "@tutorialChatMessagesReopenMessageTitle": {}, "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", + "@tutorialChatMessagesReopenMessageDesc": {}, "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", + "@memoriesEmpty": {}, "deleteTitle": "Bist du dir sicher?", + "@deleteTitle": {}, "deleteOkBtnForAll": "Für alle löschen", + "@deleteOkBtnForAll": {}, "deleteOkBtnForMe": "Für mich löschen", + "@deleteOkBtnForMe": {}, "deleteImageTitle": "Bist du dir sicher?", + "@deleteImageTitle": {}, "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", + "@deleteImageBody": {}, "backupNoticeTitle": "Kein Backup konfiguriert", + "@backupNoticeTitle": {}, "backupNoticeDesc": "Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.", + "@backupNoticeDesc": {}, "backupNoticeLater": "Später erinnern", + "@backupNoticeLater": {}, "backupNoticeOpenBackup": "Backup erstellen", + "@backupNoticeOpenBackup": {}, "backupPending": "Ausstehend", + "@backupPending": {}, "backupFailed": "Fehlgeschlagen", + "@backupFailed": {}, "backupSuccess": "Erfolgreich", + "@backupSuccess": {}, "backupTwonlySafeDesc": "Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.", + "@backupTwonlySafeDesc": {}, "backupServer": "Server", + "@backupServer": {}, "backupMaxBackupSize": "max. Backup-Größe", + "@backupMaxBackupSize": {}, "backupStorageRetention": "Speicheraufbewahrung", + "@backupStorageRetention": {}, "backupLastBackupDate": "Letztes Backup", + "@backupLastBackupDate": {}, "backupLastBackupSize": "Backup-Größe", + "@backupLastBackupSize": {}, "backupLastBackupResult": "Ergebnis", + "@backupLastBackupResult": {}, "deleteBackupTitle": "Bist du sicher?", + "@deleteBackupTitle": {}, "backupNoPasswordRecovery": "Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.", + "@backupNoPasswordRecovery": {}, "deleteBackupBody": "Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.", + "@deleteBackupBody": {}, "backupData": "Daten-Backup", + "@backupData": {}, "backupDataDesc": "Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.", + "@backupDataDesc": {}, "backupInsecurePassword": "Unsicheres Passwort", + "@backupInsecurePassword": {}, "backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.", + "@backupInsecurePasswordDesc": {}, "backupInsecurePasswordOk": "Trotzdem fortfahren", + "@backupInsecurePasswordOk": {}, "backupInsecurePasswordCancel": "Erneut versuchen", + "@backupInsecurePasswordCancel": {}, "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", + "@backupTwonlySafeLongDesc": {}, "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.", + "@backupSelectStrongPassword": {}, "password": "Passwort", + "@password": {}, "passwordRepeated": "Passwort wiederholen", + "@passwordRepeated": {}, "passwordRepeatedNotEqual": "Passwörter stimmen nicht überein.", + "@passwordRepeatedNotEqual": {}, "backupPasswordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein.", + "@backupPasswordRequirement": {}, "backupExpertSettings": "Experteneinstellungen", + "@backupExpertSettings": {}, "backupEnableBackup": "Automatische Sicherung aktivieren", + "@backupEnableBackup": {}, "backupOwnServerDesc": "Speichere dein twonly Backup auf einem Server deiner Wahl.", + "@backupOwnServerDesc": {}, "backupUseOwnServer": "Server verwenden", + "@backupUseOwnServer": {}, "backupResetServer": "Standardserver verwenden", + "@backupResetServer": {}, "backupTwonlySaveNow": "Jetzt speichern", + "@backupTwonlySaveNow": {}, "backupChangePassword": "Password ändern", + "@backupChangePassword": {}, "inviteFriends": "Freunde einladen", + "@inviteFriends": {}, "inviteFriendsShareBtn": "Teilen", + "@inviteFriendsShareBtn": {}, "inviteFriendsShareText": "Wechseln wir zu twonly: {url}", + "@inviteFriendsShareText": {}, "appOutdated": "Deine Version von twonly ist veraltet.", + "@appOutdated": {}, "appOutdatedBtn": "Jetzt aktualisieren.", + "@appOutdatedBtn": {}, "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", + "@doubleClickToReopen": {}, "retransmissionRequested": "Wird erneut versucht.", + "@retransmissionRequested": {}, "testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!", + "@testPaymentMethod": {}, "openChangeLog": "Changelog automatisch öffnen", + "@openChangeLog": {}, "reportUserTitle": "Melde {username}", + "@reportUserTitle": {}, "reportUserReason": "Meldegrund", + "@reportUserReason": {}, "reportUser": "Benutzer melden", + "@reportUser": {}, "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", + "@newDeviceRegistered": {}, "tabToRemoveEmoji": "Tippen um zu entfernen", + "@tabToRemoveEmoji": {}, "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", + "@quotedMessageWasDeleted": {}, "messageWasDeleted": "Nachricht wurde gelöscht.", + "@messageWasDeleted": {}, "messageWasDeletedShort": "Gelöscht", + "@messageWasDeletedShort": {}, "sent": "Versendet", + "@sent": {}, "sentTo": "Zugestellt an", + "@sentTo": {}, "received": "Empfangen", + "@received": {}, "opened": "Geöffnet", + "@opened": {}, "waitingForInternet": "Warten auf Internet", + "@waitingForInternet": {}, "editHistory": "Bearbeitungshistorie", + "@editHistory": {}, "archivedChats": "Archivierte Chats", + "@archivedChats": {}, "durationShortSecond": "Sek.", + "@durationShortSecond": {}, "durationShortMinute": "Min.", + "@durationShortMinute": {}, "durationShortHour": "Std", + "@durationShortHour": {}, "durationShortDays": "Tagen", + "@durationShortDays": {}, "newGroup": "Neue Gruppe", + "@newGroup": {}, "selectMembers": "Mitglieder auswählen", + "@selectMembers": {}, "selectGroupName": "Gruppennamen wählen", + "@selectGroupName": {}, "groupNameInput": "Gruppennamen", + "@groupNameInput": {}, "groupMembers": "Mitglieder", + "@groupMembers": {}, "createGroup": "Gruppe erstellen", + "@createGroup": {}, "addMember": "Mitglied hinzufügen", + "@addMember": {}, "leaveGroup": "Gruppe verlassen", + "@leaveGroup": {}, "createContactRequest": "Kontaktanfrage erstellen", + "@createContactRequest": {}, "makeAdmin": "Zum Admin machen", + "@makeAdmin": {}, "removeAdmin": "Als Admin entfernen", + "@removeAdmin": {}, "removeFromGroup": "Aus Gruppe entfernen", + "@removeFromGroup": {}, "admin": "Admin", + "@admin": {}, "revokeAdminRightsTitle": "Adminrechte von {username} entfernen?", + "@revokeAdminRightsTitle": {}, "revokeAdminRightsOkBtn": "Als Admin entfernen", + "@revokeAdminRightsOkBtn": {}, "makeAdminRightsTitle": "{username} zum Admin machen?", + "@makeAdminRightsTitle": {}, "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", + "@makeAdminRightsBody": {}, "makeAdminRightsOkBtn": "Zum Admin machen", + "@makeAdminRightsOkBtn": {}, "updateGroup": "Gruppe aktualisieren", + "@updateGroup": {}, "alreadyInGroup": "Bereits Mitglied", + "@alreadyInGroup": {}, "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?", + "@removeContactFromGroupTitle": {}, "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", + "@youChangedGroupName": {}, "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", + "@makerChangedGroupName": {}, "youCreatedGroup": "Du hast die Gruppe erstellt.", + "@youCreatedGroup": {}, "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", + "@makerCreatedGroup": {}, "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", + "@youRemovedMember": {}, "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", + "@makerRemovedMember": {}, "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", + "@youAddedMember": {}, "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", + "@makerAddedMember": {}, "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", + "@youMadeAdmin": {}, "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", + "@makerMadeAdmin": {}, "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", + "@youRevokedAdminRights": {}, "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", + "@makerRevokedAdminRights": {}, "youLeftGroup": "Du hast die Gruppe verlassen.", + "@youLeftGroup": {}, "makerLeftGroup": "{maker} hat die Gruppe verlassen.", + "@makerLeftGroup": {}, "groupActionYou": "dich", - "groupActionYour": "deine" -} \ No newline at end of file + "@groupActionYou": {}, + "groupActionYour": "deine", + "@groupActionYour": {}, + "settingsBackup": "Backup", + "@settingsBackup": {}, + "twonlySafeRecoverTitle": "Recovery", + "@twonlySafeRecoverTitle": {}, + "twonlySafeRecoverDesc": "Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.", + "@twonlySafeRecoverDesc": {}, + "twonlySafeRecoverBtn": "Backup wiederherstellen", + "@twonlySafeRecoverBtn": {} +} From 9356a1fc702f356cec06e5ca52e0dce878dd9347 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 23:01:53 +0100 Subject: [PATCH 51/76] support gifs and allow videos to pick from the gallery --- .../mediafiles/mediafile.service.dart | 6 +- .../save_to_gallery.dart | 3 +- .../camera_preview_controller_view.dart | 41 +++++- .../views/camera/share_image_editor_view.dart | 118 ++++++++++-------- lib/src/views/chats/media_viewer.view.dart | 6 +- .../memories/memories_item_thumbnail.dart | 3 +- .../memories/memories_photo_slider.view.dart | 3 +- 7 files changed, 114 insertions(+), 66 deletions(-) diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 0b11ae4..5c09e10 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -161,13 +161,12 @@ class MediaFileService { return; } switch (mediaFile.type) { + case MediaType.gif: case MediaType.image: // all images are already compress.. break; case MediaType.video: await createThumbnailsForVideo(storedPath, thumbnailPath); - case MediaType.gif: - Log.error('Thumbnail for .gif is not implemented yet'); } } @@ -183,8 +182,7 @@ class MediaFileService { case MediaType.video: await compressAndOverlayVideo(this); case MediaType.gif: - originalPath.renameSync(tempPath.path); - Log.error('Compression for .gif is not implemented yet.'); + originalPath.copySync(tempPath.path); } } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 5c81e0f..4a2be05 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -45,7 +45,8 @@ class SaveToGalleryButtonState extends State { _imageSaving = true; }); - if (widget.mediaService.mediaFile.type == MediaType.image) { + if (widget.mediaService.mediaFile.type == MediaType.image || + widget.mediaService.mediaFile.type == MediaType.gif) { await widget.storeImageAsOriginal(); } diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 4ab20d1..176d6b3 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -285,9 +285,12 @@ class _CameraPreviewViewState extends State { Future? imageBytes, File? videoFilePath, { bool sharedFromGallery = false, + MediaType? mediaType, }) async { + final type = mediaType ?? + ((videoFilePath != null) ? MediaType.video : MediaType.image); final mediaFileService = await initializeMediaUpload( - (videoFilePath != null) ? MediaType.video : MediaType.image, + type, gUser.defaultShowTime, ); if (!mounted) return true; @@ -377,14 +380,42 @@ class _CameraPreviewViewState extends State { _sharePreviewIsShown = true; }); final picker = ImagePicker(); - final pickedFile = await picker.pickImage(source: ImageSource.gallery); + final pickedFile = await picker.pickMedia(); if (pickedFile != null) { - final imageFile = File(pickedFile.path); + final imageExtensions = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.heic', + '.heif', + '.avif', + ]; + + Log.info('Picket from gallery: ${pickedFile.path}'); + + File? videoFilePath; + Future? imageBytes; + MediaType? mediaType; + + final isImage = + imageExtensions.any((ext) => pickedFile.name.contains(ext)); + if (isImage) { + if (pickedFile.name.contains('.gif')) { + mediaType = MediaType.gif; + } + imageBytes = pickedFile.readAsBytes(); + } else { + videoFilePath = File(pickedFile.path); + } + await pushMediaEditor( - imageFile.readAsBytes(), - null, + imageBytes, + videoFilePath, sharedFromGallery: true, + mediaType: mediaType, ); } setState(() { diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 32ff434..49c2b5f 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -55,6 +55,7 @@ class _ShareImageEditorView extends State { double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; + Uint8List? imageBytes; VideoPlayerController? videoController; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -66,13 +67,16 @@ class _ShareImageEditorView extends State { void initState() { super.initState(); - layers.add(FilterLayerData()); + if (media.type != MediaType.gif) { + layers.add(FilterLayerData()); + } if (widget.sendToGroup != null) { selectedGroupIds.add(widget.sendToGroup!.groupId); } - if (widget.mediaFileService.mediaFile.type == MediaType.image) { + if (widget.mediaFileService.mediaFile.type == MediaType.video || + widget.mediaFileService.mediaFile.type == MediaType.gif) { if (widget.imageBytesFuture != null) { loadImage(widget.imageBytesFuture!); } else { @@ -124,52 +128,55 @@ class _ShareImageEditorView extends State { return []; } return [ - ActionButton( - Icons.text_fields_rounded, - tooltipText: context.lang.addTextItem, - onPressed: () async { - layers = layers.where((x) => !x.isDeleted).toList(); - if (layers.any((x) => x.isEditing)) return; - undoLayers.clear(); - removedLayers.clear(); - layers.add( - TextLayerData( - textLayersBefore: layers.whereType().length, - ), - ); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.text_fields_rounded, + tooltipText: context.lang.addTextItem, + onPressed: () async { + layers = layers.where((x) => !x.isDeleted).toList(); + if (layers.any((x) => x.isEditing)) return; + undoLayers.clear(); + removedLayers.clear(); + layers.add( + TextLayerData( + textLayersBefore: layers.whereType().length, + ), + ); + setState(() {}); + }, + ), const SizedBox(height: 8), - ActionButton( - Icons.draw_rounded, - tooltipText: context.lang.addDrawing, - onPressed: () async { - undoLayers.clear(); - removedLayers.clear(); - layers.add(DrawLayerData()); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.draw_rounded, + tooltipText: context.lang.addDrawing, + onPressed: () async { + undoLayers.clear(); + removedLayers.clear(); + layers.add(DrawLayerData()); + setState(() {}); + }, + ), const SizedBox(height: 8), - ActionButton( - Icons.add_reaction_outlined, - tooltipText: context.lang.addEmoji, - onPressed: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ) as Layer?; - if (layer == null) return; - undoLayers.clear(); - removedLayers.clear(); - layers.add(layer); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.add_reaction_outlined, + tooltipText: context.lang.addEmoji, + onPressed: () async { + final layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const Emojis(); + }, + ) as Layer?; + if (layer == null) return; + undoLayers.clear(); + removedLayers.clear(); + layers.add(layer); + setState(() {}); + }, + ), const SizedBox(height: 8), NotificationBadge( count: (media.type == MediaType.video) @@ -356,12 +363,18 @@ class _ShareImageEditorView extends State { if (mediaService.tempPath.existsSync()) { mediaService.tempPath.deleteSync(); } - final imageBytes = await getEditedImageBytes(); - if (imageBytes == null) return false; - if (media.type == MediaType.image) { - mediaService.originalPath.writeAsBytesSync(imageBytes); + if (media.type == MediaType.gif) { + mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); } else { - mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + final imageBytes = await getEditedImageBytes(); + if (imageBytes == null) return false; + if (media.type == MediaType.image || media.type == MediaType.gif) { + mediaService.originalPath.writeAsBytesSync(imageBytes); + } else if (media.type == MediaType.video) { + mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + } else { + Log.error('MediaType not supported: ${media.type}'); + } } // In case the image was already stored, then rename the stored image. @@ -374,7 +387,8 @@ class _ShareImageEditorView extends State { } Future loadImage(Future imageBytesFuture) async { - await currentImage.load(await imageBytesFuture); + imageBytes = await imageBytesFuture; + await currentImage.load(imageBytes); if (isDisposed) return; if (!context.mounted) return; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 41dc818..5350601 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -298,7 +298,8 @@ class _MediaViewerViewState extends State { if (gUser.storeMediaFilesInGallery) { if (currentMedia!.mediaFile.type == MediaType.video) { await saveVideoToGallery(currentMedia!.storedPath.path); - } else if (currentMedia!.mediaFile.type == MediaType.image) { + } else if (currentMedia!.mediaFile.type == MediaType.image || + currentMedia!.mediaFile.type == MediaType.gif) { final imageBytes = await currentMedia!.storedPath.readAsBytes(); await saveImageToGallery(imageBytes); } @@ -466,7 +467,8 @@ class _MediaViewerViewState extends State { child: VideoPlayer(videoController!), ), if (currentMedia != null && - currentMedia!.mediaFile.type == MediaType.image) + currentMedia!.mediaFile.type == MediaType.image || + currentMedia!.mediaFile.type == MediaType.gif) Positioned.fill( child: Image.file( currentMedia!.tempPath, diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index a0ec884..c1486bf 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -57,7 +57,8 @@ class _MemoriesItemThumbnailState extends State { if (media.thumbnailPath.existsSync()) Image.file(media.thumbnailPath) else if (media.storedPath.existsSync() && - media.mediaFile.type == MediaType.image) + media.mediaFile.type == MediaType.image || + media.mediaFile.type == MediaType.gif) Image.file(media.storedPath) else const Text('Media file removed.'), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 99b1aa2..b2016d5 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -75,7 +75,8 @@ class _MemoriesPhotoSliderViewState extends State { try { if (item.mediaFile.type == MediaType.video) { await saveVideoToGallery(item.storedPath.path); - } else if (item.mediaFile.type == MediaType.image) { + } else if (item.mediaFile.type == MediaType.image || + item.mediaFile.type == MediaType.gif) { final imageBytes = await item.storedPath.readAsBytes(); await saveImageToGallery(imageBytes); } From c786bb55f9462801b4fe9018808aca8fe2854929 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 00:50:09 +0100 Subject: [PATCH 52/76] add push notifications for groups and add sender name --- .../NotificationService.swift | 84 +++------- .../push_notification.pb.swift | 30 ++-- lib/src/localization/app_de.arb | 21 ++- lib/src/localization/app_en.arb | 19 ++- .../generated/app_localizations.dart | 130 ++++++++++++++-- .../generated/app_localizations_de.dart | 64 +++++++- .../generated/app_localizations_en.dart | 87 +++++++++-- .../generated/push_notification.pb.dart | 16 +- .../generated/push_notification.pbenum.dart | 2 + .../generated/push_notification.pbjson.dart | 12 +- .../protobuf/client/push_notification.proto | 3 +- .../background.notifications.dart | 144 +++++------------- .../notifications/pushkeys.notifications.dart | 18 ++- .../views/camera/share_image_editor_view.dart | 2 +- lib/src/views/chats/chat_messages.view.dart | 12 ++ .../chat_list_entry.dart | 24 ++- .../chat_text_entry.dart | 126 +++++++++------ .../response_container.dart | 2 +- 18 files changed, 514 insertions(+), 282 deletions(-) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index c2c6048..0091f8f 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -84,7 +84,7 @@ func getPushNotificationData(pushData: String) -> ( pushUser = tryPushUser if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID) { - return ("blocked", "blocked", 0) + //return ("blocked", "blocked", 0) } break } @@ -109,11 +109,12 @@ func getPushNotificationData(pushData: String) -> ( } else if pushUser != nil { return ( pushUser!.displayName, - getPushNotificationText(pushNotification: pushNotification), pushUser!.userID + getPushNotificationText(pushNotification: pushNotification).0, pushUser!.userID ) } else { + let content = getPushNotificationText(pushNotification: pushNotification) return ( - "", getPushNotificationTextWithoutUserId(pushKind: pushNotification.kind), 1 + content.1, content.0, 1 ) } @@ -204,28 +205,31 @@ func readFromKeychain(key: String) -> String? { return nil } -func getPushNotificationText(pushNotification: PushNotification) -> String { +func getPushNotificationText(pushNotification: PushNotification) -> (String, String) { let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] + var title = "Someone" // Define the messages based on the system language if systemLanguage.contains("de") { // German + title = "Jemand" pushNotificationText = [ - .text: "hat dir eine Nachricht gesendet.", - .twonly: "hat dir ein twonly gesendet.", - .video: "hat dir ein Video gesendet.", - .image: "hat dir ein Bild gesendet.", + .text: "hat eine Nachricht gesendet.", + .twonly: "hat ein twonly gesendet.", + .video: "hat ein Video gesendet.", + .image: "hat ein Bild gesendet.", .contactRequest: "möchte sich mit dir vernetzen.", .acceptRequest: "ist jetzt mit dir vernetzt.", .storedMediaFile: "hat dein Bild gespeichert.", .reaction: "hat auf dein Bild reagiert.", .testNotification: "Das ist eine Testbenachrichtigung.", .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "hat mit {{reaction}} auf dein Video reagiert.", - .reactionToText: "hat mit {{reaction}} auf deinen Text reagiert.", - .reactionToImage: "hat mit {{reaction}} auf dein Bild reagiert.", + .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", + .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", + .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", .response: "hat dir geantwortet.", + .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt." ] } else { // Default to English pushNotificationText = [ @@ -239,65 +243,21 @@ func getPushNotificationText(pushNotification: PushNotification) -> String { .reaction: "has reacted to your image.", .testNotification: "This is a test notification.", .reopenedMedia: "has reopened your image.", - .reactionToVideo: "has reacted with {{reaction}} to your video.", - .reactionToText: "has reacted with {{reaction}} to your text.", - .reactionToImage: "has reacted with {{reaction}} to your image.", + .reactionToVideo: "has reacted with {{content}} to your video.", + .reactionToText: "has reacted with {{content}} to your text.", + .reactionToImage: "has reacted with {{content}} to your image.", .response: "has responded.", + .addedToGroup: "has added you to \"{{content}}\"" ] } var content = pushNotificationText[pushNotification.kind] ?? "" - if pushNotification.hasReactionContent { - content.replace("{{reaction}}", with: pushNotification.reactionContent) + if pushNotification.hasAdditionalContent { + content.replace("{{content}}", with: pushNotification.additionalContent) } // Return the corresponding message or an empty string if not found - return content + return (content, title) } -func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { - let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language - - var pushNotificationText: [PushKind: String] = [:] - - // Define the messages based on the system language - if systemLanguage.contains("de") { // German - pushNotificationText = [ - .text: "Du hast eine Nachricht erhalten.", - .twonly: "Du hast ein twonly erhalten.", - .video: "Du hast ein Video erhalten.", - .image: "Du hast ein Bild erhalten.", - .contactRequest: "Du hast eine Kontaktanfrage erhalten.", - .acceptRequest: "Deine Kontaktanfrage wurde angenommen.", - .storedMediaFile: "Dein Bild wurde gespeichert.", - .reaction: "Du hast eine Reaktion auf dein Bild erhalten.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "Du hast eine Reaktion auf dein Video erhalten.", - .reactionToText: "Du hast eine Reaktion auf deinen Text erhalten.", - .reactionToImage: "Du hast eine Reaktion auf dein Bild erhalten.", - .response: "Du hast eine Antwort erhalten.", - ] - } else { // Default to English - pushNotificationText = [ - .text: "You got a message.", - .twonly: "You got a twonly.", - .video: "You got a video.", - .image: "You got an image.", - .contactRequest: "You got a contact request.", - .acceptRequest: "Your contact request has been accepted.", - .storedMediaFile: "Your image has been saved.", - .reaction: "You got a reaction to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "You got a reaction to your video.", - .reactionToText: "You got a reaction to your text.", - .reactionToImage: "You got a reaction to your image.", - .response: "You got a response.", - ] - } - - // Return the corresponding message or an empty string if not found - return pushNotificationText[pushKind] ?? "" -} diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index 3bddafc..d3dee0c 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -37,6 +37,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case reactionToVideo // = 11 case reactionToText // = 12 case reactionToImage // = 13 + case addedToGroup // = 14 case UNRECOGNIZED(Int) init() { @@ -59,6 +60,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case 11: self = .reactionToVideo case 12: self = .reactionToText case 13: self = .reactionToImage + case 14: self = .addedToGroup default: self = .UNRECOGNIZED(rawValue) } } @@ -79,6 +81,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case .reactionToVideo: return 11 case .reactionToText: return 12 case .reactionToImage: return 13 + case .addedToGroup: return 14 case .UNRECOGNIZED(let i): return i } } @@ -99,6 +102,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { .reactionToVideo, .reactionToText, .reactionToImage, + .addedToGroup, ] } @@ -137,21 +141,21 @@ struct PushNotification: Sendable { /// Clears the value of `messageID`. Subsequent reads from it will return its default value. mutating func clearMessageID() {self._messageID = nil} - var reactionContent: String { - get {return _reactionContent ?? String()} - set {_reactionContent = newValue} + var additionalContent: String { + get {return _additionalContent ?? String()} + set {_additionalContent = newValue} } - /// Returns true if `reactionContent` has been explicitly set. - var hasReactionContent: Bool {return self._reactionContent != nil} - /// Clears the value of `reactionContent`. Subsequent reads from it will return its default value. - mutating func clearReactionContent() {self._reactionContent = nil} + /// Returns true if `additionalContent` has been explicitly set. + var hasAdditionalContent: Bool {return self._additionalContent != nil} + /// Clears the value of `additionalContent`. Subsequent reads from it will return its default value. + mutating func clearAdditionalContent() {self._additionalContent = nil} var unknownFields = SwiftProtobuf.UnknownStorage() init() {} fileprivate var _messageID: String? = nil - fileprivate var _reactionContent: String? = nil + fileprivate var _additionalContent: String? = nil } struct PushUsers: Sendable { @@ -214,7 +218,7 @@ struct PushKey: Sendable { // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PushKind: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0") + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}addedToGroup\0") } extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -264,7 +268,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushNotification" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}reactionContent\0") + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}additionalContent\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -274,7 +278,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }() case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }() - case 3: try { try decoder.decodeSingularStringField(value: &self._reactionContent) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._additionalContent) }() default: break } } @@ -291,7 +295,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme try { if let v = self._messageID { try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() - try { if let v = self._reactionContent { + try { if let v = self._additionalContent { try visitor.visitSingularStringField(value: v, fieldNumber: 3) } }() try unknownFields.traverse(visitor: &visitor) @@ -300,7 +304,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme static func ==(lhs: PushNotification, rhs: PushNotification) -> Bool { if lhs.kind != rhs.kind {return false} if lhs._messageID != rhs._messageID {return false} - if lhs._reactionContent != rhs._reactionContent {return false} + if lhs._additionalContent != rhs._additionalContent {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 2e6e6e4..8c167fb 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -773,5 +773,22 @@ "twonlySafeRecoverDesc": "Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.", "@twonlySafeRecoverDesc": {}, "twonlySafeRecoverBtn": "Backup wiederherstellen", - "@twonlySafeRecoverBtn": {} -} + "@twonlySafeRecoverBtn": {}, + "notificationText": "hat eine Nachricht gesendet.", + "notificationTwonly": "hat ein twonly gesendet.", + "notificationVideo": "hat ein Video gesendet.", + "notificationImage": "hat ein Bild gesendet.", + "notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.", + "notificationContactRequest": "möchte sich mit dir vernetzen.", + "notificationAcceptRequest": "ist jetzt mit dir vernetzt.", + "notificationStoredMediaFile": "hat dein Bild gespeichert.", + "notificationReaction": "hat auf dein Bild reagiert.", + "notificationReopenedMedia": "hat dein Bild erneut geöffnet.", + "notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.", + "notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.", + "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", + "notificationResponse": "hat dir geantwortet.", + "notificationTitleUnknownUser": "Jemand", + "notificationCategoryMessageTitle": "Nachrichten", + "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern." +} \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index a74ea19..f412f7e 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -552,5 +552,22 @@ "youLeftGroup": "You have left the group.", "makerLeftGroup": "{maker} has left the group.", "groupActionYou": "you", - "groupActionYour": "your" + "groupActionYour": "your", + "notificationText": "sent a message.", + "notificationTwonly": "sent a twonly.", + "notificationVideo": "sent a video.", + "notificationImage": "sent a image.", + "notificationAddedToGroup": "has added you to \"{groupname}\"", + "notificationContactRequest": "wants to connect with you.", + "notificationAcceptRequest": "is now connected with you.", + "notificationStoredMediaFile": "has stored your image.", + "notificationReaction": "has reacted to your image.", + "notificationReopenedMedia": "has reopened your image.", + "notificationReactionToVideo": "has reacted with {reaction} to your video.", + "notificationReactionToText": "has reacted with {reaction} to your message.", + "notificationReactionToImage": "has reacted with {reaction} to your image.", + "notificationResponse": "has responded.", + "notificationTitleUnknownUser": "Someone", + "notificationCategoryMessageTitle": "Messages", + "notificationCategoryMessageDesc": "Messages from other users." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 38d0dbe..1b37278 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2327,85 +2327,85 @@ abstract class AppLocalizations { /// No description provided for @youChangedGroupName. /// /// In en, this message translates to: - /// **'Du hast den Gruppennamen zu „{newGroupName}“ geändert.'** + /// **'You have changed the group name to \"{newGroupName}\".'** String youChangedGroupName(Object newGroupName); /// No description provided for @makerChangedGroupName. /// /// In en, this message translates to: - /// **'{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.'** + /// **'{maker} has changed the group name to \"{newGroupName}\".'** String makerChangedGroupName(Object maker, Object newGroupName); /// No description provided for @youCreatedGroup. /// /// In en, this message translates to: - /// **'Du hast die Gruppe erstellt.'** + /// **'You have created the group.'** String get youCreatedGroup; /// No description provided for @makerCreatedGroup. /// /// In en, this message translates to: - /// **'{maker} hat die Gruppe erstellt.'** + /// **'{maker} has created the group.'** String makerCreatedGroup(Object maker); /// No description provided for @youRemovedMember. /// /// In en, this message translates to: - /// **'Du hast {affected} aus der Gruppe entfernt.'** + /// **'You have removed {affected} from the group.'** String youRemovedMember(Object affected); /// No description provided for @makerRemovedMember. /// /// In en, this message translates to: - /// **'{maker} hat {affected} aus der Gruppe entfernt.'** + /// **'{maker} has removed {affected} from the group.'** String makerRemovedMember(Object affected, Object maker); /// No description provided for @youAddedMember. /// /// In en, this message translates to: - /// **'Du hast {affected} zur Gruppe hinzugefügt.'** + /// **'You have added {affected} to the group.'** String youAddedMember(Object affected); /// No description provided for @makerAddedMember. /// /// In en, this message translates to: - /// **'{maker} hat {affected} zur Gruppe hinzugefügt.'** + /// **'{maker} has added {affected} to the group.'** String makerAddedMember(Object affected, Object maker); /// No description provided for @youMadeAdmin. /// /// In en, this message translates to: - /// **'Du hast {affected} zum Administrator gemacht.'** + /// **'You made {affected} an admin.'** String youMadeAdmin(Object affected); /// No description provided for @makerMadeAdmin. /// /// In en, this message translates to: - /// **'{maker} hat {affected} zum Administrator gemacht.'** + /// **'{maker} made {affected} an admin.'** String makerMadeAdmin(Object affected, Object maker); /// No description provided for @youRevokedAdminRights. /// /// In en, this message translates to: - /// **'Du hast {affectedR} die Administratorrechte entzogen.'** + /// **'You revoked {affectedR} admin rights.'** String youRevokedAdminRights(Object affectedR); /// No description provided for @makerRevokedAdminRights. /// /// In en, this message translates to: - /// **'{maker} hat {affectedR} die Administratorrechte entzogen.'** + /// **'{maker} revoked {affectedR} admin rights.'** String makerRevokedAdminRights(Object affectedR, Object maker); /// No description provided for @youLeftGroup. /// /// In en, this message translates to: - /// **'Du hast die Gruppe verlassen.'** + /// **'You have left the group.'** String get youLeftGroup; /// No description provided for @makerLeftGroup. /// /// In en, this message translates to: - /// **'{maker} hat die Gruppe verlassen.'** + /// **'{maker} has left the group.'** String makerLeftGroup(Object maker); /// No description provided for @groupActionYou. @@ -2419,6 +2419,108 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'your'** String get groupActionYour; + + /// No description provided for @notificationText. + /// + /// In en, this message translates to: + /// **'sent a message.'** + String get notificationText; + + /// No description provided for @notificationTwonly. + /// + /// In en, this message translates to: + /// **'sent a twonly.'** + String get notificationTwonly; + + /// No description provided for @notificationVideo. + /// + /// In en, this message translates to: + /// **'sent a video.'** + String get notificationVideo; + + /// No description provided for @notificationImage. + /// + /// In en, this message translates to: + /// **'sent a image.'** + String get notificationImage; + + /// No description provided for @notificationAddedToGroup. + /// + /// In en, this message translates to: + /// **'has added you to \"{groupname}\"'** + String notificationAddedToGroup(Object groupname); + + /// No description provided for @notificationContactRequest. + /// + /// In en, this message translates to: + /// **'wants to connect with you.'** + String get notificationContactRequest; + + /// No description provided for @notificationAcceptRequest. + /// + /// In en, this message translates to: + /// **'is now connected with you.'** + String get notificationAcceptRequest; + + /// No description provided for @notificationStoredMediaFile. + /// + /// In en, this message translates to: + /// **'has stored your image.'** + String get notificationStoredMediaFile; + + /// No description provided for @notificationReaction. + /// + /// In en, this message translates to: + /// **'has reacted to your image.'** + String get notificationReaction; + + /// No description provided for @notificationReopenedMedia. + /// + /// In en, this message translates to: + /// **'has reopened your image.'** + String get notificationReopenedMedia; + + /// No description provided for @notificationReactionToVideo. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your video.'** + String notificationReactionToVideo(Object reaction); + + /// No description provided for @notificationReactionToText. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your message.'** + String notificationReactionToText(Object reaction); + + /// No description provided for @notificationReactionToImage. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your image.'** + String notificationReactionToImage(Object reaction); + + /// No description provided for @notificationResponse. + /// + /// In en, this message translates to: + /// **'has responded.'** + String get notificationResponse; + + /// No description provided for @notificationTitleUnknownUser. + /// + /// In en, this message translates to: + /// **'Someone'** + String get notificationTitleUnknownUser; + + /// No description provided for @notificationCategoryMessageTitle. + /// + /// In en, this message translates to: + /// **'Messages'** + String get notificationCategoryMessageTitle; + + /// No description provided for @notificationCategoryMessageDesc. + /// + /// In en, this message translates to: + /// **'Messages from other users.'** + String get notificationCategoryMessageDesc; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index ceec42c..0315d9a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1070,10 +1070,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Backup, you can restore it here.'; + 'Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.'; @override - String get twonlySafeRecoverBtn => 'Restore backup'; + String get twonlySafeRecoverBtn => 'Backup wiederherstellen'; @override String get inviteFriends => 'Freunde einladen'; @@ -1308,4 +1308,64 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groupActionYour => 'deine'; + + @override + String get notificationText => 'hat eine Nachricht gesendet.'; + + @override + String get notificationTwonly => 'hat ein twonly gesendet.'; + + @override + String get notificationVideo => 'hat ein Video gesendet.'; + + @override + String get notificationImage => 'hat ein Bild gesendet.'; + + @override + String notificationAddedToGroup(Object groupname) { + return 'hat dich zu \"$groupname\" hinzugefügt.'; + } + + @override + String get notificationContactRequest => 'möchte sich mit dir vernetzen.'; + + @override + String get notificationAcceptRequest => 'ist jetzt mit dir vernetzt.'; + + @override + String get notificationStoredMediaFile => 'hat dein Bild gespeichert.'; + + @override + String get notificationReaction => 'hat auf dein Bild reagiert.'; + + @override + String get notificationReopenedMedia => 'hat dein Bild erneut geöffnet.'; + + @override + String notificationReactionToVideo(Object reaction) { + return 'hat mit $reaction auf dein Video reagiert.'; + } + + @override + String notificationReactionToText(Object reaction) { + return 'hat mit $reaction auf deine Nachricht reagiert.'; + } + + @override + String notificationReactionToImage(Object reaction) { + return 'hat mit $reaction auf dein Bild reagiert.'; + } + + @override + String get notificationResponse => 'hat dir geantwortet.'; + + @override + String get notificationTitleUnknownUser => 'Jemand'; + + @override + String get notificationCategoryMessageTitle => 'Nachrichten'; + + @override + String get notificationCategoryMessageDesc => + 'Nachrichten von anderen Benutzern.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 99f7695..032ca02 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1232,68 +1232,68 @@ class AppLocalizationsEn extends AppLocalizations { @override String youChangedGroupName(Object newGroupName) { - return 'Du hast den Gruppennamen zu „$newGroupName“ geändert.'; + return 'You have changed the group name to \"$newGroupName\".'; } @override String makerChangedGroupName(Object maker, Object newGroupName) { - return '$maker hat den Gruppennamen zu „$newGroupName“ geändert.'; + return '$maker has changed the group name to \"$newGroupName\".'; } @override - String get youCreatedGroup => 'Du hast die Gruppe erstellt.'; + String get youCreatedGroup => 'You have created the group.'; @override String makerCreatedGroup(Object maker) { - return '$maker hat die Gruppe erstellt.'; + return '$maker has created the group.'; } @override String youRemovedMember(Object affected) { - return 'Du hast $affected aus der Gruppe entfernt.'; + return 'You have removed $affected from the group.'; } @override String makerRemovedMember(Object affected, Object maker) { - return '$maker hat $affected aus der Gruppe entfernt.'; + return '$maker has removed $affected from the group.'; } @override String youAddedMember(Object affected) { - return 'Du hast $affected zur Gruppe hinzugefügt.'; + return 'You have added $affected to the group.'; } @override String makerAddedMember(Object affected, Object maker) { - return '$maker hat $affected zur Gruppe hinzugefügt.'; + return '$maker has added $affected to the group.'; } @override String youMadeAdmin(Object affected) { - return 'Du hast $affected zum Administrator gemacht.'; + return 'You made $affected an admin.'; } @override String makerMadeAdmin(Object affected, Object maker) { - return '$maker hat $affected zum Administrator gemacht.'; + return '$maker made $affected an admin.'; } @override String youRevokedAdminRights(Object affectedR) { - return 'Du hast $affectedR die Administratorrechte entzogen.'; + return 'You revoked $affectedR admin rights.'; } @override String makerRevokedAdminRights(Object affectedR, Object maker) { - return '$maker hat $affectedR die Administratorrechte entzogen.'; + return '$maker revoked $affectedR admin rights.'; } @override - String get youLeftGroup => 'Du hast die Gruppe verlassen.'; + String get youLeftGroup => 'You have left the group.'; @override String makerLeftGroup(Object maker) { - return '$maker hat die Gruppe verlassen.'; + return '$maker has left the group.'; } @override @@ -1301,4 +1301,63 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groupActionYour => 'your'; + + @override + String get notificationText => 'sent a message.'; + + @override + String get notificationTwonly => 'sent a twonly.'; + + @override + String get notificationVideo => 'sent a video.'; + + @override + String get notificationImage => 'sent a image.'; + + @override + String notificationAddedToGroup(Object groupname) { + return 'has added you to \"$groupname\"'; + } + + @override + String get notificationContactRequest => 'wants to connect with you.'; + + @override + String get notificationAcceptRequest => 'is now connected with you.'; + + @override + String get notificationStoredMediaFile => 'has stored your image.'; + + @override + String get notificationReaction => 'has reacted to your image.'; + + @override + String get notificationReopenedMedia => 'has reopened your image.'; + + @override + String notificationReactionToVideo(Object reaction) { + return 'has reacted with $reaction to your video.'; + } + + @override + String notificationReactionToText(Object reaction) { + return 'has reacted with $reaction to your message.'; + } + + @override + String notificationReactionToImage(Object reaction) { + return 'has reacted with $reaction to your image.'; + } + + @override + String get notificationResponse => 'has responded.'; + + @override + String get notificationTitleUnknownUser => 'Someone'; + + @override + String get notificationCategoryMessageTitle => 'Messages'; + + @override + String get notificationCategoryMessageDesc => 'Messages from other users.'; } diff --git a/lib/src/model/protobuf/client/generated/push_notification.pb.dart b/lib/src/model/protobuf/client/generated/push_notification.pb.dart index eb929fd..c4ad7fe 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pb.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pb.dart @@ -114,7 +114,7 @@ class PushNotification extends $pb.GeneratedMessage { factory PushNotification({ PushKind? kind, $core.String? messageId, - $core.String? reactionContent, + $core.String? additionalContent, }) { final $result = create(); if (kind != null) { @@ -123,8 +123,8 @@ class PushNotification extends $pb.GeneratedMessage { if (messageId != null) { $result.messageId = messageId; } - if (reactionContent != null) { - $result.reactionContent = reactionContent; + if (additionalContent != null) { + $result.additionalContent = additionalContent; } return $result; } @@ -135,7 +135,7 @@ class PushNotification extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushNotification', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: PushKind.reaction, valueOf: PushKind.valueOf, enumValues: PushKind.values) ..aOS(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') - ..aOS(3, _omitFieldNames ? '' : 'reactionContent', protoName: 'reactionContent') + ..aOS(3, _omitFieldNames ? '' : 'additionalContent', protoName: 'additionalContent') ..hasRequiredFields = false ; @@ -179,13 +179,13 @@ class PushNotification extends $pb.GeneratedMessage { void clearMessageId() => clearField(2); @$pb.TagNumber(3) - $core.String get reactionContent => $_getSZ(2); + $core.String get additionalContent => $_getSZ(2); @$pb.TagNumber(3) - set reactionContent($core.String v) { $_setString(2, v); } + set additionalContent($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) - $core.bool hasReactionContent() => $_has(2); + $core.bool hasAdditionalContent() => $_has(2); @$pb.TagNumber(3) - void clearReactionContent() => clearField(3); + void clearAdditionalContent() => clearField(3); } class PushUsers extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart index 8bf3ded..e2d05ee 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart @@ -28,6 +28,7 @@ class PushKind extends $pb.ProtobufEnum { static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo'); static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText'); static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage'); + static const PushKind addedToGroup = PushKind._(14, _omitEnumNames ? '' : 'addedToGroup'); static const $core.List values = [ reaction, @@ -44,6 +45,7 @@ class PushKind extends $pb.ProtobufEnum { reactionToVideo, reactionToText, reactionToImage, + addedToGroup, ]; static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart index 8ff6dc0..9782873 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart @@ -31,6 +31,7 @@ const PushKind$json = { {'1': 'reactionToVideo', '2': 11}, {'1': 'reactionToText', '2': 12}, {'1': 'reactionToImage', '2': 13}, + {'1': 'addedToGroup', '2': 14}, ], }; @@ -40,7 +41,7 @@ final $typed_data.Uint8List pushKindDescriptor = $convert.base64Decode( 'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw' 'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ' 'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0' - 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0='); + 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEAoMYWRkZWRUb0dyb3VwEA4='); @$core.Deprecated('Use encryptedPushNotificationDescriptor instead') const EncryptedPushNotification$json = { @@ -65,19 +66,20 @@ const PushNotification$json = { '2': [ {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.PushKind', '10': 'kind'}, {'1': 'messageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'messageId', '17': true}, - {'1': 'reactionContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'reactionContent', '17': true}, + {'1': 'additionalContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'additionalContent', '17': true}, ], '8': [ {'1': '_messageId'}, - {'1': '_reactionContent'}, + {'1': '_additionalContent'}, ], }; /// Descriptor for `PushNotification`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List pushNotificationDescriptor = $convert.base64Decode( 'ChBQdXNoTm90aWZpY2F0aW9uEh0KBGtpbmQYASABKA4yCS5QdXNoS2luZFIEa2luZBIhCgltZX' - 'NzYWdlSWQYAiABKAlIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB' - 'Ug9yZWFjdGlvbkNvbnRlbnSIAQFCDAoKX21lc3NhZ2VJZEISChBfcmVhY3Rpb25Db250ZW50'); + 'NzYWdlSWQYAiABKAlIAFIJbWVzc2FnZUlkiAEBEjEKEWFkZGl0aW9uYWxDb250ZW50GAMgASgJ' + 'SAFSEWFkZGl0aW9uYWxDb250ZW50iAEBQgwKCl9tZXNzYWdlSWRCFAoSX2FkZGl0aW9uYWxDb2' + '50ZW50'); @$core.Deprecated('Use pushUsersDescriptor instead') const PushUsers$json = { diff --git a/lib/src/model/protobuf/client/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto index 0abeedb..c30d715 100644 --- a/lib/src/model/protobuf/client/push_notification.proto +++ b/lib/src/model/protobuf/client/push_notification.proto @@ -22,12 +22,13 @@ enum PushKind { reactionToVideo = 11; reactionToText = 12; reactionToImage = 13; + addedToGroup = 14; }; message PushNotification { PushKind kind = 1; optional string messageId = 2; - optional string reactionContent = 3; + optional string additionalContent = 3; } diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index be510f0..f998723 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -7,6 +7,9 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; +import 'package:twonly/src/localization/generated/app_localizations.dart'; +import 'package:twonly/src/localization/generated/app_localizations_de.dart'; +import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; @@ -148,10 +151,12 @@ Future showLocalPushNotification( styleInformation = FilePathAndroidBitmap(avatarPath); } + final lang = getLocalizations(); + final androidNotificationDetails = AndroidNotificationDetails( '0', - 'Messages', - channelDescription: 'Messages from other users.', + lang.notificationCategoryMessageTitle, + channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, ticker: 'You got a new message.', @@ -176,25 +181,29 @@ Future showLocalPushNotification( Future showLocalPushNotificationWithoutUserId( PushNotification pushNotification, ) async { - String? title; String? body; - body = getPushNotificationTextWithoutUserId(pushNotification.kind); + body = getPushNotificationText(pushNotification); + + final lang = getLocalizations(); + + final title = lang.notificationTitleUnknownUser; + if (body == '') { Log.error('No push notification type defined!'); } - const androidNotificationDetails = AndroidNotificationDetails( + final androidNotificationDetails = AndroidNotificationDetails( '0', - 'Messages', - channelDescription: 'Messages from other users.', + lang.notificationCategoryMessageTitle, + channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, ticker: 'You got a new message.', ); const darwinNotificationDetails = DarwinNotificationDetails(); - const notificationDetails = NotificationDetails( + final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); @@ -219,104 +228,35 @@ Future getAvatarIcon(int contactId) async { return null; } -String getPushNotificationTextWithoutUserId(PushKind pushKind) { - Map pushNotificationText; - +AppLocalizations getLocalizations() { final systemLanguage = Platform.localeName; - - if (systemLanguage.contains('de')) { - pushNotificationText = { - PushKind.text.name: 'Du hast eine neue Nachricht erhalten.', - PushKind.twonly.name: 'Du hast ein neues twonly erhalten.', - PushKind.video.name: 'Du hast ein neues Video erhalten.', - PushKind.image.name: 'Du hast ein neues Bild erhalten.', - PushKind.contactRequest.name: - 'Du hast eine neue Kontaktanfrage erhalten.', - PushKind.acceptRequest.name: 'Deine Kontaktanfrage wurde angenommen.', - PushKind.storedMediaFile.name: 'Dein Bild wurde gespeichert.', - PushKind.reaction.name: 'Du hast eine Reaktion auf dein Bild erhalten.', - PushKind.reopenedMedia.name: 'Dein Bild wurde erneut geöffnet.', - PushKind.reactionToVideo.name: - 'Du hast eine Reaktion auf dein Video erhalten.', - PushKind.reactionToText.name: - 'Du hast eine Reaktion auf deinen Text erhalten.', - PushKind.reactionToImage.name: - 'Du hast eine Reaktion auf dein Bild erhalten.', - PushKind.response.name: 'Du hast eine Antwort erhalten.', - }; - } else { - pushNotificationText = { - PushKind.text.name: 'You have received a new message.', - PushKind.twonly.name: 'You have received a new twonly.', - PushKind.video.name: 'You have received a new video.', - PushKind.image.name: 'You have received a new image.', - PushKind.contactRequest.name: 'You have received a new contact request.', - PushKind.acceptRequest.name: 'Your contact request has been accepted.', - PushKind.storedMediaFile.name: 'Your image has been saved.', - PushKind.reaction.name: 'You have received a reaction to your image.', - PushKind.reopenedMedia.name: 'Your image has been reopened.', - PushKind.reactionToVideo.name: - 'You have received a reaction to your video.', - PushKind.reactionToText.name: - 'You have received a reaction to your text.', - PushKind.reactionToImage.name: - 'You have received a reaction to your image.', - PushKind.response.name: 'You have received a response.', - }; - } - return pushNotificationText[pushKind.name] ?? ''; + if (systemLanguage.contains('de')) return AppLocalizationsDe(); + return AppLocalizationsEn(); } String getPushNotificationText(PushNotification pushNotification) { - final systemLanguage = Platform.localeName; + final lang = getLocalizations(); - Map pushNotificationText; + final pushNotificationText = { + PushKind.text.name: lang.notificationText, + PushKind.twonly.name: lang.notificationTwonly, + PushKind.video.name: lang.notificationVideo, + PushKind.image.name: lang.notificationImage, + PushKind.contactRequest.name: lang.notificationContactRequest, + PushKind.acceptRequest.name: lang.notificationAcceptRequest, + PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, + PushKind.reaction.name: lang.notificationReaction, + PushKind.reopenedMedia.name: lang.notificationReopenedMedia, + PushKind.reactionToVideo.name: + lang.notificationReactionToVideo(pushNotification.additionalContent), + PushKind.reactionToText.name: + lang.notificationReactionToText(pushNotification.additionalContent), + PushKind.reactionToImage.name: + lang.notificationReactionToImage(pushNotification.additionalContent), + PushKind.response.name: lang.notificationResponse, + PushKind.addedToGroup.name: + lang.notificationAddedToGroup(pushNotification.additionalContent), + }; - if (systemLanguage.contains('de')) { - pushNotificationText = { - PushKind.text.name: 'hat dir eine Nachricht gesendet.', - PushKind.twonly.name: 'hat dir ein twonly gesendet.', - PushKind.video.name: 'hat dir ein Video gesendet.', - PushKind.image.name: 'hat dir ein Bild gesendet.', - PushKind.contactRequest.name: 'möchte sich mit dir vernetzen.', - PushKind.acceptRequest.name: 'ist jetzt mit dir vernetzt.', - PushKind.storedMediaFile.name: 'hat dein Bild gespeichert.', - PushKind.reaction.name: 'hat auf dein Bild reagiert.', - PushKind.reopenedMedia.name: 'hat dein Bild erneut geöffnet.', - PushKind.reactionToVideo.name: - 'hat mit {{reaction}} auf dein Video reagiert.', - PushKind.reactionToText.name: - 'hat mit {{reaction}} auf deine Nachricht reagiert.', - PushKind.reactionToImage.name: - 'hat mit {{reaction}} auf dein Bild reagiert.', - PushKind.response.name: 'hat dir geantwortet.', - }; - } else { - pushNotificationText = { - PushKind.text.name: 'has sent you a message.', - PushKind.twonly.name: 'has sent you a twonly.', - PushKind.video.name: 'has sent you a video.', - PushKind.image.name: 'has sent you an image.', - PushKind.contactRequest.name: 'wants to connect with you.', - PushKind.acceptRequest.name: 'is now connected with you.', - PushKind.storedMediaFile.name: 'has stored your image.', - PushKind.reaction.name: 'has reacted to your image.', - PushKind.reopenedMedia.name: 'has reopened your image.', - PushKind.reactionToVideo.name: - 'has reacted with {{reaction}} to your video.', - PushKind.reactionToText.name: - 'has reacted with {{reaction}} to your message.', - PushKind.reactionToImage.name: - 'has reacted with {{reaction}} to your image.', - PushKind.response.name: 'has responded.', - }; - } - var contentText = pushNotificationText[pushNotification.kind.name] ?? ''; - if (pushNotification.hasReactionContent()) { - contentText = contentText.replaceAll( - '{{reaction}}', - pushNotification.reactionContent, - ); - } - return contentText; + return pushNotificationText[pushNotification.kind.name] ?? ''; } diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 4a50c61..312c3ad 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -201,7 +201,7 @@ Future getPushNotificationFromEncryptedContent( EncryptedContent content, ) async { PushKind? kind; - String? reactionContent; + String? additionalContent; if (content.hasReaction()) { if (content.reaction.remove) return null; @@ -209,7 +209,9 @@ Future getPushNotificationFromEncryptedContent( final msg = await twonlyDB.messagesDao .getMessageById(content.reaction.targetMessageId) .getSingleOrNull(); - if (msg == null) return null; + if (msg == null || msg.senderId == null || msg.senderId != toUserId) { + return null; + } if (msg.content != null) { kind = PushKind.reactionToText; } else if (msg.mediaId != null) { @@ -224,7 +226,7 @@ Future getPushNotificationFromEncryptedContent( kind = PushKind.reaction; } } - reactionContent = content.reaction.emoji; + additionalContent = content.reaction.emoji; } if (content.hasTextMessage()) { @@ -270,11 +272,17 @@ Future getPushNotificationFromEncryptedContent( } } + if (content.hasGroupCreate()) { + kind = PushKind.addedToGroup; + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + additionalContent = group!.groupName; + } + if (kind == null) return null; final pushNotification = PushNotification()..kind = kind; - if (reactionContent != null) { - pushNotification.reactionContent = reactionContent; + if (additionalContent != null) { + pushNotification.additionalContent = additionalContent; } if (messageId != null) { pushNotification.messageId = messageId; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 49c2b5f..d1b0112 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -75,7 +75,7 @@ class _ShareImageEditorView extends State { selectedGroupIds.add(widget.sendToGroup!.groupId); } - if (widget.mediaFileService.mediaFile.type == MediaType.video || + if (widget.mediaFileService.mediaFile.type == MediaType.image || widget.mediaFileService.mediaFile.type == MediaType.gif) { if (widget.imageBytesFuture != null) { loadImage(widget.imageBytesFuture!); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index b6f2d06..ff8857a 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -77,9 +77,12 @@ class _ChatMessagesViewState extends State { late StreamSubscription userSub; late StreamSubscription> messageSub; late StreamSubscription>? groupActionsSub; + late StreamSubscription>? contactSub; late StreamSubscription>>? lastOpenedMessageByContactSub; + Map userIdToContact = {}; + List messages = []; List allMessages = []; List<(Message, Contact)> lastOpenedMessageByContact = []; @@ -110,6 +113,7 @@ class _ChatMessagesViewState extends State { void dispose() { userSub.cancel(); messageSub.cancel(); + contactSub?.cancel(); groupActionsSub?.cancel(); lastOpenedMessageByContactSub?.cancel(); tutorial?.cancel(); @@ -143,6 +147,13 @@ class _ChatMessagesViewState extends State { groupActions = update; await setMessages(allMessages, lastOpenedMessageByContact, update); }); + + final contactsStream = twonlyDB.contactsDao.watchAllContacts(); + contactSub = contactsStream.listen((contacts) { + for (final contact in contacts) { + userIdToContact[contact.userId] = contact; + } + }); } final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); @@ -408,6 +419,7 @@ class _ChatMessagesViewState extends State { : null, group: group, galleryItems: galleryItems, + userIdToContact: userIdToContact, scrollToMessage: scrollToMessage, onResponseTriggered: () { setState(() { diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 646609e..0c48305 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -14,6 +14,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/message_actions. import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; class ChatListEntry extends StatefulWidget { const ChatListEntry({ @@ -24,6 +25,7 @@ class ChatListEntry extends StatefulWidget { this.onResponseTriggered, this.prevMessage, this.nextMessage, + this.userIdToContact, this.hideReactions = false, super.key, }); @@ -31,6 +33,7 @@ class ChatListEntry extends StatefulWidget { final Message? nextMessage; final Message message; final Group group; + final Map? userIdToContact; final bool hideReactions; final List galleryItems; final void Function(String)? scrollToMessage; @@ -108,6 +111,8 @@ class _ChatListEntryState extends State { ChatTextEntry( message: widget.message, nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + userIdToContact: widget.userIdToContact, borderRadius: borderRadius, minWidth: reactionsForWidth * 43, ) @@ -124,6 +129,8 @@ class _ChatListEntryState extends State { ? ChatTextEntry( message: widget.message, nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + userIdToContact: widget.userIdToContact, borderRadius: borderRadius, minWidth: reactionsForWidth * 43, ) @@ -182,9 +189,20 @@ class _ChatListEntryState extends State { if (!right && !widget.group.isDirectChat) hideContactAvatar ? const SizedBox(width: 24) - : AvatarIcon( - contactId: widget.message.senderId, - fontSize: 12, + : GestureDetector( + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ContactView(widget.message.senderId!), + ), + ); + }, + child: AvatarIcon( + contactId: widget.message.senderId, + fontSize: 12, + ), ), child, ], diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index ee5f4b7..c22df9f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart' hide TextDirection; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -12,13 +13,17 @@ class ChatTextEntry extends StatelessWidget { const ChatTextEntry({ required this.message, required this.nextMessage, + required this.prevMessage, required this.borderRadius, + required this.userIdToContact, required this.minWidth, super.key, }); final Message message; final Message? nextMessage; + final Message? prevMessage; + final Map? userIdToContact; final BorderRadius borderRadius; final double minWidth; @@ -41,6 +46,17 @@ class ChatTextEntry extends StatelessWidget { } var displayTime = !combineTextMessageWithNext(message, nextMessage); + var displayUserName = ''; + if (message.senderId != null && + prevMessage != null && + userIdToContact != null) { + if (!combineTextMessageWithNext(prevMessage!, message)) { + if (userIdToContact![message.senderId] != null) { + displayUserName = + getContactDisplayName(userIdToContact![message.senderId]!); + } + } + } var spacerWidth = minWidth - measureTextWidth(text) - 53; if (spacerWidth < 0) spacerWidth = 0; @@ -75,57 +91,71 @@ class ChatTextEntry extends StatelessWidget { color: color, borderRadius: borderRadius, ), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (expanded) - Expanded( - child: BetterText(text: text, textColor: textColor), - ) - else ...[ - BetterText(text: text, textColor: textColor), - SizedBox( - width: spacerWidth, - ), - ], - if (displayTime || message.modifiedAt != null) - Align( - alignment: AlignmentGeometry.centerRight, - child: Padding( - padding: const EdgeInsets.only(left: 6), - child: Row( - children: [ - if (message.modifiedAt != null) - Padding( - padding: const EdgeInsets.only(right: 5), - child: SizedBox( - height: 10, - child: FaIcon( - FontAwesomeIcons.pencil, - color: Colors.white.withAlpha(150), - size: 10, - ), - ), - ), - Text( - friendlyTime( - context, - (message.modifiedAt != null) - ? message.modifiedAt! - : message.createdAt, - ), - style: TextStyle( - fontSize: 10, - color: Colors.white.withAlpha(150), - decoration: TextDecoration.none, - fontWeight: FontWeight.normal, - ), - ), - ], - ), + if (displayUserName != '') + Text( + displayUserName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, ), ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (expanded) + Expanded( + child: BetterText(text: text, textColor: textColor), + ) + else ...[ + BetterText(text: text, textColor: textColor), + SizedBox( + width: spacerWidth, + ), + ], + if (displayTime || message.modifiedAt != null) + Align( + alignment: AlignmentGeometry.centerRight, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Row( + children: [ + if (message.modifiedAt != null) + Padding( + padding: const EdgeInsets.only(right: 5), + child: SizedBox( + height: 10, + child: FaIcon( + FontAwesomeIcons.pencil, + color: Colors.white.withAlpha(150), + size: 10, + ), + ), + ), + Text( + friendlyTime( + context, + (message.modifiedAt != null) + ? message.modifiedAt! + : message.createdAt, + ), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ), + ], + ), ], ), ); diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 404bb40..a0b96da 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -80,9 +80,9 @@ class _ResponseContainerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( + key: _preview, padding: const EdgeInsets.only(top: 4, right: 4, left: 4), child: Container( - key: _preview, width: minWidth, decoration: BoxDecoration( color: context.color.surface.withAlpha(150), From 562d9cbb746a40ca0867a0638155a8c3010e9687 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 00:50:57 +0100 Subject: [PATCH 53/76] increase padding --- .../bottom_sheets/all_reactions.bottom_sheet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 8853f02..5292d02 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -108,7 +108,7 @@ class _AllReactionsViewState extends State { }, child: Container( padding: const EdgeInsets.symmetric( - vertical: 5, + vertical: 10, horizontal: 30, ), color: Colors.transparent, From 48de7faaa24e0ff9dc7aa1b56bc6da22e4753593 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 11:22:22 +0100 Subject: [PATCH 54/76] fixing avatar icon --- .../NotificationService.swift | 22 +++--- lib/globals.dart | 4 + lib/main.dart | 2 + lib/src/database/daos/groups.dao.dart | 4 + lib/src/database/daos/messages.dao.dart | 10 +++ lib/src/database/tables/groups.table.dart | 1 + lib/src/database/twonly.db.g.dart | 60 +++++++++++++- lib/src/localization/app_de.arb | 11 +-- lib/src/localization/app_en.arb | 11 +-- .../generated/app_localizations.dart | 26 ++++--- .../generated/app_localizations_de.dart | 23 ++++-- .../generated/app_localizations_en.dart | 23 ++++-- .../background.notifications.dart | 17 ++-- .../notifications/pushkeys.notifications.dart | 8 ++ .../notifications/setup.notifications.dart | 2 +- lib/src/utils/misc.dart | 10 +++ lib/src/utils/storage.dart | 10 ++- lib/src/views/chats/add_new_user.view.dart | 2 +- lib/src/views/chats/chat_list.view.dart | 2 +- lib/src/views/chats/chat_messages.view.dart | 2 +- .../all_reactions.bottom_sheet.dart | 4 +- lib/src/views/chats/message_info.view.dart | 2 +- lib/src/views/chats/start_new_chat.view.dart | 2 +- .../components/avatar_icon.component.dart | 78 ++++++++++++------- lib/src/views/contact/contact.view.dart | 2 +- lib/src/views/groups/group.view.dart | 8 +- .../group_create_select_group_name.view.dart | 2 +- .../group_create_select_members.view.dart | 4 +- .../settings/privacy_view_block.users.dart | 2 +- .../views/settings/settings_main.view.dart | 4 +- 30 files changed, 263 insertions(+), 95 deletions(-) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 0091f8f..d3e095e 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -215,10 +215,10 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str if systemLanguage.contains("de") { // German title = "Jemand" pushNotificationText = [ - .text: "hat eine Nachricht gesendet.", - .twonly: "hat ein twonly gesendet.", - .video: "hat ein Video gesendet.", - .image: "hat ein Bild gesendet.", + .text: "hat eine Nachricht{inGroup} gesendet.", + .twonly: "hat ein twonly{inGroup} gesendet.", + .video: "hat ein Video{inGroup} gesendet.", + .image: "hat ein Bild{inGroup} gesendet.", .contactRequest: "möchte sich mit dir vernetzen.", .acceptRequest: "ist jetzt mit dir vernetzt.", .storedMediaFile: "hat dein Bild gespeichert.", @@ -228,15 +228,15 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", - .response: "hat dir geantwortet.", + .response: "hat dir{inGroup} geantwortet.", .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt." ] } else { // Default to English pushNotificationText = [ - .text: "has sent you a message.", - .twonly: "has sent you a twonly.", - .video: "has sent you a video.", - .image: "has sent you an image.", + .text: "sent a message{inGroup}.", + .twonly: "sent a twonly{inGroup}.", + .video: "sent a video{inGroup}.", + .image: "sent a image{inGroup}.", .contactRequest: "wants to connect with you.", .acceptRequest: "is now connected with you.", .storedMediaFile: "has stored your image.", @@ -246,7 +246,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToVideo: "has reacted with {{content}} to your video.", .reactionToText: "has reacted with {{content}} to your text.", .reactionToImage: "has reacted with {{content}} to your image.", - .response: "has responded.", + .response: "has responded{inGroup}.", .addedToGroup: "has added you to \"{{content}}\"" ] } @@ -255,6 +255,8 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str if pushNotification.hasAdditionalContent { content.replace("{{content}}", with: pushNotification.additionalContent) + content.replace("{inGroup}", with: " in {inGroup}") + content.replace("{inGroup}", with: pushNotification.additionalContent) } // Return the corresponding message or an empty string if not found diff --git a/lib/globals.dart b/lib/globals.dart index 36562dd..3bf3b22 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:camera/camera.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; @@ -26,4 +28,6 @@ void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackNewDeviceRegistered = () {}; void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; +Map globalUserDataChangedCallBack = {}; + bool globalIsAppInBackground = true; diff --git a/lib/main.dart b/lib/main.dart index 9df9558..61644cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:twonly/src/services/api/mediafiles/media_background.service.dart import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -59,6 +60,7 @@ void main() async { unawaited(finishStartedPreprocessing()); unawaited(MediaFileService.purgeTempFolder()); + unawaited(createPushAvatars()); await twonlyDB.messagesDao.purgeMessageTable(); // await twonlyDB.messagesDao.resetPendingDownloadState(); diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 96ed572..ca20c7e 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -129,8 +129,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { leftOuterJoin( groupMembers, groupMembers.contactId.equalsExp(contacts.userId), + useColumns: false, ), ]) + ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) ..where(groupMembers.groupId.equals(groupId))); return query.map((row) => row.readTable(contacts)).get(); } @@ -140,8 +142,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { leftOuterJoin( groupMembers, groupMembers.contactId.equalsExp(contacts.userId), + useColumns: false, ), ]) + ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) ..where(groupMembers.groupId.equals(groupId))); return query.map((row) => row.readTable(contacts)).watch(); } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index f68163e..5314944 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -381,6 +381,16 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ), ); + if (message.senderId.present) { + await twonlyDB.groupsDao.updateMember( + message.groupId.value, + message.senderId.value!, + GroupMembersCompanion( + lastMessage: Value(DateTime.now()), + ), + ); + } + return await (select(messages)..where((t) => t.rowId.equals(rowId))) .getSingle(); } catch (e) { diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index ea25e46..3554306 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -60,6 +60,7 @@ class GroupMembers extends Table { TextColumn get memberState => textEnum().nullable()(); BlobColumn get groupPublicKey => blob().nullable()(); + DateTimeColumn get lastMessage => dateTime().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); @override diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index f1589fe..9e58333 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -3987,6 +3987,12 @@ class $GroupMembersTable extends GroupMembers late final GeneratedColumn groupPublicKey = GeneratedColumn('group_public_key', aliasedName, true, type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _lastMessageMeta = + const VerificationMeta('lastMessage'); + @override + late final GeneratedColumn lastMessage = GeneratedColumn( + 'last_message', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -3997,7 +4003,7 @@ class $GroupMembersTable extends GroupMembers defaultValue: currentDateAndTime); @override List get $columns => - [groupId, contactId, memberState, groupPublicKey, createdAt]; + [groupId, contactId, memberState, groupPublicKey, lastMessage, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -4026,6 +4032,12 @@ class $GroupMembersTable extends GroupMembers groupPublicKey.isAcceptableOrUnknown( data['group_public_key']!, _groupPublicKeyMeta)); } + if (data.containsKey('last_message')) { + context.handle( + _lastMessageMeta, + lastMessage.isAcceptableOrUnknown( + data['last_message']!, _lastMessageMeta)); + } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); @@ -4048,6 +4060,8 @@ class $GroupMembersTable extends GroupMembers DriftSqlType.string, data['${effectivePrefix}member_state'])), groupPublicKey: attachedDatabase.typeMapping .read(DriftSqlType.blob, data['${effectivePrefix}group_public_key']), + lastMessage: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_message']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); @@ -4070,12 +4084,14 @@ class GroupMember extends DataClass implements Insertable { final int contactId; final MemberState? memberState; final Uint8List? groupPublicKey; + final DateTime? lastMessage; final DateTime createdAt; const GroupMember( {required this.groupId, required this.contactId, this.memberState, this.groupPublicKey, + this.lastMessage, required this.createdAt}); @override Map toColumns(bool nullToAbsent) { @@ -4089,6 +4105,9 @@ class GroupMember extends DataClass implements Insertable { if (!nullToAbsent || groupPublicKey != null) { map['group_public_key'] = Variable(groupPublicKey); } + if (!nullToAbsent || lastMessage != null) { + map['last_message'] = Variable(lastMessage); + } map['created_at'] = Variable(createdAt); return map; } @@ -4103,6 +4122,9 @@ class GroupMember extends DataClass implements Insertable { groupPublicKey: groupPublicKey == null && nullToAbsent ? const Value.absent() : Value(groupPublicKey), + lastMessage: lastMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastMessage), createdAt: Value(createdAt), ); } @@ -4116,6 +4138,7 @@ class GroupMember extends DataClass implements Insertable { memberState: $GroupMembersTable.$convertermemberStaten .fromJson(serializer.fromJson(json['memberState'])), groupPublicKey: serializer.fromJson(json['groupPublicKey']), + lastMessage: serializer.fromJson(json['lastMessage']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -4128,6 +4151,7 @@ class GroupMember extends DataClass implements Insertable { 'memberState': serializer.toJson( $GroupMembersTable.$convertermemberStaten.toJson(memberState)), 'groupPublicKey': serializer.toJson(groupPublicKey), + 'lastMessage': serializer.toJson(lastMessage), 'createdAt': serializer.toJson(createdAt), }; } @@ -4137,6 +4161,7 @@ class GroupMember extends DataClass implements Insertable { int? contactId, Value memberState = const Value.absent(), Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), DateTime? createdAt}) => GroupMember( groupId: groupId ?? this.groupId, @@ -4144,6 +4169,7 @@ class GroupMember extends DataClass implements Insertable { memberState: memberState.present ? memberState.value : this.memberState, groupPublicKey: groupPublicKey.present ? groupPublicKey.value : this.groupPublicKey, + lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, createdAt: createdAt ?? this.createdAt, ); GroupMember copyWithCompanion(GroupMembersCompanion data) { @@ -4155,6 +4181,8 @@ class GroupMember extends DataClass implements Insertable { groupPublicKey: data.groupPublicKey.present ? data.groupPublicKey.value : this.groupPublicKey, + lastMessage: + data.lastMessage.present ? data.lastMessage.value : this.lastMessage, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -4166,6 +4194,7 @@ class GroupMember extends DataClass implements Insertable { ..write('contactId: $contactId, ') ..write('memberState: $memberState, ') ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') ..write('createdAt: $createdAt') ..write(')')) .toString(); @@ -4173,7 +4202,7 @@ class GroupMember extends DataClass implements Insertable { @override int get hashCode => Object.hash(groupId, contactId, memberState, - $driftBlobEquality.hash(groupPublicKey), createdAt); + $driftBlobEquality.hash(groupPublicKey), lastMessage, createdAt); @override bool operator ==(Object other) => identical(this, other) || @@ -4183,6 +4212,7 @@ class GroupMember extends DataClass implements Insertable { other.memberState == this.memberState && $driftBlobEquality.equals( other.groupPublicKey, this.groupPublicKey) && + other.lastMessage == this.lastMessage && other.createdAt == this.createdAt); } @@ -4191,6 +4221,7 @@ class GroupMembersCompanion extends UpdateCompanion { final Value contactId; final Value memberState; final Value groupPublicKey; + final Value lastMessage; final Value createdAt; final Value rowid; const GroupMembersCompanion({ @@ -4198,6 +4229,7 @@ class GroupMembersCompanion extends UpdateCompanion { this.contactId = const Value.absent(), this.memberState = const Value.absent(), this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }); @@ -4206,6 +4238,7 @@ class GroupMembersCompanion extends UpdateCompanion { required int contactId, this.memberState = const Value.absent(), this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), }) : groupId = Value(groupId), @@ -4215,6 +4248,7 @@ class GroupMembersCompanion extends UpdateCompanion { Expression? contactId, Expression? memberState, Expression? groupPublicKey, + Expression? lastMessage, Expression? createdAt, Expression? rowid, }) { @@ -4223,6 +4257,7 @@ class GroupMembersCompanion extends UpdateCompanion { if (contactId != null) 'contact_id': contactId, if (memberState != null) 'member_state': memberState, if (groupPublicKey != null) 'group_public_key': groupPublicKey, + if (lastMessage != null) 'last_message': lastMessage, if (createdAt != null) 'created_at': createdAt, if (rowid != null) 'rowid': rowid, }); @@ -4233,6 +4268,7 @@ class GroupMembersCompanion extends UpdateCompanion { Value? contactId, Value? memberState, Value? groupPublicKey, + Value? lastMessage, Value? createdAt, Value? rowid}) { return GroupMembersCompanion( @@ -4240,6 +4276,7 @@ class GroupMembersCompanion extends UpdateCompanion { contactId: contactId ?? this.contactId, memberState: memberState ?? this.memberState, groupPublicKey: groupPublicKey ?? this.groupPublicKey, + lastMessage: lastMessage ?? this.lastMessage, createdAt: createdAt ?? this.createdAt, rowid: rowid ?? this.rowid, ); @@ -4261,6 +4298,9 @@ class GroupMembersCompanion extends UpdateCompanion { if (groupPublicKey.present) { map['group_public_key'] = Variable(groupPublicKey.value); } + if (lastMessage.present) { + map['last_message'] = Variable(lastMessage.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -4277,6 +4317,7 @@ class GroupMembersCompanion extends UpdateCompanion { ..write('contactId: $contactId, ') ..write('memberState: $memberState, ') ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') ..write('createdAt: $createdAt, ') ..write('rowid: $rowid') ..write(')')) @@ -10870,6 +10911,7 @@ typedef $$GroupMembersTableCreateCompanionBuilder = GroupMembersCompanion required int contactId, Value memberState, Value groupPublicKey, + Value lastMessage, Value createdAt, Value rowid, }); @@ -10879,6 +10921,7 @@ typedef $$GroupMembersTableUpdateCompanionBuilder = GroupMembersCompanion Value contactId, Value memberState, Value groupPublicKey, + Value lastMessage, Value createdAt, Value rowid, }); @@ -10935,6 +10978,9 @@ class $$GroupMembersTableFilterComposer column: $table.groupPublicKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); @@ -10995,6 +11041,9 @@ class $$GroupMembersTableOrderingComposer column: $table.groupPublicKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); @@ -11055,6 +11104,9 @@ class $$GroupMembersTableAnnotationComposer GeneratedColumn get groupPublicKey => $composableBuilder( column: $table.groupPublicKey, builder: (column) => column); + GeneratedColumn get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -11126,6 +11178,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< Value contactId = const Value.absent(), Value memberState = const Value.absent(), Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -11134,6 +11187,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< contactId: contactId, memberState: memberState, groupPublicKey: groupPublicKey, + lastMessage: lastMessage, createdAt: createdAt, rowid: rowid, ), @@ -11142,6 +11196,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< required int contactId, Value memberState = const Value.absent(), Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), Value createdAt = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -11150,6 +11205,7 @@ class $$GroupMembersTableTableManager extends RootTableManager< contactId: contactId, memberState: memberState, groupPublicKey: groupPublicKey, + lastMessage: lastMessage, createdAt: createdAt, rowid: rowid, ), diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 8c167fb..e0bcbc9 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -774,10 +774,11 @@ "@twonlySafeRecoverDesc": {}, "twonlySafeRecoverBtn": "Backup wiederherstellen", "@twonlySafeRecoverBtn": {}, - "notificationText": "hat eine Nachricht gesendet.", - "notificationTwonly": "hat ein twonly gesendet.", - "notificationVideo": "hat ein Video gesendet.", - "notificationImage": "hat ein Bild gesendet.", + "notificationFillerIn": "in", + "notificationText": "hat eine Nachricht{inGroup} gesendet.", + "notificationTwonly": "hat ein twonly{inGroup} gesendet.", + "notificationVideo": "hat ein Video{inGroup} gesendet.", + "notificationImage": "hat ein Bild{inGroup} gesendet.", "notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.", "notificationContactRequest": "möchte sich mit dir vernetzen.", "notificationAcceptRequest": "ist jetzt mit dir vernetzt.", @@ -787,7 +788,7 @@ "notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.", "notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.", "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", - "notificationResponse": "hat dir geantwortet.", + "notificationResponse": "hat dir{inGroup} geantwortet.", "notificationTitleUnknownUser": "Jemand", "notificationCategoryMessageTitle": "Nachrichten", "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern." diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index f412f7e..cfb34bc 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -553,10 +553,11 @@ "makerLeftGroup": "{maker} has left the group.", "groupActionYou": "you", "groupActionYour": "your", - "notificationText": "sent a message.", - "notificationTwonly": "sent a twonly.", - "notificationVideo": "sent a video.", - "notificationImage": "sent a image.", + "notificationFillerIn": "in", + "notificationText": "sent a message{inGroup}.", + "notificationTwonly": "sent a twonly{inGroup}.", + "notificationVideo": "sent a video{inGroup}.", + "notificationImage": "sent a image{inGroup}.", "notificationAddedToGroup": "has added you to \"{groupname}\"", "notificationContactRequest": "wants to connect with you.", "notificationAcceptRequest": "is now connected with you.", @@ -566,7 +567,7 @@ "notificationReactionToVideo": "has reacted with {reaction} to your video.", "notificationReactionToText": "has reacted with {reaction} to your message.", "notificationReactionToImage": "has reacted with {reaction} to your image.", - "notificationResponse": "has responded.", + "notificationResponse": "has responded{inGroup}.", "notificationTitleUnknownUser": "Someone", "notificationCategoryMessageTitle": "Messages", "notificationCategoryMessageDesc": "Messages from other users." diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 1b37278..f1bf190 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2420,29 +2420,35 @@ abstract class AppLocalizations { /// **'your'** String get groupActionYour; + /// No description provided for @notificationFillerIn. + /// + /// In en, this message translates to: + /// **'in'** + String get notificationFillerIn; + /// No description provided for @notificationText. /// /// In en, this message translates to: - /// **'sent a message.'** - String get notificationText; + /// **'sent a message{inGroup}.'** + String notificationText(Object inGroup); /// No description provided for @notificationTwonly. /// /// In en, this message translates to: - /// **'sent a twonly.'** - String get notificationTwonly; + /// **'sent a twonly{inGroup}.'** + String notificationTwonly(Object inGroup); /// No description provided for @notificationVideo. /// /// In en, this message translates to: - /// **'sent a video.'** - String get notificationVideo; + /// **'sent a video{inGroup}.'** + String notificationVideo(Object inGroup); /// No description provided for @notificationImage. /// /// In en, this message translates to: - /// **'sent a image.'** - String get notificationImage; + /// **'sent a image{inGroup}.'** + String notificationImage(Object inGroup); /// No description provided for @notificationAddedToGroup. /// @@ -2501,8 +2507,8 @@ abstract class AppLocalizations { /// No description provided for @notificationResponse. /// /// In en, this message translates to: - /// **'has responded.'** - String get notificationResponse; + /// **'has responded{inGroup}.'** + String notificationResponse(Object inGroup); /// No description provided for @notificationTitleUnknownUser. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0315d9a..b346930 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1310,16 +1310,27 @@ class AppLocalizationsDe extends AppLocalizations { String get groupActionYour => 'deine'; @override - String get notificationText => 'hat eine Nachricht gesendet.'; + String get notificationFillerIn => 'in'; @override - String get notificationTwonly => 'hat ein twonly gesendet.'; + String notificationText(Object inGroup) { + return 'hat eine Nachricht$inGroup gesendet.'; + } @override - String get notificationVideo => 'hat ein Video gesendet.'; + String notificationTwonly(Object inGroup) { + return 'hat ein twonly$inGroup gesendet.'; + } @override - String get notificationImage => 'hat ein Bild gesendet.'; + String notificationVideo(Object inGroup) { + return 'hat ein Video$inGroup gesendet.'; + } + + @override + String notificationImage(Object inGroup) { + return 'hat ein Bild$inGroup gesendet.'; + } @override String notificationAddedToGroup(Object groupname) { @@ -1357,7 +1368,9 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get notificationResponse => 'hat dir geantwortet.'; + String notificationResponse(Object inGroup) { + return 'hat dir$inGroup geantwortet.'; + } @override String get notificationTitleUnknownUser => 'Jemand'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 032ca02..9dff907 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1303,16 +1303,27 @@ class AppLocalizationsEn extends AppLocalizations { String get groupActionYour => 'your'; @override - String get notificationText => 'sent a message.'; + String get notificationFillerIn => 'in'; @override - String get notificationTwonly => 'sent a twonly.'; + String notificationText(Object inGroup) { + return 'sent a message$inGroup.'; + } @override - String get notificationVideo => 'sent a video.'; + String notificationTwonly(Object inGroup) { + return 'sent a twonly$inGroup.'; + } @override - String get notificationImage => 'sent a image.'; + String notificationVideo(Object inGroup) { + return 'sent a video$inGroup.'; + } + + @override + String notificationImage(Object inGroup) { + return 'sent a image$inGroup.'; + } @override String notificationAddedToGroup(Object groupname) { @@ -1350,7 +1361,9 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get notificationResponse => 'has responded.'; + String notificationResponse(Object inGroup) { + return 'has responded$inGroup.'; + } @override String get notificationTitleUnknownUser => 'Someone'; diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index f998723..935ea22 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -237,11 +237,18 @@ AppLocalizations getLocalizations() { String getPushNotificationText(PushNotification pushNotification) { final lang = getLocalizations(); + var inGroup = ''; + + if (pushNotification.hasAdditionalContent()) { + inGroup = + ' ${lang.notificationFillerIn} ${pushNotification.additionalContent}'; + } + final pushNotificationText = { - PushKind.text.name: lang.notificationText, - PushKind.twonly.name: lang.notificationTwonly, - PushKind.video.name: lang.notificationVideo, - PushKind.image.name: lang.notificationImage, + PushKind.text.name: lang.notificationText(inGroup), + PushKind.twonly.name: lang.notificationTwonly(inGroup), + PushKind.video.name: lang.notificationVideo(inGroup), + PushKind.image.name: lang.notificationImage(inGroup), PushKind.contactRequest.name: lang.notificationContactRequest, PushKind.acceptRequest.name: lang.notificationAcceptRequest, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, @@ -253,7 +260,7 @@ String getPushNotificationText(PushNotification pushNotification) { lang.notificationReactionToText(pushNotification.additionalContent), PushKind.reactionToImage.name: lang.notificationReactionToImage(pushNotification.additionalContent), - PushKind.response.name: lang.notificationResponse, + PushKind.response.name: lang.notificationResponse(inGroup), PushKind.addedToGroup.name: lang.notificationAddedToGroup(pushNotification.additionalContent), }; diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 312c3ad..033f2c3 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -234,6 +234,10 @@ Future getPushNotificationFromEncryptedContent( if (content.textMessage.hasQuoteMessageId()) { kind = PushKind.response; } + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + if (group != null && !group.isDirectChat) { + additionalContent = group.groupName; + } } if (content.hasMedia()) { switch (content.media.type) { @@ -248,6 +252,10 @@ Future getPushNotificationFromEncryptedContent( if (content.media.requiresAuthentication) { kind = PushKind.twonly; } + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + if (group != null && !group.isDirectChat) { + additionalContent = group.groupName; + } } if (content.hasContactRequest()) { diff --git a/lib/src/services/notifications/setup.notifications.dart b/lib/src/services/notifications/setup.notifications.dart index 6d9b3b0..1181354 100644 --- a/lib/src/services/notifications/setup.notifications.dart +++ b/lib/src/services/notifications/setup.notifications.dart @@ -62,7 +62,7 @@ Future createPushAvatars() async { final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); for (final contact in contacts) { - if (contact.avatarSvgCompressed == null) return; + if (contact.avatarSvgCompressed == null) continue; final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 4517571..5a5dd35 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -67,6 +67,16 @@ Uint8List getRandomUint8List(int length) { return randomBytes; } +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; +Random _rnd = Random(); + +String getRandomString(int length) => String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)), + ), + ); + String errorCodeToText(BuildContext context, ErrorCode code) { // ignore: exhaustive_cases switch (code) { diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 119bdd1..1a9f3a5 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -52,7 +52,7 @@ Mutex updateProtection = Mutex(); Future updateUserdata( UserData Function(UserData userData) updateUser, ) async { - return updateProtection.protect(() async { + final userData = await updateProtection.protect(() async { final user = await getUser(); if (user == null) return null; final updated = updateUser(user); @@ -61,6 +61,14 @@ Future updateUserdata( gUser = updated; return updated; }); + try { + for (final callBack in globalUserDataChangedCallBack.values) { + callBack(); + } + } catch (e) { + Log.error(e); + } + return userData; } Future deleteLocalUserData() async { diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index c24ffec..26db258 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -279,7 +279,7 @@ class ContactsListView extends StatelessWidget { final contact = contacts[index]; return ListTile( title: Text(substringBy(contact.username, 25)), - leading: AvatarIcon(contact: contact), + leading: AvatarIcon(contactId: contact.userId), trailing: Row( mainAxisSize: MainAxisSize.min, children: contact.requested diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 575300d..1ba8dec 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -123,7 +123,7 @@ class _ChatListViewState extends State { setState(() {}); // gUser has updated }, child: AvatarIcon( - userData: gUser, + myAvatar: true, fontSize: 14, color: context.color.onSurface.withAlpha(20), ), diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index ff8857a..64e5a44 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -386,7 +386,7 @@ class _ChatMessagesViewState extends State { children: messages[i].lastOpenedPosition!.map((w) { return AvatarIcon( key: GlobalKey(), - contact: w, + contactId: w.userId, fontSize: 12, ); }).toList(), diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 5292d02..a01b66b 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -116,8 +116,8 @@ class _AllReactionsViewState extends State { child: Row( children: [ AvatarIcon( - contact: entry.$2, - userData: (entry.$2 == null) ? gUser : null, + contactId: entry.$2?.userId, + myAvatar: entry.$2 == null, fontSize: 15, ), const SizedBox(width: 6), diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart index c4d81b6..bcf15c8 100644 --- a/lib/src/views/chats/message_info.view.dart +++ b/lib/src/views/chats/message_info.view.dart @@ -127,7 +127,7 @@ class _MessageInfoViewState extends State { child: Row( children: [ AvatarIcon( - contact: groupMember.$2, + contactId: groupMember.$2.userId, fontSize: 15, ), const SizedBox(width: 6), diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 046e492..3f0f62b 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -175,7 +175,7 @@ class UserList extends StatelessWidget { ], ), leading: AvatarIcon( - contact: user, + contactId: user.userId, fontSize: 13, ), onTap: () async { diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index 2bab8f5..341ba41 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -1,24 +1,22 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/misc.dart'; class AvatarIcon extends StatefulWidget { const AvatarIcon({ super.key, this.group, - this.contact, this.contactId, - this.userData, + this.myAvatar = false, this.fontSize = 20, this.color, }); final Group? group; - final Contact? contact; final int? contactId; - final UserData? userData; + final bool myAvatar; final double? fontSize; final Color? color; @@ -27,7 +25,12 @@ class AvatarIcon extends StatefulWidget { } class _AvatarIconState extends State { - final List _avatarSVGs = []; + List _avatarSVGs = []; + String? _globalUserDataCallBackId; + + StreamSubscription>? groupStream; + StreamSubscription>? contactsStream; + StreamSubscription? contactStream; @override void initState() { @@ -35,32 +38,53 @@ class _AvatarIconState extends State { super.initState(); } + @override + void dispose() { + groupStream?.cancel(); + contactStream?.cancel(); + contactsStream?.cancel(); + if (_globalUserDataCallBackId != null) { + globalUserDataChangedCallBack.remove(_globalUserDataCallBackId); + } + super.dispose(); + } + Future initAsync() async { if (widget.group != null) { - final contacts = - await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); - if (contacts.length == 1) { - if (contacts.first.avatarSvgCompressed != null) { - _avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); - } - } else { - for (final contact in contacts) { - if (contact.avatarSvgCompressed != null) { - _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); + groupStream = twonlyDB.groupsDao + .watchGroupContact(widget.group!.groupId) + .listen((contacts) { + _avatarSVGs = []; + if (contacts.length == 1) { + if (contacts.first.avatarSvgCompressed != null) { + _avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); + } + } else { + for (final contact in contacts) { + if (contact.avatarSvgCompressed != null) { + _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); + } } } - } - // avatarSvg = group!.avatarSvg; - } else if (widget.userData?.avatarSvg != null) { - _avatarSVGs.add(widget.userData!.avatarSvg!); - } else if (widget.contact?.avatarSvgCompressed != null) { - _avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); + setState(() {}); + }); + } else if (widget.myAvatar) { + _globalUserDataCallBackId = 'avatar_${getRandomString(10)}'; + globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () { + setState(() { + _avatarSVGs = [gUser.avatarSvg!]; + }); + }; + _avatarSVGs.add(gUser.avatarSvg!); } else if (widget.contactId != null) { - final contact = - await twonlyDB.contactsDao.getContactById(widget.contactId!); - if (contact != null && contact.avatarSvgCompressed != null) { - _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); - } + contactStream = twonlyDB.contactsDao + .watchContact(widget.contactId!) + .listen((contact) { + if (contact != null && contact.avatarSvgCompressed != null) { + _avatarSVGs = [getAvatarSvg(contact.avatarSvgCompressed!)]; + setState(() {}); + } + }); } if (mounted) setState(() {}); } diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index b615549..b77ff44 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -115,7 +115,7 @@ class _ContactViewState extends State { children: [ Padding( padding: const EdgeInsets.all(10), - child: AvatarIcon(contact: contact, fontSize: 30), + child: AvatarIcon(contactId: contact.userId, fontSize: 30), ), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 3631f46..72c4461 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -153,9 +153,8 @@ class _GroupViewState extends State { ), BetterListTile( padding: const EdgeInsets.only(left: 13), - leading: AvatarIcon( - key: GlobalKey(), - userData: gUser, + leading: const AvatarIcon( + myAvatar: true, fontSize: 16, ), text: context.lang.you, @@ -177,8 +176,7 @@ class _GroupViewState extends State { child: BetterListTile( padding: const EdgeInsets.only(left: 13), leading: AvatarIcon( - key: GlobalKey(), - contact: member.$1, + contactId: member.$1.userId, fontSize: 16, ), text: getContactDisplayName(member.$1, maxLength: 25), diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart index e6cd4b2..7f280bb 100644 --- a/lib/src/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -114,7 +114,7 @@ class _GroupCreateSelectGroupNameViewState ], ), leading: AvatarIcon( - contact: user, + contactId: user.userId, fontSize: 13, ), ), diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index f4ffd37..a497622 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -204,7 +204,7 @@ class _StartNewChatView extends State { ? Text(context.lang.alreadyInGroup) : null, leading: AvatarIcon( - contact: user, + contactId: user.userId, fontSize: 13, ), trailing: Checkbox( @@ -256,7 +256,7 @@ class _Chip extends StatelessWidget { child: Chip( key: GlobalKey(), avatar: AvatarIcon( - contact: contact, + contactId: contact.userId, fontSize: 10, ), label: Row( diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 92066d2..d80f581 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -112,7 +112,7 @@ class UserList extends StatelessWidget { Text(getContactDisplayName(user)), ], ), - leading: AvatarIcon(contact: user, fontSize: 15), + leading: AvatarIcon(contactId: user.userId, fontSize: 15), trailing: Checkbox( value: user.blocked, onChanged: (bool? value) async { diff --git a/lib/src/views/settings/settings_main.view.dart b/lib/src/views/settings/settings_main.view.dart index 6c818b5..017fd36 100644 --- a/lib/src/views/settings/settings_main.view.dart +++ b/lib/src/views/settings/settings_main.view.dart @@ -55,8 +55,8 @@ class _SettingsMainViewState extends State { color: context.color.surface.withAlpha(0), child: Row( children: [ - AvatarIcon( - userData: gUser, + const AvatarIcon( + myAvatar: true, fontSize: 30, ), Container(width: 20, color: Colors.transparent), From 678a22dd1111349192c3bb6659a8aff13350afae Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 11:40:50 +0100 Subject: [PATCH 55/76] delete messages only when all have opened it --- lib/src/database/daos/messages.dao.dart | 16 ++--- lib/src/database/tables/messages.table.dart | 1 + lib/src/database/twonly.db.g.dart | 58 +++++++++++++++++++ lib/src/services/api/messages.dart | 5 +- lib/src/views/chats/chat_messages.view.dart | 1 - .../chat_text_entry.dart | 17 +++--- .../components/avatar_icon.component.dart | 1 - .../group_create_select_members.view.dart | 1 - 8 files changed, 82 insertions(+), 18 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 5314944..4e45db6 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -122,7 +122,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { (m.mediaStored.equals(true) & m.isDeletedFromSender.equals(true) | m.mediaStored.equals(false)) & - (m.openedAt.isSmallerThanValue(deletionTime) | + (m.openedByAll.isSmallerThanValue(deletionTime) | (m.isDeletedFromSender.equals(true) & m.createdAt.isSmallerThanValue(deletionTime))), )) @@ -271,12 +271,17 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ), ); // Directly show as message opened as soon as one person has opened it - // if (await haveAllMembers(messageId, MessageActionType.openedAt)) { + final openedByAll = + await haveAllMembers(messageId, MessageActionType.openedAt) + ? DateTime.now() + : null; await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), + MessagesCompanion( + openedAt: Value(DateTime.now()), + openedByAll: Value(openedByAll), + ), ); - // } } Future handleMessageAckByServer( @@ -292,13 +297,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { actionAt: Value(timestamp), ), ); - // if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) { - /// always update the state, so it will be shown as soon as one member gets the message await twonlyDB.messagesDao.updateMessageId( messageId, MessagesCompanion(ackByServer: Value(DateTime.now())), ); - // } } Future haveAllMembers( diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 5a9aa3f..7d0f20c 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -32,6 +32,7 @@ class Messages extends Table { boolean().withDefault(const Constant(false))(); DateTimeColumn get openedAt => dateTime().nullable()(); + DateTimeColumn get openedByAll => dateTime().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get modifiedAt => dateTime().nullable()(); DateTimeColumn get ackByUser => dateTime().nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 9e58333..2e07136 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2699,6 +2699,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { late final GeneratedColumn openedAt = GeneratedColumn( 'opened_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _openedByAllMeta = + const VerificationMeta('openedByAll'); + @override + late final GeneratedColumn openedByAll = GeneratedColumn( + 'opened_by_all', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -2738,6 +2744,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { quotesMessageId, isDeletedFromSender, openedAt, + openedByAll, createdAt, modifiedAt, ackByUser, @@ -2805,6 +2812,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_openedAtMeta, openedAt.isAcceptableOrUnknown(data['opened_at']!, _openedAtMeta)); } + if (data.containsKey('opened_by_all')) { + context.handle( + _openedByAllMeta, + openedByAll.isAcceptableOrUnknown( + data['opened_by_all']!, _openedByAllMeta)); + } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); @@ -2858,6 +2871,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, openedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), + openedByAll: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_by_all']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, modifiedAt: attachedDatabase.typeMapping @@ -2890,6 +2905,7 @@ class Message extends DataClass implements Insertable { final String? quotesMessageId; final bool isDeletedFromSender; final DateTime? openedAt; + final DateTime? openedByAll; final DateTime createdAt; final DateTime? modifiedAt; final DateTime? ackByUser; @@ -2906,6 +2922,7 @@ class Message extends DataClass implements Insertable { this.quotesMessageId, required this.isDeletedFromSender, this.openedAt, + this.openedByAll, required this.createdAt, this.modifiedAt, this.ackByUser, @@ -2938,6 +2955,9 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || openedAt != null) { map['opened_at'] = Variable(openedAt); } + if (!nullToAbsent || openedByAll != null) { + map['opened_by_all'] = Variable(openedByAll); + } map['created_at'] = Variable(createdAt); if (!nullToAbsent || modifiedAt != null) { map['modified_at'] = Variable(modifiedAt); @@ -2976,6 +2996,9 @@ class Message extends DataClass implements Insertable { openedAt: openedAt == null && nullToAbsent ? const Value.absent() : Value(openedAt), + openedByAll: openedByAll == null && nullToAbsent + ? const Value.absent() + : Value(openedByAll), createdAt: Value(createdAt), modifiedAt: modifiedAt == null && nullToAbsent ? const Value.absent() @@ -3006,6 +3029,7 @@ class Message extends DataClass implements Insertable { isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), openedAt: serializer.fromJson(json['openedAt']), + openedByAll: serializer.fromJson(json['openedByAll']), createdAt: serializer.fromJson(json['createdAt']), modifiedAt: serializer.fromJson(json['modifiedAt']), ackByUser: serializer.fromJson(json['ackByUser']), @@ -3028,6 +3052,7 @@ class Message extends DataClass implements Insertable { 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), 'openedAt': serializer.toJson(openedAt), + 'openedByAll': serializer.toJson(openedByAll), 'createdAt': serializer.toJson(createdAt), 'modifiedAt': serializer.toJson(modifiedAt), 'ackByUser': serializer.toJson(ackByUser), @@ -3047,6 +3072,7 @@ class Message extends DataClass implements Insertable { Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), DateTime? createdAt, Value modifiedAt = const Value.absent(), Value ackByUser = const Value.absent(), @@ -3066,6 +3092,7 @@ class Message extends DataClass implements Insertable { : this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, openedAt: openedAt.present ? openedAt.value : this.openedAt, + openedByAll: openedByAll.present ? openedByAll.value : this.openedByAll, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, @@ -3091,6 +3118,8 @@ class Message extends DataClass implements Insertable { ? data.isDeletedFromSender.value : this.isDeletedFromSender, openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + openedByAll: + data.openedByAll.present ? data.openedByAll.value : this.openedByAll, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, modifiedAt: data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, @@ -3114,6 +3143,7 @@ class Message extends DataClass implements Insertable { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') ..write('ackByUser: $ackByUser, ') @@ -3135,6 +3165,7 @@ class Message extends DataClass implements Insertable { quotesMessageId, isDeletedFromSender, openedAt, + openedByAll, createdAt, modifiedAt, ackByUser, @@ -3154,6 +3185,7 @@ class Message extends DataClass implements Insertable { other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && other.openedAt == this.openedAt && + other.openedByAll == this.openedByAll && other.createdAt == this.createdAt && other.modifiedAt == this.modifiedAt && other.ackByUser == this.ackByUser && @@ -3172,6 +3204,7 @@ class MessagesCompanion extends UpdateCompanion { final Value quotesMessageId; final Value isDeletedFromSender; final Value openedAt; + final Value openedByAll; final Value createdAt; final Value modifiedAt; final Value ackByUser; @@ -3189,6 +3222,7 @@ class MessagesCompanion extends UpdateCompanion { this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), this.ackByUser = const Value.absent(), @@ -3207,6 +3241,7 @@ class MessagesCompanion extends UpdateCompanion { this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), this.ackByUser = const Value.absent(), @@ -3227,6 +3262,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? quotesMessageId, Expression? isDeletedFromSender, Expression? openedAt, + Expression? openedByAll, Expression? createdAt, Expression? modifiedAt, Expression? ackByUser, @@ -3246,6 +3282,7 @@ class MessagesCompanion extends UpdateCompanion { if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, if (openedAt != null) 'opened_at': openedAt, + if (openedByAll != null) 'opened_by_all': openedByAll, if (createdAt != null) 'created_at': createdAt, if (modifiedAt != null) 'modified_at': modifiedAt, if (ackByUser != null) 'ack_by_user': ackByUser, @@ -3266,6 +3303,7 @@ class MessagesCompanion extends UpdateCompanion { Value? quotesMessageId, Value? isDeletedFromSender, Value? openedAt, + Value? openedByAll, Value? createdAt, Value? modifiedAt, Value? ackByUser, @@ -3283,6 +3321,7 @@ class MessagesCompanion extends UpdateCompanion { quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, openedAt: openedAt ?? this.openedAt, + openedByAll: openedByAll ?? this.openedByAll, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt ?? this.modifiedAt, ackByUser: ackByUser ?? this.ackByUser, @@ -3328,6 +3367,9 @@ class MessagesCompanion extends UpdateCompanion { if (openedAt.present) { map['opened_at'] = Variable(openedAt.value); } + if (openedByAll.present) { + map['opened_by_all'] = Variable(openedByAll.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -3360,6 +3402,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') ..write('ackByUser: $ackByUser, ') @@ -9419,6 +9462,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value quotesMessageId, Value isDeletedFromSender, Value openedAt, + Value openedByAll, Value createdAt, Value modifiedAt, Value ackByUser, @@ -9437,6 +9481,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value quotesMessageId, Value isDeletedFromSender, Value openedAt, + Value openedByAll, Value createdAt, Value modifiedAt, Value ackByUser, @@ -9596,6 +9641,9 @@ class $$MessagesTableFilterComposer ColumnFilters get openedAt => $composableBuilder( column: $table.openedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); @@ -9789,6 +9837,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get openedAt => $composableBuilder( column: $table.openedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); @@ -9895,6 +9946,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get openedAt => $composableBuilder(column: $table.openedAt, builder: (column) => column); + GeneratedColumn get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -10093,6 +10147,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), Value createdAt = const Value.absent(), Value modifiedAt = const Value.absent(), Value ackByUser = const Value.absent(), @@ -10111,6 +10166,7 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, openedAt: openedAt, + openedByAll: openedByAll, createdAt: createdAt, modifiedAt: modifiedAt, ackByUser: ackByUser, @@ -10129,6 +10185,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), Value createdAt = const Value.absent(), Value modifiedAt = const Value.absent(), Value ackByUser = const Value.absent(), @@ -10147,6 +10204,7 @@ class $$MessagesTableTableManager extends RootTableManager< quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, openedAt: openedAt, + openedByAll: openedByAll, createdAt: createdAt, modifiedAt: modifiedAt, ackByUser: ackByUser, diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 4b72807..8a7497e 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -274,7 +274,10 @@ Future notifyContactAboutOpeningMessage( for (final messageId in messageOtherIds) { await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(openedAt: Value(actionAt)), + MessagesCompanion( + openedAt: Value(actionAt), + openedByAll: Value(actionAt), + ), ); } 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 64e5a44..1e47905 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -385,7 +385,6 @@ class _ChatMessagesViewState extends State { alignment: WrapAlignment.center, children: messages[i].lastOpenedPosition!.map((w) { return AvatarIcon( - key: GlobalKey(), contactId: w.userId, fontSize: 12, ); diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index c22df9f..7d7d3fb 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -47,13 +47,16 @@ class ChatTextEntry extends StatelessWidget { var displayTime = !combineTextMessageWithNext(message, nextMessage); var displayUserName = ''; - if (message.senderId != null && - prevMessage != null && - userIdToContact != null) { - if (!combineTextMessageWithNext(prevMessage!, message)) { - if (userIdToContact![message.senderId] != null) { - displayUserName = - getContactDisplayName(userIdToContact![message.senderId]!); + if (message.senderId != null && userIdToContact != null) { + if (prevMessage == null) { + displayUserName = + getContactDisplayName(userIdToContact![message.senderId]!); + } else { + if (!combineTextMessageWithNext(prevMessage!, message)) { + if (userIdToContact![message.senderId] != null) { + displayUserName = + getContactDisplayName(userIdToContact![message.senderId]!); + } } } } diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index 341ba41..f82bbbc 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -150,7 +150,6 @@ class _AvatarIconState extends State { } return Container( - key: GlobalKey(), constraints: BoxConstraints( minHeight: 2 * (widget.fontSize ?? 20), minWidth: 2 * (widget.fontSize ?? 20), diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index a497622..892c202 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -254,7 +254,6 @@ class _Chip extends StatelessWidget { return GestureDetector( onTap: () => onTap(contact.userId), child: Chip( - key: GlobalKey(), avatar: AvatarIcon( contactId: contact.userId, fontSize: 10, From 5235a01626fb1d9226ef79b7e3cdfd9195fcc0b7 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 16:54:49 +0100 Subject: [PATCH 56/76] allow deletion of chats and groups --- lib/src/database/daos/groups.dao.dart | 14 +- lib/src/database/daos/messages.dao.dart | 5 + lib/src/database/tables/groups.table.dart | 2 + lib/src/database/twonly.db.g.dart | 61 ++++ lib/src/localization/app_de.arb | 4 + lib/src/localization/app_en.arb | 3 + .../generated/app_localizations.dart | 18 ++ .../generated/app_localizations_de.dart | 9 + .../generated/app_localizations_en.dart | 9 + .../api/mediafiles/upload.service.dart | 1 + lib/src/views/chats/add_new_user.view.dart | 2 +- lib/src/views/chats/chat_messages.view.dart | 6 +- .../chat_text_entry.dart | 10 +- lib/src/views/chats/start_new_chat.view.dart | 303 +++++++++++------- .../group_context_menu.component.dart | 17 +- .../views/settings/backup/backup.view.dart | 2 +- 16 files changed, 339 insertions(+), 127 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index ca20c7e..1d930a0 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -164,7 +164,11 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Stream> watchGroupsForShareImage() { - return (select(groups)..where((g) => g.leftGroup.equals(false))).watch(); + return (select(groups) + ..where( + (g) => g.leftGroup.equals(false) & g.deletedContent.equals(false), + )) + .watch(); } Stream watchGroup(String groupId) { @@ -174,10 +178,18 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { Stream> watchGroupsForChatList() { return (select(groups) + ..where((t) => t.deletedContent.equals(false)) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) .watch(); } + Stream> watchGroupsForStartNewChat() { + return (select(groups) + ..where((t) => t.isDirectChat.equals(false)) + ..orderBy([(t) => OrderingTerm.asc(t.groupName)])) + .watch(); + } + Future getGroup(String groupId) { return (select(groups)..where((t) => t.groupId.equals(groupId))) .getSingleOrNull(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 4e45db6..7c7fa35 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -380,6 +380,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { GroupsCompanion( lastMessageExchange: Value(DateTime.now()), archived: const Value(false), + deletedContent: const Value(false), ), ); @@ -468,6 +469,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } + Future deleteMessagesByGroupId(String groupId) { + return (delete(messages)..where((t) => t.groupId.equals(groupId))).go(); + } + // Future deleteAllMessagesByContactId(int contactId) { // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); // } diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index 3554306..e33cafe 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -14,6 +14,8 @@ class Groups extends Table { BoolColumn get joinedGroup => boolean().withDefault(const Constant(false))(); BoolColumn get leftGroup => boolean().withDefault(const Constant(false))(); + BoolColumn get deletedContent => + boolean().withDefault(const Constant(false))(); IntColumn get stateVersionId => integer().withDefault(const Constant(0))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 2e07136..101d2ba 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -724,6 +724,16 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _deletedContentMeta = + const VerificationMeta('deletedContent'); + @override + late final GeneratedColumn deletedContent = GeneratedColumn( + 'deleted_content', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_content" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _stateVersionIdMeta = const VerificationMeta('stateVersionId'); @override @@ -848,6 +858,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { archived, joinedGroup, leftGroup, + deletedContent, stateVersionId, stateEncryptionKey, myGroupPrivateKey, @@ -911,6 +922,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { context.handle(_leftGroupMeta, leftGroup.isAcceptableOrUnknown(data['left_group']!, _leftGroupMeta)); } + if (data.containsKey('deleted_content')) { + context.handle( + _deletedContentMeta, + deletedContent.isAcceptableOrUnknown( + data['deleted_content']!, _deletedContentMeta)); + } if (data.containsKey('state_version_id')) { context.handle( _stateVersionIdMeta, @@ -1029,6 +1046,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { .read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!, leftGroup: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!, + deletedContent: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_content'])!, stateVersionId: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!, stateEncryptionKey: attachedDatabase.typeMapping.read( @@ -1083,6 +1102,7 @@ class Group extends DataClass implements Insertable { final bool archived; final bool joinedGroup; final bool leftGroup; + final bool deletedContent; final int stateVersionId; final Uint8List? stateEncryptionKey; final Uint8List? myGroupPrivateKey; @@ -1107,6 +1127,7 @@ class Group extends DataClass implements Insertable { required this.archived, required this.joinedGroup, required this.leftGroup, + required this.deletedContent, required this.stateVersionId, this.stateEncryptionKey, this.myGroupPrivateKey, @@ -1133,6 +1154,7 @@ class Group extends DataClass implements Insertable { map['archived'] = Variable(archived); map['joined_group'] = Variable(joinedGroup); map['left_group'] = Variable(leftGroup); + map['deleted_content'] = Variable(deletedContent); map['state_version_id'] = Variable(stateVersionId); if (!nullToAbsent || stateEncryptionKey != null) { map['state_encryption_key'] = Variable(stateEncryptionKey); @@ -1177,6 +1199,7 @@ class Group extends DataClass implements Insertable { archived: Value(archived), joinedGroup: Value(joinedGroup), leftGroup: Value(leftGroup), + deletedContent: Value(deletedContent), stateVersionId: Value(stateVersionId), stateEncryptionKey: stateEncryptionKey == null && nullToAbsent ? const Value.absent() @@ -1221,6 +1244,7 @@ class Group extends DataClass implements Insertable { archived: serializer.fromJson(json['archived']), joinedGroup: serializer.fromJson(json['joinedGroup']), leftGroup: serializer.fromJson(json['leftGroup']), + deletedContent: serializer.fromJson(json['deletedContent']), stateVersionId: serializer.fromJson(json['stateVersionId']), stateEncryptionKey: serializer.fromJson(json['stateEncryptionKey']), @@ -1257,6 +1281,7 @@ class Group extends DataClass implements Insertable { 'archived': serializer.toJson(archived), 'joinedGroup': serializer.toJson(joinedGroup), 'leftGroup': serializer.toJson(leftGroup), + 'deletedContent': serializer.toJson(deletedContent), 'stateVersionId': serializer.toJson(stateVersionId), 'stateEncryptionKey': serializer.toJson(stateEncryptionKey), 'myGroupPrivateKey': serializer.toJson(myGroupPrivateKey), @@ -1286,6 +1311,7 @@ class Group extends DataClass implements Insertable { bool? archived, bool? joinedGroup, bool? leftGroup, + bool? deletedContent, int? stateVersionId, Value stateEncryptionKey = const Value.absent(), Value myGroupPrivateKey = const Value.absent(), @@ -1310,6 +1336,7 @@ class Group extends DataClass implements Insertable { archived: archived ?? this.archived, joinedGroup: joinedGroup ?? this.joinedGroup, leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, stateVersionId: stateVersionId ?? this.stateVersionId, stateEncryptionKey: stateEncryptionKey.present ? stateEncryptionKey.value @@ -1355,6 +1382,9 @@ class Group extends DataClass implements Insertable { joinedGroup: data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup, leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + deletedContent: data.deletedContent.present + ? data.deletedContent.value + : this.deletedContent, stateVersionId: data.stateVersionId.present ? data.stateVersionId.value : this.stateVersionId, @@ -1413,6 +1443,7 @@ class Group extends DataClass implements Insertable { ..write('archived: $archived, ') ..write('joinedGroup: $joinedGroup, ') ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') ..write('stateVersionId: $stateVersionId, ') ..write('stateEncryptionKey: $stateEncryptionKey, ') ..write('myGroupPrivateKey: $myGroupPrivateKey, ') @@ -1443,6 +1474,7 @@ class Group extends DataClass implements Insertable { archived, joinedGroup, leftGroup, + deletedContent, stateVersionId, $driftBlobEquality.hash(stateEncryptionKey), $driftBlobEquality.hash(myGroupPrivateKey), @@ -1471,6 +1503,7 @@ class Group extends DataClass implements Insertable { other.archived == this.archived && other.joinedGroup == this.joinedGroup && other.leftGroup == this.leftGroup && + other.deletedContent == this.deletedContent && other.stateVersionId == this.stateVersionId && $driftBlobEquality.equals( other.stateEncryptionKey, this.stateEncryptionKey) && @@ -1500,6 +1533,7 @@ class GroupsCompanion extends UpdateCompanion { final Value archived; final Value joinedGroup; final Value leftGroup; + final Value deletedContent; final Value stateVersionId; final Value stateEncryptionKey; final Value myGroupPrivateKey; @@ -1525,6 +1559,7 @@ class GroupsCompanion extends UpdateCompanion { this.archived = const Value.absent(), this.joinedGroup = const Value.absent(), this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), this.stateVersionId = const Value.absent(), this.stateEncryptionKey = const Value.absent(), this.myGroupPrivateKey = const Value.absent(), @@ -1551,6 +1586,7 @@ class GroupsCompanion extends UpdateCompanion { this.archived = const Value.absent(), this.joinedGroup = const Value.absent(), this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), this.stateVersionId = const Value.absent(), this.stateEncryptionKey = const Value.absent(), this.myGroupPrivateKey = const Value.absent(), @@ -1578,6 +1614,7 @@ class GroupsCompanion extends UpdateCompanion { Expression? archived, Expression? joinedGroup, Expression? leftGroup, + Expression? deletedContent, Expression? stateVersionId, Expression? stateEncryptionKey, Expression? myGroupPrivateKey, @@ -1604,6 +1641,7 @@ class GroupsCompanion extends UpdateCompanion { if (archived != null) 'archived': archived, if (joinedGroup != null) 'joined_group': joinedGroup, if (leftGroup != null) 'left_group': leftGroup, + if (deletedContent != null) 'deleted_content': deletedContent, if (stateVersionId != null) 'state_version_id': stateVersionId, if (stateEncryptionKey != null) 'state_encryption_key': stateEncryptionKey, @@ -1638,6 +1676,7 @@ class GroupsCompanion extends UpdateCompanion { Value? archived, Value? joinedGroup, Value? leftGroup, + Value? deletedContent, Value? stateVersionId, Value? stateEncryptionKey, Value? myGroupPrivateKey, @@ -1663,6 +1702,7 @@ class GroupsCompanion extends UpdateCompanion { archived: archived ?? this.archived, joinedGroup: joinedGroup ?? this.joinedGroup, leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, stateVersionId: stateVersionId ?? this.stateVersionId, stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, @@ -1709,6 +1749,9 @@ class GroupsCompanion extends UpdateCompanion { if (leftGroup.present) { map['left_group'] = Variable(leftGroup.value); } + if (deletedContent.present) { + map['deleted_content'] = Variable(deletedContent.value); + } if (stateVersionId.present) { map['state_version_id'] = Variable(stateVersionId.value); } @@ -1780,6 +1823,7 @@ class GroupsCompanion extends UpdateCompanion { ..write('archived: $archived, ') ..write('joinedGroup: $joinedGroup, ') ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') ..write('stateVersionId: $stateVersionId, ') ..write('stateEncryptionKey: $stateEncryptionKey, ') ..write('myGroupPrivateKey: $myGroupPrivateKey, ') @@ -8333,6 +8377,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ Value archived, Value joinedGroup, Value leftGroup, + Value deletedContent, Value stateVersionId, Value stateEncryptionKey, Value myGroupPrivateKey, @@ -8359,6 +8404,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value archived, Value joinedGroup, Value leftGroup, + Value deletedContent, Value stateVersionId, Value stateEncryptionKey, Value myGroupPrivateKey, @@ -8459,6 +8505,10 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get leftGroup => $composableBuilder( column: $table.leftGroup, builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedContent => $composableBuilder( + column: $table.deletedContent, + builder: (column) => ColumnFilters(column)); + ColumnFilters get stateVersionId => $composableBuilder( column: $table.stateVersionId, builder: (column) => ColumnFilters(column)); @@ -8614,6 +8664,10 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get leftGroup => $composableBuilder( column: $table.leftGroup, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedContent => $composableBuilder( + column: $table.deletedContent, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get stateVersionId => $composableBuilder( column: $table.stateVersionId, builder: (column) => ColumnOrderings(column)); @@ -8708,6 +8762,9 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get leftGroup => $composableBuilder(column: $table.leftGroup, builder: (column) => column); + GeneratedColumn get deletedContent => $composableBuilder( + column: $table.deletedContent, builder: (column) => column); + GeneratedColumn get stateVersionId => $composableBuilder( column: $table.stateVersionId, builder: (column) => column); @@ -8853,6 +8910,7 @@ class $$GroupsTableTableManager extends RootTableManager< Value archived = const Value.absent(), Value joinedGroup = const Value.absent(), Value leftGroup = const Value.absent(), + Value deletedContent = const Value.absent(), Value stateVersionId = const Value.absent(), Value stateEncryptionKey = const Value.absent(), Value myGroupPrivateKey = const Value.absent(), @@ -8879,6 +8937,7 @@ class $$GroupsTableTableManager extends RootTableManager< archived: archived, joinedGroup: joinedGroup, leftGroup: leftGroup, + deletedContent: deletedContent, stateVersionId: stateVersionId, stateEncryptionKey: stateEncryptionKey, myGroupPrivateKey: myGroupPrivateKey, @@ -8905,6 +8964,7 @@ class $$GroupsTableTableManager extends RootTableManager< Value archived = const Value.absent(), Value joinedGroup = const Value.absent(), Value leftGroup = const Value.absent(), + Value deletedContent = const Value.absent(), Value stateVersionId = const Value.absent(), Value stateEncryptionKey = const Value.absent(), Value myGroupPrivateKey = const Value.absent(), @@ -8931,6 +8991,7 @@ class $$GroupsTableTableManager extends RootTableManager< archived: archived, joinedGroup: joinedGroup, leftGroup: leftGroup, + deletedContent: deletedContent, stateVersionId: stateVersionId, stateEncryptionKey: stateEncryptionKey, myGroupPrivateKey: myGroupPrivateKey, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index e0bcbc9..008081d 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -80,6 +80,7 @@ "@shareImageUserNotVerifiedDesc": {}, "shareImageShowArchived": "Archivierte Benutzer anzeigen", "@shareImageShowArchived": {}, + "startNewChatSearchHint": "Name, Benutzername oder Gruppenname", "searchUsernameInput": "Benutzername", "@searchUsernameInput": {}, "searchUsernameTitle": "Benutzernamen suchen", @@ -692,6 +693,9 @@ "@durationShortHour": {}, "durationShortDays": "Tagen", "@durationShortDays": {}, + "contacts": "Kontakte", + "groups": "Gruppen", + "@groups": {}, "newGroup": "Neue Gruppe", "@newGroup": {}, "selectMembers": "Mitglieder auswählen", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index cfb34bc..c8a3464 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -66,6 +66,7 @@ "@shareImagedEditorSavedImage": {}, "shareImageSearchAllContacts": "Search all contacts", "@shareImageSearchAllContacts": {}, + "startNewChatSearchHint": "Name, username or groupname", "shareImagedSelectAll": "Select all", "@shareImagedSelectAll": {}, "startNewChatTitle": "Select Contact", @@ -516,6 +517,8 @@ "durationShortMinute": "Min.", "durationShortHour": "Hrs.", "durationShortDays": "Days", + "contacts": "Contacts", + "groups": "Groups", "newGroup": "New group", "selectMembers": "Select members", "selectGroupName": "Select group name", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index f1bf190..d434ff5 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -302,6 +302,12 @@ abstract class AppLocalizations { /// **'Search all contacts'** String get shareImageSearchAllContacts; + /// No description provided for @startNewChatSearchHint. + /// + /// In en, this message translates to: + /// **'Name, username or groupname'** + String get startNewChatSearchHint; + /// No description provided for @shareImagedSelectAll. /// /// In en, this message translates to: @@ -2198,6 +2204,18 @@ abstract class AppLocalizations { /// **'Days'** String get durationShortDays; + /// No description provided for @contacts. + /// + /// In en, this message translates to: + /// **'Contacts'** + String get contacts; + + /// No description provided for @groups. + /// + /// In en, this message translates to: + /// **'Groups'** + String get groups; + /// No description provided for @newGroup. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index b346930..2cdc123 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -122,6 +122,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get shareImageSearchAllContacts => 'Alle Kontakte durchsuchen'; + @override + String get startNewChatSearchHint => 'Name, Benutzername oder Gruppenname'; + @override String get shareImagedSelectAll => 'Alle auswählen'; @@ -1166,6 +1169,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get durationShortDays => 'Tagen'; + @override + String get contacts => 'Kontakte'; + + @override + String get groups => 'Gruppen'; + @override String get newGroup => 'Neue Gruppe'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 9dff907..facac30 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -121,6 +121,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get shareImageSearchAllContacts => 'Search all contacts'; + @override + String get startNewChatSearchHint => 'Name, username or groupname'; + @override String get shareImagedSelectAll => 'Select all'; @@ -1159,6 +1162,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get durationShortDays => 'Days'; + @override + String get contacts => 'Contacts'; + + @override + String get groups => 'Groups'; + @override String get newGroup => 'New group'; diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 1db4ef1..8b5aea9 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -74,6 +74,7 @@ Future insertMediaFileInMessagesTable( message.groupId, const GroupsCompanion( archived: Value(false), + deletedContent: Value(false), ), ); } else { diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index 26db258..2b5acf8 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -191,7 +191,7 @@ class ContactsListView extends StatelessWidget { Tooltip( message: context.lang.searchUserNameArchiveUserTooltip, child: IconButton( - icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15), + icon: const FaIcon(Icons.archive_outlined, size: 15), onPressed: () async { const update = ContactsCompanion(requested: Value(false)); await twonlyDB.contactsDao.updateContact(contact.userId, update); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 1e47905..f6e9593 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -76,9 +76,9 @@ class _ChatMessagesViewState extends State { String currentInputText = ''; late StreamSubscription userSub; late StreamSubscription> messageSub; - late StreamSubscription>? groupActionsSub; - late StreamSubscription>? contactSub; - late StreamSubscription>>? + StreamSubscription>? groupActionsSub; + StreamSubscription>? contactSub; + StreamSubscription>>? lastOpenedMessageByContactSub; Map userIdToContact = {}; diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 7d7d3fb..03940ab 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -47,16 +47,16 @@ class ChatTextEntry extends StatelessWidget { var displayTime = !combineTextMessageWithNext(message, nextMessage); var displayUserName = ''; - if (message.senderId != null && userIdToContact != null) { + if (message.senderId != null && + userIdToContact != null && + userIdToContact![message.senderId] != null) { if (prevMessage == null) { displayUserName = getContactDisplayName(userIdToContact![message.senderId]!); } else { if (!combineTextMessageWithNext(prevMessage!, message)) { - if (userIdToContact![message.senderId] != null) { - displayUserName = - getContactDisplayName(userIdToContact![message.senderId]!); - } + displayUserName = + getContactDisplayName(userIdToContact![message.senderId]!); } } } diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 3f0f62b..a014939 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/group_context_menu.component.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; @@ -20,18 +21,20 @@ class StartNewChatView extends StatefulWidget { } class _StartNewChatView extends State { - List contacts = []; + List filteredContacts = []; + List filteredGroups = []; List allContacts = []; + List allNonDirectGroups = []; final TextEditingController searchUserName = TextEditingController(); late StreamSubscription> contactSub; + late StreamSubscription> allNonDirectGroupsSub; @override void initState() { super.initState(); - final stream = twonlyDB.contactsDao.watchAllAcceptedContacts(); - - contactSub = stream.listen((update) async { + contactSub = + twonlyDB.contactsDao.watchAllAcceptedContacts().listen((update) async { update.sort( (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)), ); @@ -40,18 +43,28 @@ class _StartNewChatView extends State { }); await filterUsers(); }); + + allNonDirectGroupsSub = + twonlyDB.groupsDao.watchGroupsForStartNewChat().listen((update) async { + setState(() { + allNonDirectGroups = update; + }); + await filterUsers(); + }); } @override void dispose() { - unawaited(contactSub.cancel()); + allNonDirectGroupsSub.cancel(); + contactSub.cancel(); super.dispose(); } Future filterUsers() async { if (searchUserName.value.text.isEmpty) { setState(() { - contacts = allContacts; + filteredContacts = allContacts; + filteredGroups = []; }); return; } @@ -62,11 +75,54 @@ class _StartNewChatView extends State { .contains(searchUserName.value.text.toLowerCase()), ) .toList(); + final groupsFiltered = allNonDirectGroups + .where( + (g) => g.groupName + .toLowerCase() + .contains(searchUserName.value.text.toLowerCase()), + ) + .toList(); setState(() { - contacts = usersFiltered; + filteredContacts = usersFiltered; + filteredGroups = groupsFiltered; }); } + Future _onTapUser(Contact user) async { + var directChat = await twonlyDB.groupsDao.getDirectChat(user.userId); + if (directChat == null) { + await twonlyDB.groupsDao.createNewDirectChat( + user.userId, + GroupsCompanion( + groupName: Value( + getContactDisplayName(user), + ), + ), + ); + directChat = await twonlyDB.groupsDao.getDirectChat(user.userId); + } + if (!mounted) return; + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(directChat!); + }, + ), + ); + } + + Future _onTapGroup(Group group) async { + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(group); + }, + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -88,14 +144,137 @@ class _StartNewChatView extends State { controller: searchUserName, decoration: getInputDecoration( context, - context.lang.shareImageSearchAllContacts, + context.lang.startNewChatSearchHint, ), ), ), const SizedBox(height: 10), Expanded( - child: UserList( - contacts, + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: + filteredContacts.length + 3 + filteredGroups.length, + itemBuilder: (BuildContext context, int i) { + if (searchUserName.text.isEmpty) { + if (i == 0) { + return ListTile( + title: Text(context.lang.newGroup), + leading: const CircleAvatar( + child: FaIcon( + FontAwesomeIcons.userGroup, + size: 13, + ), + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const GroupCreateSelectMembersView(), + ), + ); + }, + ); + } + if (i == 1) { + return ListTile( + title: Text(context.lang.startNewChatNewContact), + leading: const CircleAvatar( + child: FaIcon( + FontAwesomeIcons.userPlus, + size: 13, + ), + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + ); + } + if (i == 2) { + return const Divider(); + } + i = i - 3; + } else { + if (i == 0) { + return filteredContacts.isNotEmpty + ? ListTile( + title: Text( + context.lang.contacts, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ) + : Container(); + } else { + i -= 1; + } + } + + if (i < filteredContacts.length) { + return UserContextMenu( + key: Key(filteredContacts[i].userId.toString()), + contact: filteredContacts[i], + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(filteredContacts[i])), + FlameCounterWidget( + contactId: filteredContacts[i].userId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + contactId: filteredContacts[i].userId, + fontSize: 13, + ), + onTap: () => _onTapUser(filteredContacts[i]), + ), + ); + } + i -= filteredContacts.length; + + if (i == 0) { + return filteredGroups.isNotEmpty + ? ListTile( + title: Text( + context.lang.groups, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ) + : Container(); + } + + i -= 1; + + if (i < filteredGroups.length) { + return GroupContextMenu( + key: Key(filteredGroups[i].groupId), + group: filteredGroups[i], + child: ListTile( + title: Text( + filteredGroups[i].groupName, + ), + leading: AvatarIcon( + group: filteredGroups[i], + fontSize: 13, + ), + onTap: () => _onTapGroup(filteredGroups[i]), + ), + ); + } + return Container(); + }, ), ), ], @@ -105,107 +284,3 @@ class _StartNewChatView extends State { ); } } - -class UserList extends StatelessWidget { - const UserList( - this.users, { - super.key, - }); - final List users; - - @override - Widget build(BuildContext context) { - return ListView.builder( - restorationId: 'new_message_users_list', - itemCount: users.length + 3, - itemBuilder: (BuildContext context, int i) { - if (i == 1) { - return ListTile( - title: Text(context.lang.startNewChatNewContact), - leading: const CircleAvatar( - child: FaIcon( - FontAwesomeIcons.userPlus, - size: 13, - ), - ), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - ); - } - if (i == 0) { - return ListTile( - title: Text(context.lang.newGroup), - leading: const CircleAvatar( - child: FaIcon( - FontAwesomeIcons.userGroup, - size: 13, - ), - ), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const GroupCreateSelectMembersView(), - ), - ); - }, - ); - } - if (i == 2) { - return const Divider(); - } - final user = users[i - 3]; - return UserContextMenu( - key: Key(user.userId.toString()), - contact: user, - child: ListTile( - title: Row( - children: [ - Text(getContactDisplayName(user)), - FlameCounterWidget( - contactId: user.userId, - prefix: true, - ), - ], - ), - leading: AvatarIcon( - contactId: user.userId, - fontSize: 13, - ), - onTap: () async { - var directChat = - await twonlyDB.groupsDao.getDirectChat(user.userId); - if (directChat == null) { - await twonlyDB.groupsDao.createNewDirectChat( - user.userId, - GroupsCompanion( - groupName: Value( - getContactDisplayName(user), - ), - ), - ); - directChat = - await twonlyDB.groupsDao.getDirectChat(user.userId); - } - if (!context.mounted) return; - await Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(directChat!); - }, - ), - ); - }, - ), - ); - }, - ); - } -} diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index b4d87f0..eb877dd 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -29,7 +29,7 @@ class GroupContextMenu extends StatelessWidget { await twonlyDB.groupsDao.updateGroup(group.groupId, update); } }, - icon: FontAwesomeIcons.boxArchive, + icon: Icons.archive_outlined, ), if (group.archived) ContextMenuItem( @@ -40,7 +40,7 @@ class GroupContextMenu extends StatelessWidget { await twonlyDB.groupsDao.updateGroup(group.groupId, update); } }, - icon: FontAwesomeIcons.boxOpen, + icon: Icons.unarchive_outlined, ), ContextMenuItem( title: context.lang.contextMenuOpenChat, @@ -71,6 +71,19 @@ class GroupContextMenu extends StatelessWidget { ? FontAwesomeIcons.thumbtackSlash : FontAwesomeIcons.thumbtack, ), + ContextMenuItem( + title: context.lang.delete, + icon: FontAwesomeIcons.trashCan, + onTap: () async { + await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + deletedContent: Value(true), + ), + ); + }, + ), ], child: child, ); diff --git a/lib/src/views/settings/backup/backup.view.dart b/lib/src/views/settings/backup/backup.view.dart index 28417bf..1720925 100644 --- a/lib/src/views/settings/backup/backup.view.dart +++ b/lib/src/views/settings/backup/backup.view.dart @@ -201,7 +201,7 @@ class _BackupViewState extends State { label: 'twonly Backup', ), BottomNavigationBarItem( - icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 17), + icon: const FaIcon(Icons.archive_outlined, size: 17), label: context.lang.backupData, ), ], From 0d4ab84f911408c007db89633649eb6711eeba41 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 16:59:19 +0100 Subject: [PATCH 57/76] send my public key to new members --- lib/src/services/api/client2client/groups.c2c.dart | 3 +++ lib/src/services/group.services.dart | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 5eb5399..003e9c4 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -165,6 +165,9 @@ Future handleGroupJoin( if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { if (!await addNewHiddenContact(fromUserId)) { Log.error('Got group join, but could not load contact.'); + // This can happen in case the group join was received before the group create. + // In this case return false, which will cause the receipt to fail and the user + // will resend this message. return false; } } diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 9d24c39..ffe6d56 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -261,6 +261,20 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { ), ); } + + // Send the new user my public group key + if (group.myGroupPrivateKey != null) { + final keyPair = + IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + await sendCipherText( + memberId.toInt(), + EncryptedContent( + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); + } } // check if there is a member which is not in the server list... From f5dbf4a12ef4da15a42b1aded5424a4de5b1c7c7 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 17:28:39 +0100 Subject: [PATCH 58/76] create context request from context menu --- lib/src/database/daos/groups.dao.dart | 12 ++ lib/src/localization/app_de.arb | 1 + lib/src/localization/app_en.arb | 1 + .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + .../client/generated/messages.pb.dart | 60 +++++++++- .../client/generated/messages.pbjson.dart | 112 ++++++++++-------- lib/src/model/protobuf/client/messages.proto | 7 +- lib/src/services/api.service.dart | 1 + .../api/client2client/groups.c2c.dart | 19 +++ lib/src/services/api/server_messages.dart | 9 ++ lib/src/services/group.services.dart | 19 +++ .../group_create_select_group_name.view.dart | 1 - .../views/groups/group_member.context.dart | 32 ++++- 15 files changed, 224 insertions(+), 62 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 1d930a0..6a4d7de 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -219,6 +219,18 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .get(); } + Future> getAllGroupMemberWithoutPublicKey() { + final query = + ((select(groups)..where((t) => t.isDirectChat.equals(false))).join([ + leftOuterJoin( + groupMembers, + groupMembers.groupId.equalsExp(groups.groupId), + ), + ]) + ..where(groupMembers.groupPublicKey.isNull())); + return query.map((row) => row.readTable(groupMembers)).get(); + } + Future getDirectChat(int userId) async { final query = ((select(groups)..where((t) => t.isDirectChat.equals(true))).join([ diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 008081d..6c8e057 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -714,6 +714,7 @@ "@leaveGroup": {}, "createContactRequest": "Kontaktanfrage erstellen", "@createContactRequest": {}, + "contactRequestSend": "Kontakanfrage gesendet", "makeAdmin": "Zum Admin machen", "@makeAdmin": {}, "removeAdmin": "Als Admin entfernen", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c8a3464..d856fb3 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -528,6 +528,7 @@ "createGroup": "Create group", "leaveGroup": "Leave group", "createContactRequest": "Create contact request", + "contactRequestSend": "Contact request send", "makeAdmin": "Make admin", "removeAdmin": "Remove as admin", "removeFromGroup": "Remove from group", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d434ff5..c1e11b7 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2270,6 +2270,12 @@ abstract class AppLocalizations { /// **'Create contact request'** String get createContactRequest; + /// No description provided for @contactRequestSend. + /// + /// In en, this message translates to: + /// **'Contact request send'** + String get contactRequestSend; + /// No description provided for @makeAdmin. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2cdc123..44fbc6f 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1202,6 +1202,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get createContactRequest => 'Kontaktanfrage erstellen'; + @override + String get contactRequestSend => 'Kontakanfrage gesendet'; + @override String get makeAdmin => 'Zum Admin machen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index facac30..86bfc4d 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1195,6 +1195,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get createContactRequest => 'Create contact request'; + @override + String get contactRequestSend => 'Contact request send'; + @override String get makeAdmin => 'Make admin'; diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 756be8b..4763a2f 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -342,7 +342,7 @@ class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { factory EncryptedContent_GroupJoin.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupJoin', createEmptyInstance: create) - ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') ..hasRequiredFields = false ; @@ -368,14 +368,46 @@ class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { static EncryptedContent_GroupJoin? _defaultInstance; /// key for the state stored on the server - @$pb.TagNumber(4) + @$pb.TagNumber(1) $core.List<$core.int> get groupPublicKey => $_getN(0); - @$pb.TagNumber(4) + @$pb.TagNumber(1) set groupPublicKey($core.List<$core.int> v) { $_setBytes(0, v); } - @$pb.TagNumber(4) + @$pb.TagNumber(1) $core.bool hasGroupPublicKey() => $_has(0); - @$pb.TagNumber(4) - void clearGroupPublicKey() => clearField(4); + @$pb.TagNumber(1) + void clearGroupPublicKey() => clearField(1); +} + +class EncryptedContent_ResendGroupPublicKey extends $pb.GeneratedMessage { + factory EncryptedContent_ResendGroupPublicKey() => create(); + EncryptedContent_ResendGroupPublicKey._() : super(); + factory EncryptedContent_ResendGroupPublicKey.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ResendGroupPublicKey.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ResendGroupPublicKey', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ResendGroupPublicKey clone() => EncryptedContent_ResendGroupPublicKey()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ResendGroupPublicKey copyWith(void Function(EncryptedContent_ResendGroupPublicKey) updates) => super.copyWith((message) => updates(message as EncryptedContent_ResendGroupPublicKey)) as EncryptedContent_ResendGroupPublicKey; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ResendGroupPublicKey create() => EncryptedContent_ResendGroupPublicKey._(); + EncryptedContent_ResendGroupPublicKey createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ResendGroupPublicKey getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ResendGroupPublicKey? _defaultInstance; } class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { @@ -1281,6 +1313,7 @@ class EncryptedContent extends $pb.GeneratedMessage { EncryptedContent_GroupCreate? groupCreate, EncryptedContent_GroupJoin? groupJoin, EncryptedContent_GroupUpdate? groupUpdate, + EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, }) { final $result = create(); if (groupId != null) { @@ -1328,6 +1361,9 @@ class EncryptedContent extends $pb.GeneratedMessage { if (groupUpdate != null) { $result.groupUpdate = groupUpdate; } + if (resendGroupPublicKey != null) { + $result.resendGroupPublicKey = resendGroupPublicKey; + } return $result; } EncryptedContent._() : super(); @@ -1350,6 +1386,7 @@ class EncryptedContent extends $pb.GeneratedMessage { ..aOM(14, _omitFieldNames ? '' : 'groupCreate', protoName: 'groupCreate', subBuilder: EncryptedContent_GroupCreate.create) ..aOM(15, _omitFieldNames ? '' : 'groupJoin', protoName: 'groupJoin', subBuilder: EncryptedContent_GroupJoin.create) ..aOM(16, _omitFieldNames ? '' : 'groupUpdate', protoName: 'groupUpdate', subBuilder: EncryptedContent_GroupUpdate.create) + ..aOM(17, _omitFieldNames ? '' : 'resendGroupPublicKey', protoName: 'resendGroupPublicKey', subBuilder: EncryptedContent_ResendGroupPublicKey.create) ..hasRequiredFields = false ; @@ -1533,6 +1570,17 @@ class EncryptedContent extends $pb.GeneratedMessage { void clearGroupUpdate() => clearField(16); @$pb.TagNumber(16) EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14); + + @$pb.TagNumber(17) + EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(15); + @$pb.TagNumber(17) + set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey v) { setField(17, v); } + @$pb.TagNumber(17) + $core.bool hasResendGroupPublicKey() => $_has(15); + @$pb.TagNumber(17) + void clearResendGroupPublicKey() => clearField(17); + @$pb.TagNumber(17) + EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() => $_ensure(15); } diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 82a3e7a..f07d909 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -118,8 +118,9 @@ const EncryptedContent$json = { {'1': 'groupCreate', '3': 14, '4': 1, '5': 11, '6': '.EncryptedContent.GroupCreate', '9': 12, '10': 'groupCreate', '17': true}, {'1': 'groupJoin', '3': 15, '4': 1, '5': 11, '6': '.EncryptedContent.GroupJoin', '9': 13, '10': 'groupJoin', '17': true}, {'1': 'groupUpdate', '3': 16, '4': 1, '5': 11, '6': '.EncryptedContent.GroupUpdate', '9': 14, '10': 'groupUpdate', '17': true}, + {'1': 'resendGroupPublicKey', '3': 17, '4': 1, '5': 11, '6': '.EncryptedContent.ResendGroupPublicKey', '9': 15, '10': 'resendGroupPublicKey', '17': true}, ], - '3': [EncryptedContent_GroupCreate$json, EncryptedContent_GroupJoin$json, EncryptedContent_GroupUpdate$json, EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], + '3': [EncryptedContent_GroupCreate$json, EncryptedContent_GroupJoin$json, EncryptedContent_ResendGroupPublicKey$json, EncryptedContent_GroupUpdate$json, EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], '8': [ {'1': '_groupId'}, {'1': '_isDirectChat'}, @@ -136,6 +137,7 @@ const EncryptedContent$json = { {'1': '_groupCreate'}, {'1': '_groupJoin'}, {'1': '_groupUpdate'}, + {'1': '_resendGroupPublicKey'}, ], }; @@ -152,10 +154,15 @@ const EncryptedContent_GroupCreate$json = { const EncryptedContent_GroupJoin$json = { '1': 'GroupJoin', '2': [ - {'1': 'groupPublicKey', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, + {'1': 'groupPublicKey', '3': 1, '4': 1, '5': 12, '10': 'groupPublicKey'}, ], }; +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ResendGroupPublicKey$json = { + '1': 'ResendGroupPublicKey', +}; + @$core.Deprecated('Use encryptedContentDescriptor instead') const EncryptedContent_GroupUpdate$json = { '1': 'GroupUpdate', @@ -376,53 +383,56 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'Mh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cENyZWF0ZUgMUgtncm91cENyZWF0ZYgBARI+Cglncm' '91cEpvaW4YDyABKAsyGy5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgNUglncm91cEpvaW6I' 'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA' - '5SC2dyb3VwVXBkYXRliAEBGlEKC0dyb3VwQ3JlYXRlEhoKCHN0YXRlS2V5GAMgASgMUghzdGF0' - 'ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQdWJsaWNLZXkaMwoJR3JvdXBKb2' - 'luEiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRq6AQoLR3JvdXBVcGRh' - 'dGUSKAoPZ3JvdXBBY3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZW' - 'RDb250YWN0SWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1l' - 'GAMgASgJSAFSDG5ld0dyb3VwTmFtZYgBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0' - 'dyb3VwTmFtZRqpAQoLVGV4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5k' - 'ZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbW' - 'VzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9f' - 'cXVvdGVNZXNzYWdlSWQaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YX' - 'JnZXRNZXNzYWdlSWQSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVt' - 'b3ZlGrcCCg1NZXNzYWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk' - '1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2Vu' - 'ZGVyTWVzc2FnZUlkiAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdG' - 'lwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3Rh' - 'bXAYBSABKANSCXRpbWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEg' - 'oKBk9QRU5FRBACQhIKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQajAUKBU1lZGlhEigKD3Nl' - 'bmRlck1lc3NhZ2VJZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5Fbm' - 'NyeXB0ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNl' - 'Y29uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZX' - 'NBdXRoZW50aWNhdGlvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3Rh' - 'bXAYBSABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3' - 'NhZ2VJZIgBARIpCg1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoN' - 'ZW5jcnlwdGlvbktleRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYW' - 'MYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIP' - 'ZW5jcnlwdGlvbk5vbmNliAEBIjMKBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBV' - 'ZJREVPEAISBwoDR0lGEANCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW90' - 'ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5jcn' - 'lwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlGqcBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEg' - 'ASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigKD3RhcmdldE' - '1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIK' - 'CgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdH' - 'lwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIr' - 'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrwAQoNQ29udGFjdF' - 'VwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5' - 'cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcm' - 'Vzc2VkiAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUS' - 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQg4KDF9kaX' - 'NwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQu' - 'UHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAy' - 'ABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlw' - 'ZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZW' - 'RBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYK' - 'Fmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHg' - 'oKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RD' - 'aGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaW' - 'FCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIM' - 'CgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg' - '4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZQ=='); + '5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw' + 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' + 'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ' + 'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym' + 'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkaugEK' + 'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj' + 'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l' + 'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQFCFAoSX2FmZmVjdGVkQ29udGFjdE' + 'lkQg8KDV9uZXdHcm91cE5hbWUaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgB' + 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGA' + 'MgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdl' + 'SWSIAQFCEQoPX3F1b3RlTWVzc2FnZUlkGmIKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZB' + 'gBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVlbW9qaRIWCgZyZW1vdmUY' + 'AyABKAhSBnJlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdG' + 'VkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIg' + 'ASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAy' + 'ADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQES' + 'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRE' + 'lUX1RFWFQQARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVN' + 'ZWRpYRIoCg9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGA' + 'IgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1p' + 'dEluTWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEj' + 'YKFnJlcXVpcmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24S' + 'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAV' + 'IOcXVvdGVNZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRv' + 'a2VuiAEBEikKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbm' + 'NyeXB0aW9uTWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNl' + 'GAogASgMSAVSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU' + '1BR0UQARIJCgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k' + 'c0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZX' + 'lCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUS' + 'NgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZR' + 'IoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJF' + 'T1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZX' + 'F1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5' + 'cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa8A' + 'EKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFj' + 'dFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdG' + 'FyU3ZnQ29tcHJlc3NlZIgBARIlCgtkaXNwbGF5TmFtZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgB' + 'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' + 'NlZEIOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0' + 'ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQ' + 'ESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0' + 'iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5Qg' + 'wKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1l' + 'Q291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudG' + 'VyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCDwoN' + 'X2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdG' + 'VCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFj' + 'dFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZX' + 'h0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVC' + 'FwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index e47999f..dc21e61 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -50,6 +50,7 @@ message EncryptedContent { optional GroupCreate groupCreate = 14; optional GroupJoin groupJoin = 15; optional GroupUpdate groupUpdate = 16; + optional ResendGroupPublicKey resendGroupPublicKey = 17; message GroupCreate { @@ -60,7 +61,11 @@ message EncryptedContent { message GroupJoin { // key for the state stored on the server - bytes groupPublicKey = 4; + bytes groupPublicKey = 1; + } + + message ResendGroupPublicKey { + } message GroupUpdate { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 05d8d88..ae7ff3b 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -101,6 +101,7 @@ class ApiService { unawaited(setupNotificationWithUsers()); unawaited(signalHandleNewServerConnection()); unawaited(fetchGroupStatesForUnjoinedGroups()); + unawaited(fetchMissingGroupPublicKey()); } } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 003e9c4..e0c0b5f 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -180,3 +180,22 @@ Future handleGroupJoin( ); return true; } + +Future handleResendGroupPublicKey( + int fromUserId, + String groupId, + EncryptedContent_GroupJoin join, +) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null || group.myGroupPrivateKey == null) return; + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + await sendCipherText( + fromUserId, + EncryptedContent( + groupId: groupId, + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); +} diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index b072b30..1d85e4f 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -288,6 +288,15 @@ Future handleEncryptedMessage( return null; } + if (content.hasResendGroupPublicKey()) { + await handleResendGroupPublicKey( + fromUserId, + content.groupId, + content.groupJoin, + ); + return null; + } + if (content.hasTextMessage()) { await handleTextMessage( fromUserId, diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index ffe6d56..0b40768 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -149,6 +149,25 @@ Future fetchGroupStatesForUnjoinedGroups() async { } } +Future fetchMissingGroupPublicKey() async { + final members = await twonlyDB.groupsDao.getAllGroupMemberWithoutPublicKey(); + + for (final member in members) { + if (member.lastMessage == null) continue; + // only request if the users has send a message in the last two days. + if (member.lastMessage! + .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + await sendCipherText( + member.contactId, + EncryptedContent( + groupId: member.groupId, + resendGroupPublicKey: EncryptedContent_ResendGroupPublicKey(), + ), + ); + } + } +} + Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { if (group.leftGroup) { Log.error( diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart index 7f280bb..3af27e5 100644 --- a/lib/src/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -101,7 +101,6 @@ class _GroupCreateSelectGroupNameViewState itemBuilder: (BuildContext context, int i) { final user = widget.selectedUsers[i]; return UserContextMenu( - key: GlobalKey(), contact: user, child: ListTile( title: Row( diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart index d3b7b42..08084e1 100644 --- a/lib/src/views/groups/group_member.context.dart +++ b/lib/src/views/groups/group_member.context.dart @@ -1,9 +1,12 @@ +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; @@ -85,6 +88,31 @@ class GroupMemberContextMenu extends StatelessWidget { } } + Future _makeContactRequest(BuildContext context) async { + await twonlyDB.contactsDao.updateContact( + member.contactId, + const ContactsCompanion( + requested: Value(true), + ), + ); + await sendCipherText( + member.contactId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), + ), + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.contactRequestSend), + duration: const Duration(seconds: 3), + ), + ); + } + } + @override Widget build(BuildContext context) { return ContextMenu( @@ -112,9 +140,7 @@ class GroupMemberContextMenu extends StatelessWidget { if (!contact.accepted) ContextMenuItem( title: context.lang.createContactRequest, - onTap: () async { - // onResponseTriggered(); - }, + onTap: () => _makeContactRequest(context), icon: FontAwesomeIcons.userPlus, ), if (member.groupPublicKey != null && From 0dc8c8773234ef39fccf3173ada5151736dbcac4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 17:46:45 +0100 Subject: [PATCH 59/76] fixes some small issues --- lib/src/localization/app_de.arb | 3 ++- lib/src/localization/app_en.arb | 3 ++- .../generated/app_localizations.dart | 6 ++++++ .../generated/app_localizations_de.dart | 4 ++++ .../generated/app_localizations_en.dart | 4 ++++ .../api/client2client/contact.c2c.dart | 1 + lib/src/services/group.services.dart | 3 +++ .../group_context_menu.component.dart | 20 +++++++++++++------ lib/src/views/contact/contact.view.dart | 18 +++++++++-------- lib/src/views/groups/group.view.dart | 2 +- .../group_create_select_members.view.dart | 3 ++- 11 files changed, 49 insertions(+), 18 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 6c8e057..084ed87 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -796,5 +796,6 @@ "notificationResponse": "hat dir{inGroup} geantwortet.", "notificationTitleUnknownUser": "Jemand", "notificationCategoryMessageTitle": "Nachrichten", - "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern." + "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.", + "groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index d856fb3..ba4a6da 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -574,5 +574,6 @@ "notificationResponse": "has responded{inGroup}.", "notificationTitleUnknownUser": "Someone", "notificationCategoryMessageTitle": "Messages", - "notificationCategoryMessageDesc": "Messages from other users." + "notificationCategoryMessageDesc": "Messages from other users.", + "groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index c1e11b7..0a8a138 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2551,6 +2551,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Messages from other users.'** String get notificationCategoryMessageDesc; + + /// No description provided for @groupContextMenuDeleteGroup. + /// + /// In en, this message translates to: + /// **'This will permanently delete all messages in this chat.'** + String get groupContextMenuDeleteGroup; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 44fbc6f..3d67b92 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1393,4 +1393,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get notificationCategoryMessageDesc => 'Nachrichten von anderen Benutzern.'; + + @override + String get groupContextMenuDeleteGroup => + 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 86bfc4d..c0cb321 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1385,4 +1385,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationCategoryMessageDesc => 'Messages from other users.'; + + @override + String get groupContextMenuDeleteGroup => + 'This will permanently delete all messages in this chat.'; } diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index a961536..61376c4 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -59,6 +59,7 @@ Future handleContactRequest( const ContactsCompanion( requested: Value(false), accepted: Value(true), + deletedByUser: Value(false), ), ); final contact = await twonlyDB.contactsDao diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 0b40768..c034afb 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -18,6 +19,7 @@ import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -365,6 +367,7 @@ Future addNewHiddenContact(int contactId) async { ), ); await createNewSignalSession(userData); + unawaited(setupNotificationWithUsers(forceContact: contactId)); return true; } diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index eb877dd..6dca019 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -5,6 +5,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/context_menu.component.dart'; class GroupContextMenu extends StatelessWidget { @@ -75,13 +76,20 @@ class GroupContextMenu extends StatelessWidget { title: context.lang.delete, icon: FontAwesomeIcons.trashCan, onTap: () async { - await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); - await twonlyDB.groupsDao.updateGroup( - group.groupId, - const GroupsCompanion( - deletedContent: Value(true), - ), + final ok = await showAlertDialog( + context, + context.lang.deleteTitle, + context.lang.groupContextMenuDeleteGroup, ); + if (ok) { + await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + deletedContent: Value(true), + ), + ); + } }, ), ], diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index b77ff44..c37f514 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; @@ -110,7 +111,6 @@ class _ContactViewState extends State { return Container(); } final contact = snapshot.data!; - // final flameCounter = getFlameCounterFromContact(contact); return ListView( children: [ Padding( @@ -122,18 +122,19 @@ class _ContactViewState extends State { children: [ Padding( padding: const EdgeInsets.only(right: 10), - child: VerifiedShield(key: GlobalKey(), contact: contact), + child: VerifiedShield( + key: GlobalKey(), + contact: contact, + ), ), Text( getContactDisplayName(contact, maxLength: 20), style: const TextStyle(fontSize: 20), ), - // if (flameCounter > 0) - // FlameCounterWidget( - // contact, - // flameCounter, - // prefix: true, - // ), + FlameCounterWidget( + contactId: contact.userId, + prefix: true, + ), ], ), if (getContactDisplayName(contact) != contact.username) @@ -166,6 +167,7 @@ class _ContactViewState extends State { }, ), ); + setState(() {}); }, ), // BetterListTile( diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 72c4461..a16a2e7 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -117,7 +117,7 @@ class _GroupViewState extends State { children: [ Padding( padding: const EdgeInsets.only(right: 10), - child: VerifiedShield(key: GlobalKey(), group: group), + child: VerifiedShield(key: Key(group.groupId), group: group), ), Text( substringBy(group.groupName, 25), diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index 892c202..0b2ca38 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -188,7 +188,8 @@ class _StartNewChatView extends State { } final user = contacts[i]; return UserContextMenu( - key: GlobalKey(), + // when this is not set, then the avatar is not updated in the list when searching :/ + key: Key(user.userId.toString()), contact: user, child: ListTile( title: Row( From 2a3152e707f253f098c4e9e2fba2b266a262df36 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 3 Nov 2025 22:58:58 +0100 Subject: [PATCH 60/76] leave groups works now --- lib/src/database/daos/groups.dao.dart | 13 +- lib/src/localization/app_de.arb | 9 +- lib/src/localization/app_en.arb | 9 +- .../generated/app_localizations.dart | 42 +++ .../generated/app_localizations_de.dart | 24 ++ .../generated/app_localizations_en.dart | 23 ++ .../protobuf/api/http/http_requests.pb.dart | 265 +++++++++++++- .../api/http/http_requests.pbjson.dart | 69 +++- .../protobuf/client/generated/groups.pb.dart | 54 +++ .../client/generated/groups.pbenum.dart | 19 + .../client/generated/groups.pbjson.dart | 22 ++ lib/src/model/protobuf/client/groups.proto | 7 + .../api/client2client/groups.c2c.dart | 1 + lib/src/services/group.services.dart | 342 ++++++++++++++---- .../views/components/better_list_title.dart | 2 +- lib/src/views/groups/group.view.dart | 93 ++++- .../views/groups/group_member.context.dart | 8 +- test/unit_test.dart | 18 + 18 files changed, 903 insertions(+), 117 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 6a4d7de..a84e62f 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -38,10 +38,21 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future> getGroupMembers(String groupId) async { - return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + return (select(groupMembers) + ..where( + (t) => + t.groupId.equals(groupId) & + t.memberState.equals(MemberState.leftGroup.name).not(), + )) .get(); } + Future getGroupMemberByPublicKey(Uint8List publicKey) async { + return (select(groupMembers) + ..where((t) => t.groupPublicKey.equals(publicKey))) + .getSingleOrNull(); + } + Future createNewGroup(GroupsCompanion group) async { return _insertGroup(group); } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 084ed87..7a5b7cc 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -797,5 +797,12 @@ "notificationTitleUnknownUser": "Jemand", "notificationCategoryMessageTitle": "Nachrichten", "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.", - "groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht." + "groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.", + "groupYouAreNowLongerAMember": "Du bist nicht mehr Mitglied dieser Gruppe.", + "groupNetworkIssue": "Netzwerkproblem. Bitte probiere es später noch einmal.", + "leaveGroupSelectOtherAdminTitle": "Einen Admin auswählen", + "leaveGroupSelectOtherAdminBody": "Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.", + "leaveGroupSureTitle": "Gruppe verlassen", + "leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?", + "leaveGroupSureOkBtn": "Gruppe verlassen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index ba4a6da..1ea6317 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -575,5 +575,12 @@ "notificationTitleUnknownUser": "Someone", "notificationCategoryMessageTitle": "Messages", "notificationCategoryMessageDesc": "Messages from other users.", - "groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat." + "groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.", + "groupYouAreNowLongerAMember": "You are no longer part of this group.", + "groupNetworkIssue": "Network issue. Try again later.", + "leaveGroupSelectOtherAdminTitle": "Select another admin", + "leaveGroupSelectOtherAdminBody": "To leave the group, you must first select a new administrator.", + "leaveGroupSureTitle": "Leave group", + "leaveGroupSureBody": "Do you really want to leave the group?", + "leaveGroupSureOkBtn": "Leave group" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 0a8a138..2184042 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2557,6 +2557,48 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'This will permanently delete all messages in this chat.'** String get groupContextMenuDeleteGroup; + + /// No description provided for @groupYouAreNowLongerAMember. + /// + /// In en, this message translates to: + /// **'You are no longer part of this group.'** + String get groupYouAreNowLongerAMember; + + /// No description provided for @groupNetworkIssue. + /// + /// In en, this message translates to: + /// **'Network issue. Try again later.'** + String get groupNetworkIssue; + + /// No description provided for @leaveGroupSelectOtherAdminTitle. + /// + /// In en, this message translates to: + /// **'Select another admin'** + String get leaveGroupSelectOtherAdminTitle; + + /// No description provided for @leaveGroupSelectOtherAdminBody. + /// + /// In en, this message translates to: + /// **'To leave the group, you must first select a new administrator.'** + String get leaveGroupSelectOtherAdminBody; + + /// No description provided for @leaveGroupSureTitle. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroupSureTitle; + + /// No description provided for @leaveGroupSureBody. + /// + /// In en, this message translates to: + /// **'Do you really want to leave the group?'** + String get leaveGroupSureBody; + + /// No description provided for @leaveGroupSureOkBtn. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroupSureOkBtn; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 3d67b92..01d99e2 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1397,4 +1397,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groupContextMenuDeleteGroup => 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; + + @override + String get groupYouAreNowLongerAMember => + 'Du bist nicht mehr Mitglied dieser Gruppe.'; + + @override + String get groupNetworkIssue => + 'Netzwerkproblem. Bitte probiere es später noch einmal.'; + + @override + String get leaveGroupSelectOtherAdminTitle => 'Einen Admin auswählen'; + + @override + String get leaveGroupSelectOtherAdminBody => + 'Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.'; + + @override + String get leaveGroupSureTitle => 'Gruppe verlassen'; + + @override + String get leaveGroupSureBody => 'Willst du die Gruppe wirklich verlassen?'; + + @override + String get leaveGroupSureOkBtn => 'Gruppe verlassen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index c0cb321..ddbfafa 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1389,4 +1389,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groupContextMenuDeleteGroup => 'This will permanently delete all messages in this chat.'; + + @override + String get groupYouAreNowLongerAMember => + 'You are no longer part of this group.'; + + @override + String get groupNetworkIssue => 'Network issue. Try again later.'; + + @override + String get leaveGroupSelectOtherAdminTitle => 'Select another admin'; + + @override + String get leaveGroupSelectOtherAdminBody => + 'To leave the group, you must first select a new administrator.'; + + @override + String get leaveGroupSureTitle => 'Leave group'; + + @override + String get leaveGroupSureBody => 'Do you really want to leave the group?'; + + @override + String get leaveGroupSureOkBtn => 'Leave group'; } diff --git a/lib/src/model/protobuf/api/http/http_requests.pb.dart b/lib/src/model/protobuf/api/http/http_requests.pb.dart index 132252d..cb5f517 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pb.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pb.dart @@ -373,10 +373,10 @@ class NewGroupState extends $pb.GeneratedMessage { factory NewGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'NewGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') - ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) - ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOS(1, _omitFieldNames ? '' : 'groupId') + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) ..hasRequiredFields = false ; @@ -419,29 +419,202 @@ class NewGroupState extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearVersionId() => clearField(2); - @$pb.TagNumber(4) + @$pb.TagNumber(3) $core.List<$core.int> get encryptedGroupState => $_getN(2); - @$pb.TagNumber(4) + @$pb.TagNumber(3) set encryptedGroupState($core.List<$core.int> v) { $_setBytes(2, v); } - @$pb.TagNumber(4) + @$pb.TagNumber(3) $core.bool hasEncryptedGroupState() => $_has(2); - @$pb.TagNumber(4) - void clearEncryptedGroupState() => clearField(4); + @$pb.TagNumber(3) + void clearEncryptedGroupState() => clearField(3); - @$pb.TagNumber(5) + @$pb.TagNumber(4) $core.List<$core.int> get publicKey => $_getN(3); - @$pb.TagNumber(5) + @$pb.TagNumber(4) set publicKey($core.List<$core.int> v) { $_setBytes(3, v); } - @$pb.TagNumber(5) + @$pb.TagNumber(4) $core.bool hasPublicKey() => $_has(3); - @$pb.TagNumber(5) - void clearPublicKey() => clearField(5); + @$pb.TagNumber(4) + void clearPublicKey() => clearField(4); +} + +class AppendGroupState_AppendTBS extends $pb.GeneratedMessage { + factory AppendGroupState_AppendTBS({ + $core.List<$core.int>? encryptedGroupStateAppend, + $core.List<$core.int>? publicKey, + $core.String? groupId, + $core.List<$core.int>? nonce, + }) { + final $result = create(); + if (encryptedGroupStateAppend != null) { + $result.encryptedGroupStateAppend = encryptedGroupStateAppend; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + if (groupId != null) { + $result.groupId = groupId; + } + if (nonce != null) { + $result.nonce = nonce; + } + return $result; + } + AppendGroupState_AppendTBS._() : super(); + factory AppendGroupState_AppendTBS.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupState_AppendTBS.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupState.AppendTBS', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'encryptedGroupStateAppend', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'groupId') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupState_AppendTBS clone() => AppendGroupState_AppendTBS()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupState_AppendTBS copyWith(void Function(AppendGroupState_AppendTBS) updates) => super.copyWith((message) => updates(message as AppendGroupState_AppendTBS)) as AppendGroupState_AppendTBS; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupState_AppendTBS create() => AppendGroupState_AppendTBS._(); + AppendGroupState_AppendTBS createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupState_AppendTBS getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupState_AppendTBS? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get encryptedGroupStateAppend => $_getN(0); + @$pb.TagNumber(1) + set encryptedGroupStateAppend($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasEncryptedGroupStateAppend() => $_has(0); + @$pb.TagNumber(1) + void clearEncryptedGroupStateAppend() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get publicKey => $_getN(1); + @$pb.TagNumber(2) + set publicKey($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasPublicKey() => $_has(1); + @$pb.TagNumber(2) + void clearPublicKey() => clearField(2); + + @$pb.TagNumber(3) + $core.String get groupId => $_getSZ(2); + @$pb.TagNumber(3) + set groupId($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasGroupId() => $_has(2); + @$pb.TagNumber(3) + void clearGroupId() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get nonce => $_getN(3); + @$pb.TagNumber(4) + set nonce($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasNonce() => $_has(3); + @$pb.TagNumber(4) + void clearNonce() => clearField(4); +} + +class AppendGroupState extends $pb.GeneratedMessage { + factory AppendGroupState({ + $core.List<$core.int>? signature, + AppendGroupState_AppendTBS? appendTBS, + $fixnum.Int64? versionId, + }) { + final $result = create(); + if (signature != null) { + $result.signature = signature; + } + if (appendTBS != null) { + $result.appendTBS = appendTBS; + } + if (versionId != null) { + $result.versionId = versionId; + } + return $result; + } + AppendGroupState._() : super(); + factory AppendGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY) + ..aOM(2, _omitFieldNames ? '' : 'appendTBS', protoName: 'appendTBS', subBuilder: AppendGroupState_AppendTBS.create) + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupState clone() => AppendGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupState copyWith(void Function(AppendGroupState) updates) => super.copyWith((message) => updates(message as AppendGroupState)) as AppendGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupState create() => AppendGroupState._(); + AppendGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get signature => $_getN(0); + @$pb.TagNumber(1) + set signature($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasSignature() => $_has(0); + @$pb.TagNumber(1) + void clearSignature() => clearField(1); + + @$pb.TagNumber(2) + AppendGroupState_AppendTBS get appendTBS => $_getN(1); + @$pb.TagNumber(2) + set appendTBS(AppendGroupState_AppendTBS v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasAppendTBS() => $_has(1); + @$pb.TagNumber(2) + void clearAppendTBS() => clearField(2); + @$pb.TagNumber(2) + AppendGroupState_AppendTBS ensureAppendTBS() => $_ensure(1); + + @$pb.TagNumber(3) + $fixnum.Int64 get versionId => $_getI64(2); + @$pb.TagNumber(3) + set versionId($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasVersionId() => $_has(2); + @$pb.TagNumber(3) + void clearVersionId() => clearField(3); } class GroupState extends $pb.GeneratedMessage { factory GroupState({ $fixnum.Int64? versionId, $core.List<$core.int>? encryptedGroupState, + $core.Iterable? appendedGroupStates, }) { final $result = create(); if (versionId != null) { @@ -450,6 +623,9 @@ class GroupState extends $pb.GeneratedMessage { if (encryptedGroupState != null) { $result.encryptedGroupState = encryptedGroupState; } + if (appendedGroupStates != null) { + $result.appendedGroupStates.addAll(appendedGroupStates); + } return $result; } GroupState._() : super(); @@ -457,8 +633,9 @@ class GroupState extends $pb.GeneratedMessage { factory GroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) - ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..pc(3, _omitFieldNames ? '' : 'appendedGroupStates', $pb.PbFieldType.PM, subBuilder: AppendGroupState.create) ..hasRequiredFields = false ; @@ -492,14 +669,62 @@ class GroupState extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearVersionId() => clearField(1); - @$pb.TagNumber(3) + @$pb.TagNumber(2) $core.List<$core.int> get encryptedGroupState => $_getN(1); - @$pb.TagNumber(3) + @$pb.TagNumber(2) set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } - @$pb.TagNumber(3) + @$pb.TagNumber(2) $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedGroupState() => clearField(2); + @$pb.TagNumber(3) - void clearEncryptedGroupState() => clearField(3); + $core.List get appendedGroupStates => $_getList(2); +} + +/// this is just a database helper to store multiple appends +class AppendGroupStateHelper extends $pb.GeneratedMessage { + factory AppendGroupStateHelper({ + $core.Iterable? appendedGroupStates, + }) { + final $result = create(); + if (appendedGroupStates != null) { + $result.appendedGroupStates.addAll(appendedGroupStates); + } + return $result; + } + AppendGroupStateHelper._() : super(); + factory AppendGroupStateHelper.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupStateHelper.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupStateHelper', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'appendedGroupStates', $pb.PbFieldType.PM, subBuilder: AppendGroupState.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupStateHelper clone() => AppendGroupStateHelper()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupStateHelper copyWith(void Function(AppendGroupStateHelper) updates) => super.copyWith((message) => updates(message as AppendGroupStateHelper)) as AppendGroupStateHelper; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupStateHelper create() => AppendGroupStateHelper._(); + AppendGroupStateHelper createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupStateHelper getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupStateHelper? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get appendedGroupStates => $_getList(0); } diff --git a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart index be8ef01..c93b93c 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart @@ -89,30 +89,77 @@ final $typed_data.Uint8List updateGroupStateDescriptor = $convert.base64Decode( const NewGroupState$json = { '1': 'NewGroupState', '2': [ - {'1': 'groupId', '3': 1, '4': 1, '5': 9, '10': 'groupId'}, - {'1': 'versionId', '3': 2, '4': 1, '5': 4, '10': 'versionId'}, - {'1': 'encrypted_group_state', '3': 4, '4': 1, '5': 12, '10': 'encryptedGroupState'}, - {'1': 'public_key', '3': 5, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'group_id', '3': 1, '4': 1, '5': 9, '10': 'groupId'}, + {'1': 'version_id', '3': 2, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'public_key', '3': 4, '4': 1, '5': 12, '10': 'publicKey'}, ], }; /// Descriptor for `NewGroupState`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List newGroupStateDescriptor = $convert.base64Decode( - 'Cg1OZXdHcm91cFN0YXRlEhgKB2dyb3VwSWQYASABKAlSB2dyb3VwSWQSHAoJdmVyc2lvbklkGA' - 'IgASgEUgl2ZXJzaW9uSWQSMgoVZW5jcnlwdGVkX2dyb3VwX3N0YXRlGAQgASgMUhNlbmNyeXB0' - 'ZWRHcm91cFN0YXRlEh0KCnB1YmxpY19rZXkYBSABKAxSCXB1YmxpY0tleQ=='); + 'Cg1OZXdHcm91cFN0YXRlEhkKCGdyb3VwX2lkGAEgASgJUgdncm91cElkEh0KCnZlcnNpb25faW' + 'QYAiABKARSCXZlcnNpb25JZBIyChVlbmNyeXB0ZWRfZ3JvdXBfc3RhdGUYAyABKAxSE2VuY3J5' + 'cHRlZEdyb3VwU3RhdGUSHQoKcHVibGljX2tleRgEIAEoDFIJcHVibGljS2V5'); + +@$core.Deprecated('Use appendGroupStateDescriptor instead') +const AppendGroupState$json = { + '1': 'AppendGroupState', + '2': [ + {'1': 'signature', '3': 1, '4': 1, '5': 12, '10': 'signature'}, + {'1': 'appendTBS', '3': 2, '4': 1, '5': 11, '6': '.http_requests.AppendGroupState.AppendTBS', '10': 'appendTBS'}, + {'1': 'versionId', '3': 3, '4': 1, '5': 4, '10': 'versionId'}, + ], + '3': [AppendGroupState_AppendTBS$json], +}; + +@$core.Deprecated('Use appendGroupStateDescriptor instead') +const AppendGroupState_AppendTBS$json = { + '1': 'AppendTBS', + '2': [ + {'1': 'encrypted_group_state_append', '3': 1, '4': 1, '5': 12, '10': 'encryptedGroupStateAppend'}, + {'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'group_id', '3': 3, '4': 1, '5': 9, '10': 'groupId'}, + {'1': 'nonce', '3': 4, '4': 1, '5': 12, '10': 'nonce'}, + ], +}; + +/// Descriptor for `AppendGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List appendGroupStateDescriptor = $convert.base64Decode( + 'ChBBcHBlbmRHcm91cFN0YXRlEhwKCXNpZ25hdHVyZRgBIAEoDFIJc2lnbmF0dXJlEkcKCWFwcG' + 'VuZFRCUxgCIAEoCzIpLmh0dHBfcmVxdWVzdHMuQXBwZW5kR3JvdXBTdGF0ZS5BcHBlbmRUQlNS' + 'CWFwcGVuZFRCUxIcCgl2ZXJzaW9uSWQYAyABKARSCXZlcnNpb25JZBqcAQoJQXBwZW5kVEJTEj' + '8KHGVuY3J5cHRlZF9ncm91cF9zdGF0ZV9hcHBlbmQYASABKAxSGWVuY3J5cHRlZEdyb3VwU3Rh' + 'dGVBcHBlbmQSHQoKcHVibGljX2tleRgCIAEoDFIJcHVibGljS2V5EhkKCGdyb3VwX2lkGAMgAS' + 'gJUgdncm91cElkEhQKBW5vbmNlGAQgASgMUgVub25jZQ=='); @$core.Deprecated('Use groupStateDescriptor instead') const GroupState$json = { '1': 'GroupState', '2': [ - {'1': 'versionId', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, - {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'version_id', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 2, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'appended_group_states', '3': 3, '4': 3, '5': 11, '6': '.http_requests.AppendGroupState', '10': 'appendedGroupStates'}, ], }; /// Descriptor for `GroupState`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List groupStateDescriptor = $convert.base64Decode( - 'CgpHcm91cFN0YXRlEhwKCXZlcnNpb25JZBgBIAEoBFIJdmVyc2lvbklkEjIKFWVuY3J5cHRlZF' - '9ncm91cF9zdGF0ZRgDIAEoDFITZW5jcnlwdGVkR3JvdXBTdGF0ZQ=='); + 'CgpHcm91cFN0YXRlEh0KCnZlcnNpb25faWQYASABKARSCXZlcnNpb25JZBIyChVlbmNyeXB0ZW' + 'RfZ3JvdXBfc3RhdGUYAiABKAxSE2VuY3J5cHRlZEdyb3VwU3RhdGUSUwoVYXBwZW5kZWRfZ3Jv' + 'dXBfc3RhdGVzGAMgAygLMh8uaHR0cF9yZXF1ZXN0cy5BcHBlbmRHcm91cFN0YXRlUhNhcHBlbm' + 'RlZEdyb3VwU3RhdGVz'); + +@$core.Deprecated('Use appendGroupStateHelperDescriptor instead') +const AppendGroupStateHelper$json = { + '1': 'AppendGroupStateHelper', + '2': [ + {'1': 'appended_group_states', '3': 1, '4': 3, '5': 11, '6': '.http_requests.AppendGroupState', '10': 'appendedGroupStates'}, + ], +}; + +/// Descriptor for `AppendGroupStateHelper`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List appendGroupStateHelperDescriptor = $convert.base64Decode( + 'ChZBcHBlbmRHcm91cFN0YXRlSGVscGVyElMKFWFwcGVuZGVkX2dyb3VwX3N0YXRlcxgBIAMoCz' + 'IfLmh0dHBfcmVxdWVzdHMuQXBwZW5kR3JvdXBTdGF0ZVITYXBwZW5kZWRHcm91cFN0YXRlcw=='); diff --git a/lib/src/model/protobuf/client/generated/groups.pb.dart b/lib/src/model/protobuf/client/generated/groups.pb.dart index e3d50f5..1be9b98 100644 --- a/lib/src/model/protobuf/client/generated/groups.pb.dart +++ b/lib/src/model/protobuf/client/generated/groups.pb.dart @@ -14,6 +14,10 @@ import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; +import 'groups.pbenum.dart'; + +export 'groups.pbenum.dart'; + /// Stored encrypted on the server in the members columns. class EncryptedGroupState extends $pb.GeneratedMessage { factory EncryptedGroupState({ @@ -109,6 +113,56 @@ class EncryptedGroupState extends $pb.GeneratedMessage { void clearPadding() => clearField(5); } +class EncryptedAppendedGroupState extends $pb.GeneratedMessage { + factory EncryptedAppendedGroupState({ + EncryptedAppendedGroupState_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + EncryptedAppendedGroupState._() : super(); + factory EncryptedAppendedGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedAppendedGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedAppendedGroupState', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedAppendedGroupState_Type.LEFT_GROUP, valueOf: EncryptedAppendedGroupState_Type.valueOf, enumValues: EncryptedAppendedGroupState_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedAppendedGroupState clone() => EncryptedAppendedGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedAppendedGroupState copyWith(void Function(EncryptedAppendedGroupState) updates) => super.copyWith((message) => updates(message as EncryptedAppendedGroupState)) as EncryptedAppendedGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedAppendedGroupState create() => EncryptedAppendedGroupState._(); + EncryptedAppendedGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedAppendedGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedAppendedGroupState? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedAppendedGroupState_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedAppendedGroupState_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + class EncryptedGroupStateEnvelop extends $pb.GeneratedMessage { factory EncryptedGroupStateEnvelop({ $core.List<$core.int>? nonce, diff --git a/lib/src/model/protobuf/client/generated/groups.pbenum.dart b/lib/src/model/protobuf/client/generated/groups.pbenum.dart index c03fb19..69a0e68 100644 --- a/lib/src/model/protobuf/client/generated/groups.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/groups.pbenum.dart @@ -9,3 +9,22 @@ // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class EncryptedAppendedGroupState_Type extends $pb.ProtobufEnum { + static const EncryptedAppendedGroupState_Type LEFT_GROUP = EncryptedAppendedGroupState_Type._(0, _omitEnumNames ? '' : 'LEFT_GROUP'); + + static const $core.List values = [ + LEFT_GROUP, + ]; + + static final $core.Map<$core.int, EncryptedAppendedGroupState_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedAppendedGroupState_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedAppendedGroupState_Type._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/groups.pbjson.dart b/lib/src/model/protobuf/client/generated/groups.pbjson.dart index 97fd3f8..1d81d71 100644 --- a/lib/src/model/protobuf/client/generated/groups.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/groups.pbjson.dart @@ -36,6 +36,28 @@ final $typed_data.Uint8List encryptedGroupStateDescriptor = $convert.base64Decod 'VzQWZ0ZXJNaWxsaXNlY29uZHOIAQESGAoHcGFkZGluZxgFIAEoDFIHcGFkZGluZ0IiCiBfZGVs' 'ZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcw=='); +@$core.Deprecated('Use encryptedAppendedGroupStateDescriptor instead') +const EncryptedAppendedGroupState$json = { + '1': 'EncryptedAppendedGroupState', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedAppendedGroupState.Type', '10': 'type'}, + ], + '4': [EncryptedAppendedGroupState_Type$json], +}; + +@$core.Deprecated('Use encryptedAppendedGroupStateDescriptor instead') +const EncryptedAppendedGroupState_Type$json = { + '1': 'Type', + '2': [ + {'1': 'LEFT_GROUP', '2': 0}, + ], +}; + +/// Descriptor for `EncryptedAppendedGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedAppendedGroupStateDescriptor = $convert.base64Decode( + 'ChtFbmNyeXB0ZWRBcHBlbmRlZEdyb3VwU3RhdGUSNQoEdHlwZRgBIAEoDjIhLkVuY3J5cHRlZE' + 'FwcGVuZGVkR3JvdXBTdGF0ZS5UeXBlUgR0eXBlIhYKBFR5cGUSDgoKTEVGVF9HUk9VUBAA'); + @$core.Deprecated('Use encryptedGroupStateEnvelopDescriptor instead') const EncryptedGroupStateEnvelop$json = { '1': 'EncryptedGroupStateEnvelop', diff --git a/lib/src/model/protobuf/client/groups.proto b/lib/src/model/protobuf/client/groups.proto index 813563c..268ad1f 100644 --- a/lib/src/model/protobuf/client/groups.proto +++ b/lib/src/model/protobuf/client/groups.proto @@ -9,6 +9,13 @@ message EncryptedGroupState { bytes padding = 5; } +message EncryptedAppendedGroupState { + enum Type { + LEFT_GROUP = 0; + } + Type type = 1; +} + message EncryptedGroupStateEnvelop { bytes nonce = 1; bytes encryptedGroupState = 2; diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index e0c0b5f..ca5b906 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -57,6 +57,7 @@ Future handleGroupCreate( groupName: const Value(''), joinedGroup: const Value(false), leftGroup: const Value(false), + deletedContent: const Value(false), ), ); } diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index c034afb..3c3f5de 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; -import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; import 'package:hashlib/random.dart'; import 'package:http/http.dart' as http; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; @@ -170,32 +170,13 @@ Future fetchMissingGroupPublicKey() async { } } -Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { - if (group.leftGroup) { - Log.error( - 'Could not refresh group state, as user is no longer part of the group', - ); - return null; - } +Future?> _decryptEnvelop( + Group group, + List encryptedGroupState, +) async { try { - var isSuccess = true; - - final response = await http - .get( - Uri.parse('${getGroupStateUrl()}/${group.groupId}'), - ) - .timeout(const Duration(seconds: 10)); - - if (response.statusCode != 200) { - Log.error( - 'Could not load group state. Got status code ${response.statusCode} from server.', - ); - return null; - } - - final groupStateServer = GroupState.fromBuffer(response.bodyBytes); final envelope = EncryptedGroupStateEnvelop.fromBuffer( - groupStateServer.encryptedGroupState, + encryptedGroupState, ); final chacha20 = FlutterChacha20.poly1305Aead(); @@ -210,19 +191,111 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { secretKey: SecretKey(group.stateEncryptionKey!), ); + return encryptedGroupStateRaw; + } catch (e) { + Log.error(e); + return null; + } +} + +Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { + try { + var isSuccess = true; + + final response = await http + .get( + Uri.parse('${getGroupStateUrl()}/${group.groupId}'), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + if (response.statusCode == 404) { + // group does not exists any more. + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + leftGroup: Value(true), + ), + ); + } + Log.error( + 'Could not load group state. Got status code ${response.statusCode} from server.', + ); + return null; + } + + final groupStateServer = GroupState.fromBuffer(response.bodyBytes); + + final encryptedStateRaw = + await _decryptEnvelop(group, groupStateServer.encryptedGroupState); + if (encryptedStateRaw == null) return null; + final encryptedGroupState = - EncryptedGroupState.fromBuffer(encryptedGroupStateRaw); + EncryptedGroupState.fromBuffer(encryptedStateRaw); if (group.stateVersionId >= groupStateServer.versionId.toInt()) { Log.info( 'Group ${group.groupId} has already newest group state from the server!', ); - // return (groupStateServer.versionId.toInt(), encryptedGroupState); } - if (!encryptedGroupState.memberIds.contains(Int64(gUser.userId))) { + final memberIds = List.from(encryptedGroupState.memberIds); + final adminIds = List.from(encryptedGroupState.adminIds); + + for (final appendedState in groupStateServer.appendedGroupStates) { + final identityKey = IdentityKey.fromBytes( + Uint8List.fromList(appendedState.appendTBS.publicKey), + 0, + ); + + final valid = Curve.verifySignature( + identityKey.publicKey, + appendedState.appendTBS.writeToBuffer(), + Uint8List.fromList(appendedState.signature), + ); + + if (!valid) { + Log.error('Invalid signature for the appendedState'); + continue; + } + + final encryptedStateRaw = await _decryptEnvelop( + group, + appendedState.appendTBS.encryptedGroupStateAppend, + ); + if (encryptedStateRaw == null) continue; + + final appended = + EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw); + if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) { + final keyPair = + IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final appendedPubKey = appendedState.appendTBS.publicKey; + final myPubKey = keyPair.getPublicKey().serialize().toList(); + + if (listEquals(appendedPubKey, myPubKey)) { + adminIds.remove(Int64(gUser.userId)); + memberIds + .remove(Int64(gUser.userId)); // -> Will remove the user later... + } else { + Log.info('A non admin left the group!!!'); + + final member = await twonlyDB.groupsDao + .getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey)); + if (member == null) { + Log.error('Member is already not in this group...'); + continue; + } + adminIds.remove(Int64(member.contactId)); + memberIds.remove(Int64(member.contactId)); + } + } + } + + if (!memberIds.contains(Int64(gUser.userId))) { // OH no, I am no longer a member of this group... - // -> + // Return from the group... await twonlyDB.groupsDao.updateGroup( group.groupId, const GroupsCompanion( @@ -232,9 +305,41 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { return (groupStateServer.versionId.toInt(), encryptedGroupState); } - final isGroupAdmin = encryptedGroupState.adminIds - .firstWhereOrNull((t) => t.toInt() == gUser.userId) != - null; + final isGroupAdmin = + adminIds.firstWhereOrNull((t) => t.toInt() == gUser.userId) != null; + + if (!listEquals(memberIds, encryptedGroupState.memberIds)) { + if (isGroupAdmin) { + try { + // this removes the appended_group_state from the server and merges the changes into the main group state + final newState = EncryptedGroupState( + groupName: encryptedGroupState.groupName, + deleteMessagesAfterMilliseconds: + encryptedGroupState.deleteMessagesAfterMilliseconds, + memberIds: memberIds, + adminIds: adminIds, + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + // send new state to the server + if (!await _updateGroupState( + group, + newState, + versionId: groupStateServer.versionId.toInt() + 1, + )) { + // could not update the group state... + Log.error('Update the state to remove the appended state...'); + return null; + } + // the state is now updated and the appended_group_state should be removed on the server, so just call this + // function again, to sync the local database + return fetchGroupState(group); + } catch (e) { + Log.error(e); + return null; + } + } + // in case this is not an admin, just work with the new memberIds and adminIds... + } await twonlyDB.groupsDao.updateGroup( group.groupId, @@ -251,7 +356,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { await twonlyDB.groupsDao.getGroupMembers(group.groupId); // First find and insert NEW members - for (final memberId in encryptedGroupState.memberIds) { + for (final memberId in memberIds) { if (memberId == Int64(gUser.userId)) { continue; } @@ -313,7 +418,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { MemberState? newMemberState; - if (encryptedGroupState.adminIds.contains(Int64(member.contactId))) { + if (adminIds.contains(Int64(member.contactId))) { if (member.memberState == MemberState.normal) { // user was promoted newMemberState = MemberState.admin; @@ -376,6 +481,7 @@ Future _updateGroupState( EncryptedGroupState state, { Uint8List? addAdmin, Uint8List? removeAdmin, + int? versionId, }) async { final chacha20 = FlutterChacha20.poly1305Aead(); final encryptionNonce = chacha20.newNonce(); @@ -397,26 +503,14 @@ Future _updateGroupState( final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); - final publicKey = uint8ListToHex(keyPair.getPublicKey().serialize()); - - final responseNonce = await http - .get( - Uri.parse('${getGroupChallengeUrl()}/$publicKey'), - ) - .timeout(const Duration(seconds: 10)); - - if (responseNonce.statusCode != 200) { - Log.error( - 'Could not load nonce. Got status code ${responseNonce.statusCode} from server.', - ); - return false; - } + final nonce = await getNonce(keyPair.getPublicKey().serialize()); + if (nonce == null) return false; final updateTBS = UpdateGroupState_UpdateTBS( - versionId: Int64(group.stateVersionId + 1), + versionId: Int64(versionId ?? group.stateVersionId + 1), encryptedGroupState: encryptedGroupState.writeToBuffer(), publicKey: keyPair.getPublicKey().serialize(), - nonce: responseNonce.bodyBytes, + nonce: nonce, addAdmin: addAdmin, removeAdmin: removeAdmin, ); @@ -452,7 +546,7 @@ Future _updateGroupState( Future manageAdminState( Group group, - GroupMember member, + Uint8List groupPublicKey, int contactId, bool remove, ) async { @@ -469,7 +563,7 @@ Future manageAdminState( if (remove) { if (state.adminIds.contains(userId)) { state.adminIds.remove(userId); - removeAdmin = member.groupPublicKey; + removeAdmin = groupPublicKey; } else { Log.info('User was already removed as admin.'); return true; @@ -477,7 +571,7 @@ Future manageAdminState( } else { if (!state.adminIds.contains(userId)) { state.adminIds.add(userId); - addAdmin = member.groupPublicKey; + addAdmin = groupPublicKey; } else { Log.info('User is already admin.'); return true; @@ -524,7 +618,7 @@ Future manageAdminState( return (await fetchGroupState(group)) != null; } -Future updateGroupeName(Group group, String groupName) async { +Future updateGroupName(Group group, String groupName) async { // ensure the latest state is used final currentState = await fetchGroupState(group); if (currentState == null) return false; @@ -624,7 +718,7 @@ Future addNewGroupMembers( Future removeMemberFromGroup( Group group, - GroupMember member, + Uint8List groupPublicKey, int removeContactId, ) async { // ensure the latest state is used @@ -642,16 +736,16 @@ Future removeMemberFromGroup( return true; } if (adminIdSet.contains(contactId)) { - if (member.groupPublicKey == null) { - // If the admin public key is not removed, that the user could potentially still update the group state. So only - // allow the user removal, if this key is known. It is better the users can not remove the other user, then - // the he can but the other user, could still update the group state. - Log.error( - 'Could not remove user. User is admin, but groupPublicKey is unknown.', - ); - return false; - } - removeAdmin = member.groupPublicKey; + // if (member.groupPublicKey == null) { + // // If the admin public key is not removed, that the user could potentially still update the group state. So only + // // allow the user removal, if this key is known. It is better the users can not remove the other user, then + // // the he can but the other user, could still update the group state. + // Log.error( + // 'Could not remove user. User is admin, but groupPublicKey is unknown.', + // ); + // return false; + // } + removeAdmin = groupPublicKey; } membersIdSet.remove(contactId); @@ -684,10 +778,126 @@ Future removeMemberFromGroup( GroupHistoriesCompanion( groupId: Value(group.groupId), type: const Value(GroupActionType.removedMember), - affectedContactId: Value(removeContactId), + affectedContactId: Value( + removeContactId == gUser.userId ? null : removeContactId, + ), ), ); // Updates the groupMembers table :) return (await fetchGroupState(group)) != null; } + +Future getNonce(Uint8List publicKey) async { + final publicKeyHex = uint8ListToHex(publicKey); + + final responseNonce = await http + .get( + Uri.parse('${getGroupChallengeUrl()}/$publicKeyHex'), + ) + .timeout(const Duration(seconds: 10)); + + if (responseNonce.statusCode != 200) { + Log.error( + 'Could not load nonce. Got status code ${responseNonce.statusCode} from server.', + ); + return null; + } + return responseNonce.bodyBytes; +} + +Future leaveAsNonAdminFromGroup(Group group) async { + final currentState = await fetchGroupState(group); + if (currentState == null) { + Log.error('Could not load current state'); + return false; + } + + final (version, _) = currentState; + if (group.stateVersionId != version) { + Log.error('Version is not valid. Just retry.'); + return false; + } + + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final state = EncryptedAppendedGroupState( + type: EncryptedAppendedGroupState_Type.LEFT_GROUP, + ); + + final secretBox = await chacha20.encrypt( + state.writeToBuffer(), + secretKey: SecretKey(group.stateEncryptionKey!), + nonce: encryptionNonce, + ); + + final encryptedGroupStateAppend = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + { + // Upload the group state, if this fails, the group can not be created. + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final nonce = await getNonce(keyPair.getPublicKey().serialize()); + if (nonce == null) return false; + + final appendTBS = AppendGroupState_AppendTBS( + publicKey: keyPair.getPublicKey().serialize(), + encryptedGroupStateAppend: encryptedGroupStateAppend.writeToBuffer(), + groupId: group.groupId, + nonce: nonce, + ); + + final random = getRandomUint8List(32); + final signature = sign( + keyPair.getPrivateKey().serialize(), + appendTBS.writeToBuffer(), + random, + ); + + final newGroupState = AppendGroupState( + versionId: Int64(group.stateVersionId + 1), + appendTBS: appendTBS, + signature: signature, + ); + + final response = await http + .post( + Uri.parse('${getGroupStateUrl()}/append'), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not patch group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + const groupActionType = GroupActionType.leftGroup; + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: groupActionType.name, + affectedContactId: Int64(gUser.userId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(groupActionType), + ), + ); + + // Updates the table :) + return (await fetchGroupState(group)) != null; +} diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index 50fb542..45aadfa 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -20,7 +20,7 @@ class BetterListTile extends StatelessWidget { final String text; final Widget? subtitle; final Color? color; - final VoidCallback onTap; + final VoidCallback? onTap; final double iconSize; final EdgeInsets? padding; diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index a16a2e7..851d5ca 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; @@ -74,7 +76,7 @@ class _GroupViewState extends State { newGroupName != null && newGroupName != '' && newGroupName != group.groupName) { - if (!await updateGroupeName(group, newGroupName)) { + if (!await updateGroupName(group, newGroupName)) { if (mounted) { showNetworkIssue(context); } @@ -97,6 +99,60 @@ class _GroupViewState extends State { } } + Future _leaveGroup() async { + final ok = await showAlertDialog( + context, + context.lang.leaveGroupSureTitle, + context.lang.leaveGroupSureBody, + customOk: context.lang.leaveGroupSureOkBtn, + ); + if (!ok) return; + + // 1. Check if I am the only admin, while there are still normal members + // -> ERROR first select new admin + + if (members.isNotEmpty) { + // In case there are other members, check that there is at least one other admin before I leave the group. + + if (group.isGroupAdmin) { + if (!members.any((m) => m.$2.memberState == MemberState.admin)) { + if (!mounted) return; + await showAlertDialog( + context, + context.lang.leaveGroupSelectOtherAdminTitle, + context.lang.leaveGroupSelectOtherAdminBody, + customCancel: '', + ); + return; + } + } + } + + late bool success; + + if (group.isGroupAdmin) { + // Current user is a admin, to the state can be updated by the user him self. + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + success = await removeMemberFromGroup( + group, + keyPair.getPublicKey().serialize(), + gUser.userId, + ); + } else { + success = await leaveAsNonAdminFromGroup(group); + } + + if (!success) { + if (mounted) { + showNetworkIssue(context); + return; + } + } + // If not admin -> append to the server state + + // -> Inform the other users + } + @override Widget build(BuildContext context) { return Scaffold( @@ -126,7 +182,7 @@ class _GroupViewState extends State { ], ), const SizedBox(height: 50), - if (group.isGroupAdmin) + if (group.isGroupAdmin && !group.leftGroup) BetterListTile( icon: FontAwesomeIcons.pencil, text: context.lang.groupNameInput, @@ -145,7 +201,7 @@ class _GroupViewState extends State { ), ), ), - if (group.isGroupAdmin) + if (group.isGroupAdmin && !group.leftGroup) BetterListTile( icon: FontAwesomeIcons.plus, text: context.lang.addMember, @@ -197,12 +253,25 @@ class _GroupViewState extends State { const SizedBox(height: 10), const Divider(), const SizedBox(height: 10), - BetterListTile( - icon: FontAwesomeIcons.rightFromBracket, - color: Colors.red, - text: context.lang.leaveGroup, - onTap: () => {}, - ), + if (!group.leftGroup) + BetterListTile( + icon: FontAwesomeIcons.rightFromBracket, + color: Colors.red, + text: context.lang.leaveGroup, + onTap: _leaveGroup, + ) + else + ListTile( + title: Padding( + padding: const EdgeInsets.only(left: 17), + child: Text( + context.lang.groupYouAreNowLongerAMember, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ), ], ), ); @@ -246,9 +315,9 @@ Future showGroupNameChangeDialog( void showNetworkIssue(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Network issue. Try again later.'), - duration: Duration(seconds: 3), + SnackBar( + content: Text(context.lang.groupNetworkIssue), + duration: const Duration(seconds: 3), ), ); } diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart index 08084e1..687401f 100644 --- a/lib/src/views/groups/group_member.context.dart +++ b/lib/src/views/groups/group_member.context.dart @@ -37,7 +37,7 @@ class GroupMemberContextMenu extends StatelessWidget { if (ok) { if (!await manageAdminState( group, - member, + member.groupPublicKey!, contact.userId, false, )) { @@ -58,7 +58,7 @@ class GroupMemberContextMenu extends StatelessWidget { if (ok) { if (!await manageAdminState( group, - member, + member.groupPublicKey!, contact.userId, true, )) { @@ -78,7 +78,7 @@ class GroupMemberContextMenu extends StatelessWidget { if (ok) { if (!await removeMemberFromGroup( group, - member, + member.groupPublicKey!, contact.userId, )) { if (context.mounted) { @@ -159,7 +159,7 @@ class GroupMemberContextMenu extends StatelessWidget { onTap: () => _removeContactAsAdmin(context), icon: FontAwesomeIcons.key, ), - if (group.isGroupAdmin) + if (group.isGroupAdmin && member.groupPublicKey != null) ContextMenuItem( title: context.lang.removeFromGroup, onTap: () => _removeContactFromGroup(context), diff --git a/test/unit_test.dart b/test/unit_test.dart index dafea36..bf6ffea 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,6 +1,9 @@ import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -60,5 +63,20 @@ void main() { test('Reject values > 0x7fffffff', () { expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError); }); + + test('sign and verify', () { + final keyPair = generateIdentityKeyPair(); + final message = Uint8List(10); + + final random = getRandomUint8List(32); + + final signature = + sign(keyPair.getPrivateKey().serialize(), message, random); + + expect( + verifySig(keyPair.getPublicKey().serialize(), message, signature), + true, + ); + }); }); } From 57334d9eeecc60ac4aadf7d07254115fe42b0cd0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 4 Nov 2025 14:00:21 +0100 Subject: [PATCH 61/76] fixes #290 --- .../src/main/kotlin/eu/twonly/MainActivity.kt | 18 ++++- ios/Podfile.lock | 12 +++ lib/src/services/api/server_messages.dart | 24 +++--- .../camera_preview_controller_view.dart | 78 +++++++++++++++++-- lib/src/views/camera/camera_send_to_view.dart | 1 + lib/src/views/chats/chat_messages.view.dart | 71 +++++++++++++---- lib/src/views/home.view.dart | 2 + pubspec.lock | 32 ++++++++ pubspec.yaml | 3 + 9 files changed, 207 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt index ce8739d..bb90f69 100644 --- a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt +++ b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt @@ -1,5 +1,21 @@ package eu.twonly import io.flutter.embedding.android.FlutterFragmentActivity +import android.view.KeyEvent +import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink +import android.view.KeyEvent.KEYCODE_VOLUME_DOWN +import android.view.KeyEvent.KEYCODE_VOLUME_UP -class MainActivity: FlutterFragmentActivity() +class MainActivity : FlutterFragmentActivity() { + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) { + eventSink!!.success(true) + return true + } + if (keyCode == KEYCODE_VOLUME_UP && eventSink != null) { + eventSink!!.success(false) + return true + } + return super.onKeyDown(keyCode, event) + } +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 440ffc5..3c00a85 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,8 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - emoji_picker_flutter (0.0.1): + - Flutter - ffmpeg_kit_flutter_new (1.0.0): - ffmpeg_kit_flutter_new/full-gpl (= 1.0.0) - Flutter @@ -82,6 +84,8 @@ PODS: - flutter_secure_storage_darwin (10.0.0): - Flutter - FlutterMacOS + - flutter_volume_controller (0.0.1): + - Flutter - flutter_zxing (0.0.1): - Flutter - gal (1.0.0): @@ -248,6 +252,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - 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`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -260,6 +265,7 @@ DEPENDENCIES: - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_zxing (from `.symlinks/plugins/flutter_zxing/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - GoogleUtilities @@ -311,6 +317,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cryptography_flutter_plus/ios" device_info_plus: :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" firebase_core: @@ -327,6 +335,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage_darwin: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + flutter_volume_controller: + :path: ".symlinks/plugins/flutter_volume_controller/ios" flutter_zxing: :path: ".symlinks/plugins/flutter_zxing/ios" gal: @@ -364,6 +374,7 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 @@ -378,6 +389,7 @@ SPEC CHECKSUMS: flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468 + flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93 gal: baecd024ebfd13c441269ca7404792a7152fde89 GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1 diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 1d85e4f..58c37f8 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -32,19 +32,19 @@ Future handleServerMessage(server.ServerToClient msg) async { 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()) { - final body = Uint8List.fromList(msg.v0.newMessage.body); - final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - await handleClient2ClientMessage(fromUserId, body); - } else { - Log.error('Unknown server message: $msg'); + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + final body = Uint8List.fromList(msg.v0.newMessage.body); + final fromUserId = msg.v0.newMessage.fromUserId.toInt(); + await handleClient2ClientMessage(fromUserId, body); + } 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 diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 176d6b3..5a4a93c 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; +import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -88,6 +90,7 @@ class CameraPreviewControllerView extends StatelessWidget { required this.selectCamera, required this.selectedCameraDetails, required this.screenshotController, + required this.isVisible, super.key, this.sendToGroup, }); @@ -97,6 +100,7 @@ class CameraPreviewControllerView extends StatelessWidget { final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; + final bool isVisible; @override Widget build(BuildContext context) { @@ -111,6 +115,7 @@ class CameraPreviewControllerView extends StatelessWidget { cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, + isVisible: isVisible, ); } else { return PermissionHandlerView( @@ -133,6 +138,7 @@ class CameraPreviewView extends StatefulWidget { required this.cameraController, required this.selectedCameraDetails, required this.screenshotController, + required this.isVisible, super.key, this.sendToGroup, }); @@ -144,6 +150,7 @@ class CameraPreviewView extends StatefulWidget { final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; + final bool isVisible; @override State createState() => _CameraPreviewViewState(); @@ -164,12 +171,71 @@ class _CameraPreviewViewState extends State { final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); + StreamSubscription? androidVolumeDownSub; + @override void initState() { super.initState(); + initVolumeControl(); initAsync(); } + @override + void didUpdateWidget(covariant CameraPreviewView oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isVisible != widget.isVisible) { + if (widget.isVisible) { + initVolumeControl(); + } else { + deInitVolumeControl(); + } + } + } + + @override + void dispose() { + _videoRecordingTimer?.cancel(); + deInitVolumeControl(); + super.dispose(); + } + + Future initVolumeControl() async { + if (Platform.isIOS) { + await FlutterVolumeController.updateShowSystemUI(false); + double? startedVolume; + + FlutterVolumeController.addListener( + (volume) async { + if (startedVolume == null) { + startedVolume = volume; + return; + } + if (startedVolume == volume) { + return; + } + // reset the volume back to the original value + await FlutterVolumeController.setVolume(startedVolume!); + await takePicture(); + }, + ); + } + if (Platform.isAndroid) { + androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) { + takePicture(); + }); + } + } + + Future deInitVolumeControl() async { + if (Platform.isIOS) { + await FlutterVolumeController.updateShowSystemUI(true); + FlutterVolumeController.removeListener(); + } + if (Platform.isAndroid) { + await androidVolumeDownSub?.cancel(); + } + } + Future initAsync() async { _hasAudioPermission = await Permission.microphone.isGranted; @@ -184,12 +250,6 @@ class _CameraPreviewViewState extends State { setState(() {}); } - @override - void dispose() { - _videoRecordingTimer?.cancel(); - super.dispose(); - } - Future requestMicrophonePermission() async { final statuses = await [ Permission.microphone, @@ -309,6 +369,9 @@ class _CameraPreviewViewState extends State { // unawaited(mediaFileService.compressMedia()); } + await deInitVolumeControl(); + if (!mounted) return true; + final shouldReturn = await Navigator.push( context, PageRouteBuilder( @@ -333,11 +396,12 @@ class _CameraPreviewViewState extends State { }); } if (!mounted) return true; + await initVolumeControl(); // shouldReturn is null when the user used the back button if (shouldReturn != null && shouldReturn) { if (widget.sendToGroup == null) { globalUpdateOfHomeViewPageIndex(0); - } else { + } else if (mounted) { Navigator.pop(context); } return true; diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index cb36407..578c955 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -79,6 +79,7 @@ class CameraSendToViewState extends State { cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, + isVisible: true, ), ], ), diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index f6e9593..da3bc96 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -474,20 +474,63 @@ class _ChatMessagesViewState extends State { child: Row( children: [ Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), + child: Container( + color: Colors.grey, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + child: Row( + children: [ + const FaIcon(FontAwesomeIcons.faceSmile), + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + // contentPadding: const EdgeInsets.symmetric( + // horizontal: 20, + // vertical: 10, + // ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide( + color: Theme.of(context) + .colorScheme + .primary, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: const BorderSide( + color: Colors.grey, + width: 2, + ), + ), + ), + ), + ), + ], + ), ), ), if (currentInputText != '') diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 7571421..ee2907d 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -193,6 +193,8 @@ class HomeViewState extends State { screenshotController: screenshotController, selectedCameraDetails: selectedCameraDetails, selectCamera: selectCamera, + isVisible: + ((1 - (offsetRatio * 4) % 1) == 1) && activePageIdx == 1, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index eb37e53..b1d0c98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -386,6 +386,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d" + url: "https://pub.dev" + source: hosted + version: "4.3.0" fake_async: dependency: transitive description: @@ -519,6 +527,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_android_volume_keydown: + dependency: "direct main" + description: + name: flutter_android_volume_keydown + sha256: bf7fed0be85541b939d9deb97b375cb12e6e703aa013754441318b0b9014e711 + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_cache_manager: dependency: transitive description: @@ -738,6 +754,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_volume_controller: + dependency: "direct main" + description: + name: flutter_volume_controller + sha256: "22edb0993ad03ecbc8d1164daeb5b39d798d409625db692675a86889403b1532" + url: "https://pub.dev" + source: hosted + version: "1.3.4" flutter_web_plugins: dependency: transitive description: flutter @@ -1675,6 +1699,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2984317..352eb50 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,12 +20,14 @@ dependencies: device_info_plus: ^12.1.0 drift: ^2.25.1 drift_flutter: ^0.2.4 + emoji_picker_flutter: ^4.3.0 ffmpeg_kit_flutter_new: ^4.1.0 firebase_core: ^4.2.0 firebase_messaging: ^16.0.3 fixnum: ^1.1.1 flutter: sdk: flutter + flutter_android_volume_keydown: ^1.0.1 flutter_image_compress: ^2.4.0 flutter_local_notifications: ^19.1.0 flutter_localizations: @@ -36,6 +38,7 @@ dependencies: ref: 71b75a36f35f2ce945998e20c6c6aa1820babfc6 # from develop path: flutter_secure_storage/ flutter_svg: ^2.0.17 + flutter_volume_controller: ^1.3.4 flutter_zxing: path: ./dependencies/flutter_zxing font_awesome_flutter: ^10.10.0 From 94f60b5806d815d4db7a7dd126230851a68045d6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 4 Nov 2025 21:32:43 +0100 Subject: [PATCH 62/76] fix #289 --- CHANGELOG.md | 2 + lib/app.dart | 2 + lib/src/model/json/userdata.dart | 2 - lib/src/model/json/userdata.g.dart | 4 - .../image_editor/layers/text_layer.dart | 26 ++- .../image_editor/modules/all_emojis.dart | 138 +++++------ .../views/camera/share_image_editor_view.dart | 2 +- lib/src/views/chats/chat_messages.view.dart | 123 +--------- .../message_context_menu.dart | 2 +- .../message_input.dart | 216 ++++++++++++++++++ 10 files changed, 309 insertions(+), 208 deletions(-) create mode 100644 lib/src/views/chats/chat_messages_components/message_input.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a6183..2b9e6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ - Support for groups - Edit & Delete messages - Switched to FFmpeg for improved video compression +- Create images using volume buttons - Video max. length increased to 60 seconds +- New and improved emoji picker - Removing audio after recording is possible - Edited image is now embedded into the video - New context menu and other UI enhancements diff --git a/lib/app.dart b/lib/app.dart index 0025085..5b430a7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -118,6 +118,8 @@ class _AppState extends State with WidgetsBindingObserver { colorScheme: ColorScheme.fromSeed( brightness: Brightness.dark, seedColor: const Color(0xFF57CC99), + surface: const Color.fromARGB(255, 20, 18, 23), + surfaceContainer: const Color.fromARGB(255, 33, 30, 39), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 2a921f3..604c273 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -64,8 +64,6 @@ class UserData { @JsonKey(defaultValue: false) bool storeMediaFilesInGallery = false; - List? lastUsedEditorEmojis; - String? lastPlanBallance; String? additionalUserInvites; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 17cf5cf..6de5cbd 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -40,9 +40,6 @@ UserData _$UserDataFromJson(Map json) => UserData( ) ..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool? ?? false - ..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List?) - ?.map((e) => e as String) - .toList() ..lastPlanBallance = json['lastPlanBallance'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String? ..tutorialDisplayed = (json['tutorialDisplayed'] as List?) @@ -93,7 +90,6 @@ Map _$UserDataToJson(UserData instance) => { 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, - 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastPlanBallance': instance.lastPlanBallance, 'additionalUserInvites': instance.additionalUserInvites, 'tutorialDisplayed': instance.tutorialDisplayed, diff --git a/lib/src/views/camera/image_editor/layers/text_layer.dart b/lib/src/views/camera/image_editor/layers/text_layer.dart index 5cff596..72dc428 100755 --- a/lib/src/views/camera/image_editor/layers/text_layer.dart +++ b/lib/src/views/camera/image_editor/layers/text_layer.dart @@ -24,6 +24,7 @@ class TextLayer extends StatefulWidget { class _TextViewState extends State { double initialRotation = 0; bool deleteLayer = false; + double localBottom = 0; bool isDeleted = false; bool elementIsScaled = false; final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey @@ -35,9 +36,19 @@ class _TextViewState extends State { textController.text = widget.layerData.text; - if (widget.layerData.offset.dy == 0) { - // Set the initial offset to the center of the screen - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final mq = MediaQuery.of(context); + final globalDesiredBottom = mq.viewInsets.bottom + mq.viewPadding.bottom; + final parentBox = context.findRenderObject() as RenderBox?; + if (parentBox != null) { + final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy; + final screenHeight = mq.size.height; + localBottom = (screenHeight - globalDesiredBottom) - + parentTopGlobal - + (parentBox.size.height); + } + + if (widget.layerData.offset.dy == 0) { setState(() { widget.layerData.offset = Offset( 0, @@ -47,17 +58,20 @@ class _TextViewState extends State { ); textController.text = widget.layerData.text; }); - }); - } + } + }); } @override Widget build(BuildContext context) { if (widget.layerData.isDeleted) return Container(); + final bottom = MediaQuery.of(context).viewInsets.bottom + + MediaQuery.of(context).viewPadding.bottom; + if (widget.layerData.isEditing) { return Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom - 100, + bottom: bottom - localBottom, left: 0, right: 0, child: Container( diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index 423342a..3ab1180 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -1,107 +1,83 @@ -import 'dart:async'; - +import 'dart:io'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/image_editor/data/data.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -class Emojis extends StatefulWidget { - const Emojis({super.key}); - - @override - State createState() => _EmojisState(); -} - -class _EmojisState extends State { - List lastUsed = emojis; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - setState(() { - lastUsed = gUser.lastUsedEditorEmojis ?? []; - lastUsed.addAll(emojis); - }); - } - - Future selectEmojis(String emoji) async { - await updateUserdata((user) { - if (user.lastUsedEditorEmojis == null) { - user.lastUsedEditorEmojis = [emoji]; - } else { - if (user.lastUsedEditorEmojis!.contains(emoji)) { - user.lastUsedEditorEmojis!.remove(emoji); - } - user.lastUsedEditorEmojis!.insert(0, emoji); - if (user.lastUsedEditorEmojis!.length > 12) { - user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12); - } - user.lastUsedEditorEmojis!.toSet().toList(); - } - return user; - }); - if (!mounted) return; - Navigator.pop( - context, - EmojiLayerData( - text: emoji, - ), - ); - } +class EmojiPickerBottom extends StatelessWidget { + const EmojiPickerBottom({super.key}); @override Widget build(BuildContext context) { return SingleChildScrollView( child: Container( padding: EdgeInsets.zero, - height: 400, - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(32), topRight: Radius.circular(32), ), - color: Colors.black, + color: context.color.surfaceContainer, boxShadow: [ BoxShadow( blurRadius: 10.9, - color: Color.fromRGBO(0, 0, 0, 0.1), + color: context.color.surfaceContainer.withAlpha(25), ), ], ), child: Column( children: [ - const SizedBox(height: 16), Container( - height: 315, - padding: EdgeInsets.zero, - child: GridView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 60, - ), - children: lastUsed.map((String emoji) { - return GridTile( - child: GestureDetector( - onTap: () async { - await selectEmojis(emoji); - }, - child: Container( - padding: EdgeInsets.zero, - alignment: Alignment.center, - child: Text( - emoji, - style: const TextStyle(fontSize: 35), - ), - ), + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + Navigator.pop( + context, + EmojiLayerData( + text: emoji.emoji, ), ); - }).toList(), + }, + // textEditingController: _textFieldController, + config: Config( + height: 400, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), ), ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d1b0112..bfa5c3b 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -167,7 +167,7 @@ class _ShareImageEditorView extends State { context: context, backgroundColor: Colors.black, builder: (BuildContext context) { - return const Emojis(); + return const EmojiPickerBottom(); }, ) as Layer?; if (layer == null) return; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index da3bc96..5b48640 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -11,11 +11,10 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -70,10 +69,8 @@ class ChatMessagesView extends StatefulWidget { } class _ChatMessagesViewState extends State { - TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); late Group group; - String currentInputText = ''; late StreamSubscription userSub; late StreamSubscription> messageSub; StreamSubscription>? groupActionsSub; @@ -267,21 +264,6 @@ class _ChatMessagesViewState extends State { setState(() {}); } - Future _sendMessage() async { - if (newMessageController.text == '') return; - - await insertAndSendTextMessage( - group.groupId, - newMessageController.text, - quotesMessage?.messageId, - ); - - newMessageController.clear(); - currentInputText = ''; - quotesMessage = null; - setState(() {}); - } - Future scrollToMessage(String messageId) async { final index = messages.indexWhere( (x) => x.isMessage && x.message!.messageId == messageId, @@ -464,100 +446,15 @@ class _ChatMessagesViewState extends State { ), ), if (!group.leftGroup) - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: Container( - color: Colors.grey, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ), - ), - child: Row( - children: [ - const FaIcon(FontAwesomeIcons.faceSmile), - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - // contentPadding: const EdgeInsets.symmetric( - // horizontal: 20, - // vertical: 10, - // ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: BorderSide( - color: Theme.of(context) - .colorScheme - .primary, - width: 2, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: const BorderSide( - color: Colors.grey, - width: 2, - ), - ), - ), - ), - ), - ], - ), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, - ), - ], - ), + MessageInput( + group: group, + quotesMessage: quotesMessage, + textFieldFocus: textFieldFocus, + onMessageSend: () { + setState(() { + quotesMessage = null; + }); + }, ), ], ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 3a95cc1..88903f8 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -45,7 +45,7 @@ class MessageContextMenu extends StatelessWidget { context: context, backgroundColor: Colors.black, builder: (BuildContext context) { - return const Emojis(); + return const EmojiPickerBottom(); }, ) as EmojiLayerData?; if (layer == null) return; diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart new file mode 100644 index 0000000..d989a62 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -0,0 +1,216 @@ +import 'dart:io'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_send_to_view.dart'; + +class MessageInput extends StatefulWidget { + const MessageInput({ + required this.group, + required this.quotesMessage, + required this.textFieldFocus, + required this.onMessageSend, + super.key, + }); + + final Group group; + final FocusNode textFieldFocus; + final Message? quotesMessage; + final VoidCallback onMessageSend; + + @override + State createState() => _MessageInputState(); +} + +class _MessageInputState extends State { + late final TextEditingController _textFieldController; + final bool isApple = Platform.isIOS; + bool _emojiShowing = false; + + Future _sendMessage() async { + if (_textFieldController.text == '') return; + + await insertAndSendTextMessage( + widget.group.groupId, + _textFieldController.text, + widget.quotesMessage?.messageId, + ); + + _textFieldController.clear(); + _emojiShowing = false; + widget.onMessageSend(); + setState(() {}); + } + + @override + void initState() { + _textFieldController = TextEditingController(); + widget.textFieldFocus.addListener(_handleTextFocusChange); + super.initState(); + } + + @override + void dispose() { + widget.textFieldFocus.removeListener(_handleTextFocusChange); + widget.textFieldFocus.dispose(); + super.dispose(); + } + + void _handleTextFocusChange() { + if (widget.textFieldFocus.hasFocus) { + setState(() { + _emojiShowing = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 10, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, + ), + ), + ), + Expanded( + child: TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + ], + ), + ), + ), + if (_textFieldController.text != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), + ), + Offstage( + offstage: !_emojiShowing, + child: EmojiPicker( + textEditingController: _textFieldController, + onEmojiSelected: (category, emoji) { + setState(() {}); + }, + onBackspacePressed: () { + setState(() {}); + }, + config: Config( + height: 300, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), + ), + ), + ], + ); + } +} From 07d36c133c754984d974f1be4f3d8813b9f2af48 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 4 Nov 2025 23:25:17 +0100 Subject: [PATCH 63/76] fix #214 --- lib/src/database/daos/groups.dao.dart | 7 +- lib/src/database/daos/messages.dao.dart | 3 +- lib/src/localization/app_de.arb | 2 +- lib/src/localization/app_en.arb | 2 +- .../generated/app_localizations.dart | 2 +- .../generated/app_localizations_de.dart | 2 +- .../generated/app_localizations_en.dart | 3 +- .../api/websocket/client_to_server.pb.dart | 532 ++++++++++-------- .../websocket/client_to_server.pbjson.dart | 162 +++--- .../client/generated/messages.pb.dart | 24 +- .../client/generated/messages.pbjson.dart | 37 +- lib/src/model/protobuf/client/messages.proto | 3 +- lib/src/services/api.service.dart | 48 +- .../api/client2client/contact.c2c.dart | 10 +- .../mediafiles/media_background.service.dart | 2 +- .../api/mediafiles/upload.service.dart | 2 +- lib/src/services/api/messages.dart | 3 +- lib/src/services/flame.service.dart | 2 +- lib/src/services/group.services.dart | 4 +- lib/src/views/chats/chat_messages.view.dart | 3 +- .../views/settings/profile/profile.view.dart | 102 +++- 21 files changed, 565 insertions(+), 390 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index a84e62f..c3920b5 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -37,7 +37,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .write(updates); } - Future> getGroupMembers(String groupId) async { + Future> getGroupNonLeftMembers(String groupId) async { return (select(groupMembers) ..where( (t) => @@ -47,6 +47,11 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .get(); } + Future> getAllGroupMembers(String groupId) async { + return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + .get(); + } + Future getGroupMemberByPublicKey(Uint8List publicKey) async { return (select(groupMembers) ..where((t) => t.groupPublicKey.equals(publicKey))) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7c7fa35..993ea08 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -310,7 +310,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { final message = await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); if (message == null) return true; - final members = await twonlyDB.groupsDao.getGroupMembers(message.groupId); + final members = + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); final actions = await (select(messageActions) ..where( diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 7a5b7cc..279201e 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -375,7 +375,7 @@ "@errorInternalError": {}, "errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.", "@errorInvalidInvitationCode": {}, - "errorUsernameAlreadyTaken": "Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.", + "errorUsernameAlreadyTaken": "Der Benutzername ist bereits vergeben.", "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.", "@errorSignatureNotValid": {}, diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 1ea6317..909fa8a 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -346,7 +346,7 @@ "@errorInternalError": {}, "errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.", "@errorInvalidInvitationCode": {}, - "errorUsernameAlreadyTaken": "The username you want to use is already taken. Please choose a different username.", + "errorUsernameAlreadyTaken": "The username is already taken.", "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.", "@errorSignatureNotValid": {}, diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 2184042..d238c71 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1229,7 +1229,7 @@ abstract class AppLocalizations { /// No description provided for @errorUsernameAlreadyTaken. /// /// In en, this message translates to: - /// **'The username you want to use is already taken. Please choose a different username.'** + /// **'The username is already taken.'** String get errorUsernameAlreadyTaken; /// No description provided for @errorSignatureNotValid. diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 01d99e2..37b0129 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -630,7 +630,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get errorUsernameAlreadyTaken => - 'Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.'; + 'Der Benutzername ist bereits vergeben.'; @override String get errorSignatureNotValid => diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index ddbfafa..913445b 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -624,8 +624,7 @@ class AppLocalizationsEn extends AppLocalizations { 'The invitation code you provided is invalid. Please check the code and try again.'; @override - String get errorUsernameAlreadyTaken => - 'The username you want to use is already taken. Please choose a different username.'; + String get errorUsernameAlreadyTaken => 'The username is already taken.'; @override String get errorSignatureNotValid => diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 2535881..c986530 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -793,6 +793,56 @@ class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { void clearUsername() => clearField(1); } +class ApplicationData_ChangeUsername extends $pb.GeneratedMessage { + factory ApplicationData_ChangeUsername({ + $core.String? username, + }) { + final $result = create(); + if (username != null) { + $result.username = username; + } + return $result; + } + ApplicationData_ChangeUsername._() : super(); + factory ApplicationData_ChangeUsername.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ApplicationData_ChangeUsername.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.ChangeUsername', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'username') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ApplicationData_ChangeUsername clone() => ApplicationData_ChangeUsername()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ApplicationData_ChangeUsername copyWith(void Function(ApplicationData_ChangeUsername) updates) => super.copyWith((message) => updates(message as ApplicationData_ChangeUsername)) as ApplicationData_ChangeUsername; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_ChangeUsername create() => ApplicationData_ChangeUsername._(); + ApplicationData_ChangeUsername createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ApplicationData_ChangeUsername getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_ChangeUsername? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get username => $_getSZ(0); + @$pb.TagNumber(1) + set username($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUsername() => $_has(0); + @$pb.TagNumber(1) + void clearUsername() => clearField(1); +} + class ApplicationData_UpdateGoogleFcmToken extends $pb.GeneratedMessage { factory ApplicationData_UpdateGoogleFcmToken({ $core.String? googleFcm, @@ -1706,117 +1756,122 @@ class ApplicationData_DeleteAccount extends $pb.GeneratedMessage { } enum ApplicationData_ApplicationData { - textmessage, - getuserbyusername, - getprekeysbyuserid, - getuserbyid, - updategooglefcmtoken, - getlocation, - getcurrentplaninfos, - redeemvoucher, - getavailableplans, - createvoucher, - getvouchers, - switchtopayedplan, - getaddaccountsinvites, - redeemadditionalcode, - removeadditionaluser, - updateplanoptions, - downloaddone, - getsignedprekeybyuserid, - updatesignedprekey, - deleteaccount, - reportuser, + textMessage, + getUserByUsername, + getPrekeysByUserId, + getUserById, + updateGoogleFcmToken, + getLocation, + getCurrentPlanInfos, + redeemVoucher, + getAvailablePlans, + createVoucher, + getVouchers, + switchtoPayedPlan, + getAddaccountsInvites, + redeemAdditionalCode, + removeAdditionalUser, + updatePlanOptions, + downloadDone, + getSignedPrekeyByUserid, + updateSignedPrekey, + deleteAccount, + reportUser, + changeUsername, notSet } class ApplicationData extends $pb.GeneratedMessage { factory ApplicationData({ - ApplicationData_TextMessage? textmessage, - ApplicationData_GetUserByUsername? getuserbyusername, - ApplicationData_GetPrekeysByUserId? getprekeysbyuserid, - ApplicationData_GetUserById? getuserbyid, - ApplicationData_UpdateGoogleFcmToken? updategooglefcmtoken, - ApplicationData_GetLocation? getlocation, - ApplicationData_GetCurrentPlanInfos? getcurrentplaninfos, - ApplicationData_RedeemVoucher? redeemvoucher, - ApplicationData_GetAvailablePlans? getavailableplans, - ApplicationData_CreateVoucher? createvoucher, - ApplicationData_GetVouchers? getvouchers, - ApplicationData_SwitchToPayedPlan? switchtopayedplan, - ApplicationData_GetAddAccountsInvites? getaddaccountsinvites, - ApplicationData_RedeemAdditionalCode? redeemadditionalcode, - ApplicationData_RemoveAdditionalUser? removeadditionaluser, - ApplicationData_UpdatePlanOptions? updateplanoptions, - ApplicationData_DownloadDone? downloaddone, - ApplicationData_GetSignedPreKeyByUserId? getsignedprekeybyuserid, - ApplicationData_UpdateSignedPreKey? updatesignedprekey, - ApplicationData_DeleteAccount? deleteaccount, - ApplicationData_ReportUser? reportuser, + ApplicationData_TextMessage? textMessage, + ApplicationData_GetUserByUsername? getUserByUsername, + ApplicationData_GetPrekeysByUserId? getPrekeysByUserId, + ApplicationData_GetUserById? getUserById, + ApplicationData_UpdateGoogleFcmToken? updateGoogleFcmToken, + ApplicationData_GetLocation? getLocation, + ApplicationData_GetCurrentPlanInfos? getCurrentPlanInfos, + ApplicationData_RedeemVoucher? redeemVoucher, + ApplicationData_GetAvailablePlans? getAvailablePlans, + ApplicationData_CreateVoucher? createVoucher, + ApplicationData_GetVouchers? getVouchers, + ApplicationData_SwitchToPayedPlan? switchtoPayedPlan, + ApplicationData_GetAddAccountsInvites? getAddaccountsInvites, + ApplicationData_RedeemAdditionalCode? redeemAdditionalCode, + ApplicationData_RemoveAdditionalUser? removeAdditionalUser, + ApplicationData_UpdatePlanOptions? updatePlanOptions, + ApplicationData_DownloadDone? downloadDone, + ApplicationData_GetSignedPreKeyByUserId? getSignedPrekeyByUserid, + ApplicationData_UpdateSignedPreKey? updateSignedPrekey, + ApplicationData_DeleteAccount? deleteAccount, + ApplicationData_ReportUser? reportUser, + ApplicationData_ChangeUsername? changeUsername, }) { final $result = create(); - if (textmessage != null) { - $result.textmessage = textmessage; + if (textMessage != null) { + $result.textMessage = textMessage; } - if (getuserbyusername != null) { - $result.getuserbyusername = getuserbyusername; + if (getUserByUsername != null) { + $result.getUserByUsername = getUserByUsername; } - if (getprekeysbyuserid != null) { - $result.getprekeysbyuserid = getprekeysbyuserid; + if (getPrekeysByUserId != null) { + $result.getPrekeysByUserId = getPrekeysByUserId; } - if (getuserbyid != null) { - $result.getuserbyid = getuserbyid; + if (getUserById != null) { + $result.getUserById = getUserById; } - if (updategooglefcmtoken != null) { - $result.updategooglefcmtoken = updategooglefcmtoken; + if (updateGoogleFcmToken != null) { + $result.updateGoogleFcmToken = updateGoogleFcmToken; } - if (getlocation != null) { - $result.getlocation = getlocation; + if (getLocation != null) { + $result.getLocation = getLocation; } - if (getcurrentplaninfos != null) { - $result.getcurrentplaninfos = getcurrentplaninfos; + if (getCurrentPlanInfos != null) { + $result.getCurrentPlanInfos = getCurrentPlanInfos; } - if (redeemvoucher != null) { - $result.redeemvoucher = redeemvoucher; + if (redeemVoucher != null) { + $result.redeemVoucher = redeemVoucher; } - if (getavailableplans != null) { - $result.getavailableplans = getavailableplans; + if (getAvailablePlans != null) { + $result.getAvailablePlans = getAvailablePlans; } - if (createvoucher != null) { - $result.createvoucher = createvoucher; + if (createVoucher != null) { + $result.createVoucher = createVoucher; } - if (getvouchers != null) { - $result.getvouchers = getvouchers; + if (getVouchers != null) { + $result.getVouchers = getVouchers; } - if (switchtopayedplan != null) { - $result.switchtopayedplan = switchtopayedplan; + if (switchtoPayedPlan != null) { + $result.switchtoPayedPlan = switchtoPayedPlan; } - if (getaddaccountsinvites != null) { - $result.getaddaccountsinvites = getaddaccountsinvites; + if (getAddaccountsInvites != null) { + $result.getAddaccountsInvites = getAddaccountsInvites; } - if (redeemadditionalcode != null) { - $result.redeemadditionalcode = redeemadditionalcode; + if (redeemAdditionalCode != null) { + $result.redeemAdditionalCode = redeemAdditionalCode; } - if (removeadditionaluser != null) { - $result.removeadditionaluser = removeadditionaluser; + if (removeAdditionalUser != null) { + $result.removeAdditionalUser = removeAdditionalUser; } - if (updateplanoptions != null) { - $result.updateplanoptions = updateplanoptions; + if (updatePlanOptions != null) { + $result.updatePlanOptions = updatePlanOptions; } - if (downloaddone != null) { - $result.downloaddone = downloaddone; + if (downloadDone != null) { + $result.downloadDone = downloadDone; } - if (getsignedprekeybyuserid != null) { - $result.getsignedprekeybyuserid = getsignedprekeybyuserid; + if (getSignedPrekeyByUserid != null) { + $result.getSignedPrekeyByUserid = getSignedPrekeyByUserid; } - if (updatesignedprekey != null) { - $result.updatesignedprekey = updatesignedprekey; + if (updateSignedPrekey != null) { + $result.updateSignedPrekey = updateSignedPrekey; } - if (deleteaccount != null) { - $result.deleteaccount = deleteaccount; + if (deleteAccount != null) { + $result.deleteAccount = deleteAccount; } - if (reportuser != null) { - $result.reportuser = reportuser; + if (reportUser != null) { + $result.reportUser = reportUser; + } + if (changeUsername != null) { + $result.changeUsername = changeUsername; } return $result; } @@ -1825,52 +1880,54 @@ class ApplicationData extends $pb.GeneratedMessage { factory ApplicationData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static const $core.Map<$core.int, ApplicationData_ApplicationData> _ApplicationData_ApplicationDataByTag = { - 1 : ApplicationData_ApplicationData.textmessage, - 2 : ApplicationData_ApplicationData.getuserbyusername, - 3 : ApplicationData_ApplicationData.getprekeysbyuserid, - 6 : ApplicationData_ApplicationData.getuserbyid, - 8 : ApplicationData_ApplicationData.updategooglefcmtoken, - 9 : ApplicationData_ApplicationData.getlocation, - 10 : ApplicationData_ApplicationData.getcurrentplaninfos, - 11 : ApplicationData_ApplicationData.redeemvoucher, - 12 : ApplicationData_ApplicationData.getavailableplans, - 13 : ApplicationData_ApplicationData.createvoucher, - 14 : ApplicationData_ApplicationData.getvouchers, - 15 : ApplicationData_ApplicationData.switchtopayedplan, - 16 : ApplicationData_ApplicationData.getaddaccountsinvites, - 17 : ApplicationData_ApplicationData.redeemadditionalcode, - 18 : ApplicationData_ApplicationData.removeadditionaluser, - 19 : ApplicationData_ApplicationData.updateplanoptions, - 20 : ApplicationData_ApplicationData.downloaddone, - 22 : ApplicationData_ApplicationData.getsignedprekeybyuserid, - 23 : ApplicationData_ApplicationData.updatesignedprekey, - 24 : ApplicationData_ApplicationData.deleteaccount, - 25 : ApplicationData_ApplicationData.reportuser, + 1 : ApplicationData_ApplicationData.textMessage, + 2 : ApplicationData_ApplicationData.getUserByUsername, + 3 : ApplicationData_ApplicationData.getPrekeysByUserId, + 6 : ApplicationData_ApplicationData.getUserById, + 8 : ApplicationData_ApplicationData.updateGoogleFcmToken, + 9 : ApplicationData_ApplicationData.getLocation, + 10 : ApplicationData_ApplicationData.getCurrentPlanInfos, + 11 : ApplicationData_ApplicationData.redeemVoucher, + 12 : ApplicationData_ApplicationData.getAvailablePlans, + 13 : ApplicationData_ApplicationData.createVoucher, + 14 : ApplicationData_ApplicationData.getVouchers, + 15 : ApplicationData_ApplicationData.switchtoPayedPlan, + 16 : ApplicationData_ApplicationData.getAddaccountsInvites, + 17 : ApplicationData_ApplicationData.redeemAdditionalCode, + 18 : ApplicationData_ApplicationData.removeAdditionalUser, + 19 : ApplicationData_ApplicationData.updatePlanOptions, + 20 : ApplicationData_ApplicationData.downloadDone, + 22 : ApplicationData_ApplicationData.getSignedPrekeyByUserid, + 23 : ApplicationData_ApplicationData.updateSignedPrekey, + 24 : ApplicationData_ApplicationData.deleteAccount, + 25 : ApplicationData_ApplicationData.reportUser, + 26 : ApplicationData_ApplicationData.changeUsername, 0 : ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25]) - ..aOM(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create) - ..aOM(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create) - ..aOM(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create) - ..aOM(6, _omitFieldNames ? '' : 'getuserbyid', subBuilder: ApplicationData_GetUserById.create) - ..aOM(8, _omitFieldNames ? '' : 'updategooglefcmtoken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) - ..aOM(9, _omitFieldNames ? '' : 'getlocation', subBuilder: ApplicationData_GetLocation.create) - ..aOM(10, _omitFieldNames ? '' : 'getcurrentplaninfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) - ..aOM(11, _omitFieldNames ? '' : 'redeemvoucher', subBuilder: ApplicationData_RedeemVoucher.create) - ..aOM(12, _omitFieldNames ? '' : 'getavailableplans', subBuilder: ApplicationData_GetAvailablePlans.create) - ..aOM(13, _omitFieldNames ? '' : 'createvoucher', subBuilder: ApplicationData_CreateVoucher.create) - ..aOM(14, _omitFieldNames ? '' : 'getvouchers', subBuilder: ApplicationData_GetVouchers.create) - ..aOM(15, _omitFieldNames ? '' : 'Switchtopayedplan', protoName: 'Switchtopayedplan', subBuilder: ApplicationData_SwitchToPayedPlan.create) - ..aOM(16, _omitFieldNames ? '' : 'getaddaccountsinvites', subBuilder: ApplicationData_GetAddAccountsInvites.create) - ..aOM(17, _omitFieldNames ? '' : 'redeemadditionalcode', subBuilder: ApplicationData_RedeemAdditionalCode.create) - ..aOM(18, _omitFieldNames ? '' : 'removeadditionaluser', subBuilder: ApplicationData_RemoveAdditionalUser.create) - ..aOM(19, _omitFieldNames ? '' : 'updateplanoptions', subBuilder: ApplicationData_UpdatePlanOptions.create) - ..aOM(20, _omitFieldNames ? '' : 'downloaddone', subBuilder: ApplicationData_DownloadDone.create) - ..aOM(22, _omitFieldNames ? '' : 'getsignedprekeybyuserid', subBuilder: ApplicationData_GetSignedPreKeyByUserId.create) - ..aOM(23, _omitFieldNames ? '' : 'updatesignedprekey', subBuilder: ApplicationData_UpdateSignedPreKey.create) - ..aOM(24, _omitFieldNames ? '' : 'deleteaccount', subBuilder: ApplicationData_DeleteAccount.create) - ..aOM(25, _omitFieldNames ? '' : 'reportuser', subBuilder: ApplicationData_ReportUser.create) + ..oo(0, [1, 2, 3, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26]) + ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: ApplicationData_TextMessage.create) + ..aOM(2, _omitFieldNames ? '' : 'getUserByUsername', protoName: 'getUserByUsername', subBuilder: ApplicationData_GetUserByUsername.create) + ..aOM(3, _omitFieldNames ? '' : 'getPrekeysByUserId', protoName: 'getPrekeysByUserId', subBuilder: ApplicationData_GetPrekeysByUserId.create) + ..aOM(6, _omitFieldNames ? '' : 'getUserById', protoName: 'getUserById', subBuilder: ApplicationData_GetUserById.create) + ..aOM(8, _omitFieldNames ? '' : 'updateGoogleFcmToken', protoName: 'updateGoogleFcmToken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) + ..aOM(9, _omitFieldNames ? '' : 'getLocation', protoName: 'getLocation', subBuilder: ApplicationData_GetLocation.create) + ..aOM(10, _omitFieldNames ? '' : 'getCurrentPlanInfos', protoName: 'getCurrentPlanInfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) + ..aOM(11, _omitFieldNames ? '' : 'redeemVoucher', protoName: 'redeemVoucher', subBuilder: ApplicationData_RedeemVoucher.create) + ..aOM(12, _omitFieldNames ? '' : 'getAvailablePlans', protoName: 'getAvailablePlans', subBuilder: ApplicationData_GetAvailablePlans.create) + ..aOM(13, _omitFieldNames ? '' : 'createVoucher', protoName: 'createVoucher', subBuilder: ApplicationData_CreateVoucher.create) + ..aOM(14, _omitFieldNames ? '' : 'getVouchers', protoName: 'getVouchers', subBuilder: ApplicationData_GetVouchers.create) + ..aOM(15, _omitFieldNames ? '' : 'switchtoPayedPlan', protoName: 'switchtoPayedPlan', subBuilder: ApplicationData_SwitchToPayedPlan.create) + ..aOM(16, _omitFieldNames ? '' : 'getAddaccountsInvites', protoName: 'getAddaccountsInvites', subBuilder: ApplicationData_GetAddAccountsInvites.create) + ..aOM(17, _omitFieldNames ? '' : 'redeemAdditionalCode', protoName: 'redeemAdditionalCode', subBuilder: ApplicationData_RedeemAdditionalCode.create) + ..aOM(18, _omitFieldNames ? '' : 'removeAdditionalUser', protoName: 'removeAdditionalUser', subBuilder: ApplicationData_RemoveAdditionalUser.create) + ..aOM(19, _omitFieldNames ? '' : 'updatePlanOptions', protoName: 'updatePlanOptions', subBuilder: ApplicationData_UpdatePlanOptions.create) + ..aOM(20, _omitFieldNames ? '' : 'downloadDone', protoName: 'downloadDone', subBuilder: ApplicationData_DownloadDone.create) + ..aOM(22, _omitFieldNames ? '' : 'getSignedPrekeyByUserid', protoName: 'getSignedPrekeyByUserid', subBuilder: ApplicationData_GetSignedPreKeyByUserId.create) + ..aOM(23, _omitFieldNames ? '' : 'updateSignedPrekey', protoName: 'updateSignedPrekey', subBuilder: ApplicationData_UpdateSignedPreKey.create) + ..aOM(24, _omitFieldNames ? '' : 'deleteAccount', protoName: 'deleteAccount', subBuilder: ApplicationData_DeleteAccount.create) + ..aOM(25, _omitFieldNames ? '' : 'reportUser', protoName: 'reportUser', subBuilder: ApplicationData_ReportUser.create) + ..aOM(26, _omitFieldNames ? '' : 'changeUsername', protoName: 'changeUsername', subBuilder: ApplicationData_ChangeUsername.create) ..hasRequiredFields = false ; @@ -1899,235 +1956,246 @@ class ApplicationData extends $pb.GeneratedMessage { void clearApplicationData() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - ApplicationData_TextMessage get textmessage => $_getN(0); + ApplicationData_TextMessage get textMessage => $_getN(0); @$pb.TagNumber(1) - set textmessage(ApplicationData_TextMessage v) { setField(1, v); } + set textMessage(ApplicationData_TextMessage v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasTextmessage() => $_has(0); + $core.bool hasTextMessage() => $_has(0); @$pb.TagNumber(1) - void clearTextmessage() => clearField(1); + void clearTextMessage() => clearField(1); @$pb.TagNumber(1) - ApplicationData_TextMessage ensureTextmessage() => $_ensure(0); + ApplicationData_TextMessage ensureTextMessage() => $_ensure(0); @$pb.TagNumber(2) - ApplicationData_GetUserByUsername get getuserbyusername => $_getN(1); + ApplicationData_GetUserByUsername get getUserByUsername => $_getN(1); @$pb.TagNumber(2) - set getuserbyusername(ApplicationData_GetUserByUsername v) { setField(2, v); } + set getUserByUsername(ApplicationData_GetUserByUsername v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasGetuserbyusername() => $_has(1); + $core.bool hasGetUserByUsername() => $_has(1); @$pb.TagNumber(2) - void clearGetuserbyusername() => clearField(2); + void clearGetUserByUsername() => clearField(2); @$pb.TagNumber(2) - ApplicationData_GetUserByUsername ensureGetuserbyusername() => $_ensure(1); + ApplicationData_GetUserByUsername ensureGetUserByUsername() => $_ensure(1); @$pb.TagNumber(3) - ApplicationData_GetPrekeysByUserId get getprekeysbyuserid => $_getN(2); + ApplicationData_GetPrekeysByUserId get getPrekeysByUserId => $_getN(2); @$pb.TagNumber(3) - set getprekeysbyuserid(ApplicationData_GetPrekeysByUserId v) { setField(3, v); } + set getPrekeysByUserId(ApplicationData_GetPrekeysByUserId v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasGetprekeysbyuserid() => $_has(2); + $core.bool hasGetPrekeysByUserId() => $_has(2); @$pb.TagNumber(3) - void clearGetprekeysbyuserid() => clearField(3); + void clearGetPrekeysByUserId() => clearField(3); @$pb.TagNumber(3) - ApplicationData_GetPrekeysByUserId ensureGetprekeysbyuserid() => $_ensure(2); + ApplicationData_GetPrekeysByUserId ensureGetPrekeysByUserId() => $_ensure(2); @$pb.TagNumber(6) - ApplicationData_GetUserById get getuserbyid => $_getN(3); + ApplicationData_GetUserById get getUserById => $_getN(3); @$pb.TagNumber(6) - set getuserbyid(ApplicationData_GetUserById v) { setField(6, v); } + set getUserById(ApplicationData_GetUserById v) { setField(6, v); } @$pb.TagNumber(6) - $core.bool hasGetuserbyid() => $_has(3); + $core.bool hasGetUserById() => $_has(3); @$pb.TagNumber(6) - void clearGetuserbyid() => clearField(6); + void clearGetUserById() => clearField(6); @$pb.TagNumber(6) - ApplicationData_GetUserById ensureGetuserbyid() => $_ensure(3); + ApplicationData_GetUserById ensureGetUserById() => $_ensure(3); @$pb.TagNumber(8) - ApplicationData_UpdateGoogleFcmToken get updategooglefcmtoken => $_getN(4); + ApplicationData_UpdateGoogleFcmToken get updateGoogleFcmToken => $_getN(4); @$pb.TagNumber(8) - set updategooglefcmtoken(ApplicationData_UpdateGoogleFcmToken v) { setField(8, v); } + set updateGoogleFcmToken(ApplicationData_UpdateGoogleFcmToken v) { setField(8, v); } @$pb.TagNumber(8) - $core.bool hasUpdategooglefcmtoken() => $_has(4); + $core.bool hasUpdateGoogleFcmToken() => $_has(4); @$pb.TagNumber(8) - void clearUpdategooglefcmtoken() => clearField(8); + void clearUpdateGoogleFcmToken() => clearField(8); @$pb.TagNumber(8) - ApplicationData_UpdateGoogleFcmToken ensureUpdategooglefcmtoken() => $_ensure(4); + ApplicationData_UpdateGoogleFcmToken ensureUpdateGoogleFcmToken() => $_ensure(4); @$pb.TagNumber(9) - ApplicationData_GetLocation get getlocation => $_getN(5); + ApplicationData_GetLocation get getLocation => $_getN(5); @$pb.TagNumber(9) - set getlocation(ApplicationData_GetLocation v) { setField(9, v); } + set getLocation(ApplicationData_GetLocation v) { setField(9, v); } @$pb.TagNumber(9) - $core.bool hasGetlocation() => $_has(5); + $core.bool hasGetLocation() => $_has(5); @$pb.TagNumber(9) - void clearGetlocation() => clearField(9); + void clearGetLocation() => clearField(9); @$pb.TagNumber(9) - ApplicationData_GetLocation ensureGetlocation() => $_ensure(5); + ApplicationData_GetLocation ensureGetLocation() => $_ensure(5); @$pb.TagNumber(10) - ApplicationData_GetCurrentPlanInfos get getcurrentplaninfos => $_getN(6); + ApplicationData_GetCurrentPlanInfos get getCurrentPlanInfos => $_getN(6); @$pb.TagNumber(10) - set getcurrentplaninfos(ApplicationData_GetCurrentPlanInfos v) { setField(10, v); } + set getCurrentPlanInfos(ApplicationData_GetCurrentPlanInfos v) { setField(10, v); } @$pb.TagNumber(10) - $core.bool hasGetcurrentplaninfos() => $_has(6); + $core.bool hasGetCurrentPlanInfos() => $_has(6); @$pb.TagNumber(10) - void clearGetcurrentplaninfos() => clearField(10); + void clearGetCurrentPlanInfos() => clearField(10); @$pb.TagNumber(10) - ApplicationData_GetCurrentPlanInfos ensureGetcurrentplaninfos() => $_ensure(6); + ApplicationData_GetCurrentPlanInfos ensureGetCurrentPlanInfos() => $_ensure(6); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher get redeemvoucher => $_getN(7); + ApplicationData_RedeemVoucher get redeemVoucher => $_getN(7); @$pb.TagNumber(11) - set redeemvoucher(ApplicationData_RedeemVoucher v) { setField(11, v); } + set redeemVoucher(ApplicationData_RedeemVoucher v) { setField(11, v); } @$pb.TagNumber(11) - $core.bool hasRedeemvoucher() => $_has(7); + $core.bool hasRedeemVoucher() => $_has(7); @$pb.TagNumber(11) - void clearRedeemvoucher() => clearField(11); + void clearRedeemVoucher() => clearField(11); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher ensureRedeemvoucher() => $_ensure(7); + ApplicationData_RedeemVoucher ensureRedeemVoucher() => $_ensure(7); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans get getavailableplans => $_getN(8); + ApplicationData_GetAvailablePlans get getAvailablePlans => $_getN(8); @$pb.TagNumber(12) - set getavailableplans(ApplicationData_GetAvailablePlans v) { setField(12, v); } + set getAvailablePlans(ApplicationData_GetAvailablePlans v) { setField(12, v); } @$pb.TagNumber(12) - $core.bool hasGetavailableplans() => $_has(8); + $core.bool hasGetAvailablePlans() => $_has(8); @$pb.TagNumber(12) - void clearGetavailableplans() => clearField(12); + void clearGetAvailablePlans() => clearField(12); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans ensureGetavailableplans() => $_ensure(8); + ApplicationData_GetAvailablePlans ensureGetAvailablePlans() => $_ensure(8); @$pb.TagNumber(13) - ApplicationData_CreateVoucher get createvoucher => $_getN(9); + ApplicationData_CreateVoucher get createVoucher => $_getN(9); @$pb.TagNumber(13) - set createvoucher(ApplicationData_CreateVoucher v) { setField(13, v); } + set createVoucher(ApplicationData_CreateVoucher v) { setField(13, v); } @$pb.TagNumber(13) - $core.bool hasCreatevoucher() => $_has(9); + $core.bool hasCreateVoucher() => $_has(9); @$pb.TagNumber(13) - void clearCreatevoucher() => clearField(13); + void clearCreateVoucher() => clearField(13); @$pb.TagNumber(13) - ApplicationData_CreateVoucher ensureCreatevoucher() => $_ensure(9); + ApplicationData_CreateVoucher ensureCreateVoucher() => $_ensure(9); @$pb.TagNumber(14) - ApplicationData_GetVouchers get getvouchers => $_getN(10); + ApplicationData_GetVouchers get getVouchers => $_getN(10); @$pb.TagNumber(14) - set getvouchers(ApplicationData_GetVouchers v) { setField(14, v); } + set getVouchers(ApplicationData_GetVouchers v) { setField(14, v); } @$pb.TagNumber(14) - $core.bool hasGetvouchers() => $_has(10); + $core.bool hasGetVouchers() => $_has(10); @$pb.TagNumber(14) - void clearGetvouchers() => clearField(14); + void clearGetVouchers() => clearField(14); @$pb.TagNumber(14) - ApplicationData_GetVouchers ensureGetvouchers() => $_ensure(10); + ApplicationData_GetVouchers ensureGetVouchers() => $_ensure(10); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan get switchtopayedplan => $_getN(11); + ApplicationData_SwitchToPayedPlan get switchtoPayedPlan => $_getN(11); @$pb.TagNumber(15) - set switchtopayedplan(ApplicationData_SwitchToPayedPlan v) { setField(15, v); } + set switchtoPayedPlan(ApplicationData_SwitchToPayedPlan v) { setField(15, v); } @$pb.TagNumber(15) - $core.bool hasSwitchtopayedplan() => $_has(11); + $core.bool hasSwitchtoPayedPlan() => $_has(11); @$pb.TagNumber(15) - void clearSwitchtopayedplan() => clearField(15); + void clearSwitchtoPayedPlan() => clearField(15); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan ensureSwitchtopayedplan() => $_ensure(11); + ApplicationData_SwitchToPayedPlan ensureSwitchtoPayedPlan() => $_ensure(11); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites get getaddaccountsinvites => $_getN(12); + ApplicationData_GetAddAccountsInvites get getAddaccountsInvites => $_getN(12); @$pb.TagNumber(16) - set getaddaccountsinvites(ApplicationData_GetAddAccountsInvites v) { setField(16, v); } + set getAddaccountsInvites(ApplicationData_GetAddAccountsInvites v) { setField(16, v); } @$pb.TagNumber(16) - $core.bool hasGetaddaccountsinvites() => $_has(12); + $core.bool hasGetAddaccountsInvites() => $_has(12); @$pb.TagNumber(16) - void clearGetaddaccountsinvites() => clearField(16); + void clearGetAddaccountsInvites() => clearField(16); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites ensureGetaddaccountsinvites() => $_ensure(12); + ApplicationData_GetAddAccountsInvites ensureGetAddaccountsInvites() => $_ensure(12); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode get redeemadditionalcode => $_getN(13); + ApplicationData_RedeemAdditionalCode get redeemAdditionalCode => $_getN(13); @$pb.TagNumber(17) - set redeemadditionalcode(ApplicationData_RedeemAdditionalCode v) { setField(17, v); } + set redeemAdditionalCode(ApplicationData_RedeemAdditionalCode v) { setField(17, v); } @$pb.TagNumber(17) - $core.bool hasRedeemadditionalcode() => $_has(13); + $core.bool hasRedeemAdditionalCode() => $_has(13); @$pb.TagNumber(17) - void clearRedeemadditionalcode() => clearField(17); + void clearRedeemAdditionalCode() => clearField(17); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode ensureRedeemadditionalcode() => $_ensure(13); + ApplicationData_RedeemAdditionalCode ensureRedeemAdditionalCode() => $_ensure(13); @$pb.TagNumber(18) - ApplicationData_RemoveAdditionalUser get removeadditionaluser => $_getN(14); + ApplicationData_RemoveAdditionalUser get removeAdditionalUser => $_getN(14); @$pb.TagNumber(18) - set removeadditionaluser(ApplicationData_RemoveAdditionalUser v) { setField(18, v); } + set removeAdditionalUser(ApplicationData_RemoveAdditionalUser v) { setField(18, v); } @$pb.TagNumber(18) - $core.bool hasRemoveadditionaluser() => $_has(14); + $core.bool hasRemoveAdditionalUser() => $_has(14); @$pb.TagNumber(18) - void clearRemoveadditionaluser() => clearField(18); + void clearRemoveAdditionalUser() => clearField(18); @$pb.TagNumber(18) - ApplicationData_RemoveAdditionalUser ensureRemoveadditionaluser() => $_ensure(14); + ApplicationData_RemoveAdditionalUser ensureRemoveAdditionalUser() => $_ensure(14); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions get updateplanoptions => $_getN(15); + ApplicationData_UpdatePlanOptions get updatePlanOptions => $_getN(15); @$pb.TagNumber(19) - set updateplanoptions(ApplicationData_UpdatePlanOptions v) { setField(19, v); } + set updatePlanOptions(ApplicationData_UpdatePlanOptions v) { setField(19, v); } @$pb.TagNumber(19) - $core.bool hasUpdateplanoptions() => $_has(15); + $core.bool hasUpdatePlanOptions() => $_has(15); @$pb.TagNumber(19) - void clearUpdateplanoptions() => clearField(19); + void clearUpdatePlanOptions() => clearField(19); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions ensureUpdateplanoptions() => $_ensure(15); + ApplicationData_UpdatePlanOptions ensureUpdatePlanOptions() => $_ensure(15); @$pb.TagNumber(20) - ApplicationData_DownloadDone get downloaddone => $_getN(16); + ApplicationData_DownloadDone get downloadDone => $_getN(16); @$pb.TagNumber(20) - set downloaddone(ApplicationData_DownloadDone v) { setField(20, v); } + set downloadDone(ApplicationData_DownloadDone v) { setField(20, v); } @$pb.TagNumber(20) - $core.bool hasDownloaddone() => $_has(16); + $core.bool hasDownloadDone() => $_has(16); @$pb.TagNumber(20) - void clearDownloaddone() => clearField(20); + void clearDownloadDone() => clearField(20); @$pb.TagNumber(20) - ApplicationData_DownloadDone ensureDownloaddone() => $_ensure(16); + ApplicationData_DownloadDone ensureDownloadDone() => $_ensure(16); @$pb.TagNumber(22) - ApplicationData_GetSignedPreKeyByUserId get getsignedprekeybyuserid => $_getN(17); + ApplicationData_GetSignedPreKeyByUserId get getSignedPrekeyByUserid => $_getN(17); @$pb.TagNumber(22) - set getsignedprekeybyuserid(ApplicationData_GetSignedPreKeyByUserId v) { setField(22, v); } + set getSignedPrekeyByUserid(ApplicationData_GetSignedPreKeyByUserId v) { setField(22, v); } @$pb.TagNumber(22) - $core.bool hasGetsignedprekeybyuserid() => $_has(17); + $core.bool hasGetSignedPrekeyByUserid() => $_has(17); @$pb.TagNumber(22) - void clearGetsignedprekeybyuserid() => clearField(22); + void clearGetSignedPrekeyByUserid() => clearField(22); @$pb.TagNumber(22) - ApplicationData_GetSignedPreKeyByUserId ensureGetsignedprekeybyuserid() => $_ensure(17); + ApplicationData_GetSignedPreKeyByUserId ensureGetSignedPrekeyByUserid() => $_ensure(17); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey get updatesignedprekey => $_getN(18); + ApplicationData_UpdateSignedPreKey get updateSignedPrekey => $_getN(18); @$pb.TagNumber(23) - set updatesignedprekey(ApplicationData_UpdateSignedPreKey v) { setField(23, v); } + set updateSignedPrekey(ApplicationData_UpdateSignedPreKey v) { setField(23, v); } @$pb.TagNumber(23) - $core.bool hasUpdatesignedprekey() => $_has(18); + $core.bool hasUpdateSignedPrekey() => $_has(18); @$pb.TagNumber(23) - void clearUpdatesignedprekey() => clearField(23); + void clearUpdateSignedPrekey() => clearField(23); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey ensureUpdatesignedprekey() => $_ensure(18); + ApplicationData_UpdateSignedPreKey ensureUpdateSignedPrekey() => $_ensure(18); @$pb.TagNumber(24) - ApplicationData_DeleteAccount get deleteaccount => $_getN(19); + ApplicationData_DeleteAccount get deleteAccount => $_getN(19); @$pb.TagNumber(24) - set deleteaccount(ApplicationData_DeleteAccount v) { setField(24, v); } + set deleteAccount(ApplicationData_DeleteAccount v) { setField(24, v); } @$pb.TagNumber(24) - $core.bool hasDeleteaccount() => $_has(19); + $core.bool hasDeleteAccount() => $_has(19); @$pb.TagNumber(24) - void clearDeleteaccount() => clearField(24); + void clearDeleteAccount() => clearField(24); @$pb.TagNumber(24) - ApplicationData_DeleteAccount ensureDeleteaccount() => $_ensure(19); + ApplicationData_DeleteAccount ensureDeleteAccount() => $_ensure(19); @$pb.TagNumber(25) - ApplicationData_ReportUser get reportuser => $_getN(20); + ApplicationData_ReportUser get reportUser => $_getN(20); @$pb.TagNumber(25) - set reportuser(ApplicationData_ReportUser v) { setField(25, v); } + set reportUser(ApplicationData_ReportUser v) { setField(25, v); } @$pb.TagNumber(25) - $core.bool hasReportuser() => $_has(20); + $core.bool hasReportUser() => $_has(20); @$pb.TagNumber(25) - void clearReportuser() => clearField(25); + void clearReportUser() => clearField(25); @$pb.TagNumber(25) - ApplicationData_ReportUser ensureReportuser() => $_ensure(20); + ApplicationData_ReportUser ensureReportUser() => $_ensure(20); + + @$pb.TagNumber(26) + ApplicationData_ChangeUsername get changeUsername => $_getN(21); + @$pb.TagNumber(26) + set changeUsername(ApplicationData_ChangeUsername v) { setField(26, v); } + @$pb.TagNumber(26) + $core.bool hasChangeUsername() => $_has(21); + @$pb.TagNumber(26) + void clearChangeUsername() => clearField(26); + @$pb.TagNumber(26) + ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(21); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index edb5470..c95798c 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -139,29 +139,30 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( const ApplicationData$json = { '1': 'ApplicationData', '2': [ - {'1': 'textmessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textmessage'}, - {'1': 'getuserbyusername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getuserbyusername'}, - {'1': 'getprekeysbyuserid', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getprekeysbyuserid'}, - {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, - {'1': 'updategooglefcmtoken', '3': 8, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateGoogleFcmToken', '9': 0, '10': 'updategooglefcmtoken'}, - {'1': 'getlocation', '3': 9, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetLocation', '9': 0, '10': 'getlocation'}, - {'1': 'getcurrentplaninfos', '3': 10, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetCurrentPlanInfos', '9': 0, '10': 'getcurrentplaninfos'}, - {'1': 'redeemvoucher', '3': 11, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemVoucher', '9': 0, '10': 'redeemvoucher'}, - {'1': 'getavailableplans', '3': 12, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAvailablePlans', '9': 0, '10': 'getavailableplans'}, - {'1': 'createvoucher', '3': 13, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.CreateVoucher', '9': 0, '10': 'createvoucher'}, - {'1': 'getvouchers', '3': 14, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetVouchers', '9': 0, '10': 'getvouchers'}, - {'1': 'Switchtopayedplan', '3': 15, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.SwitchToPayedPlan', '9': 0, '10': 'Switchtopayedplan'}, - {'1': 'getaddaccountsinvites', '3': 16, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAddAccountsInvites', '9': 0, '10': 'getaddaccountsinvites'}, - {'1': 'redeemadditionalcode', '3': 17, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemAdditionalCode', '9': 0, '10': 'redeemadditionalcode'}, - {'1': 'removeadditionaluser', '3': 18, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RemoveAdditionalUser', '9': 0, '10': 'removeadditionaluser'}, - {'1': 'updateplanoptions', '3': 19, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdatePlanOptions', '9': 0, '10': 'updateplanoptions'}, - {'1': 'downloaddone', '3': 20, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadDone', '9': 0, '10': 'downloaddone'}, - {'1': 'getsignedprekeybyuserid', '3': 22, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetSignedPreKeyByUserId', '9': 0, '10': 'getsignedprekeybyuserid'}, - {'1': 'updatesignedprekey', '3': 23, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateSignedPreKey', '9': 0, '10': 'updatesignedprekey'}, - {'1': 'deleteaccount', '3': 24, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DeleteAccount', '9': 0, '10': 'deleteaccount'}, - {'1': 'reportuser', '3': 25, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ReportUser', '9': 0, '10': 'reportuser'}, + {'1': 'textMessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textMessage'}, + {'1': 'getUserByUsername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getUserByUsername'}, + {'1': 'getPrekeysByUserId', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getPrekeysByUserId'}, + {'1': 'getUserById', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getUserById'}, + {'1': 'updateGoogleFcmToken', '3': 8, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateGoogleFcmToken', '9': 0, '10': 'updateGoogleFcmToken'}, + {'1': 'getLocation', '3': 9, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetLocation', '9': 0, '10': 'getLocation'}, + {'1': 'getCurrentPlanInfos', '3': 10, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetCurrentPlanInfos', '9': 0, '10': 'getCurrentPlanInfos'}, + {'1': 'redeemVoucher', '3': 11, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemVoucher', '9': 0, '10': 'redeemVoucher'}, + {'1': 'getAvailablePlans', '3': 12, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAvailablePlans', '9': 0, '10': 'getAvailablePlans'}, + {'1': 'createVoucher', '3': 13, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.CreateVoucher', '9': 0, '10': 'createVoucher'}, + {'1': 'getVouchers', '3': 14, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetVouchers', '9': 0, '10': 'getVouchers'}, + {'1': 'switchtoPayedPlan', '3': 15, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.SwitchToPayedPlan', '9': 0, '10': 'switchtoPayedPlan'}, + {'1': 'getAddaccountsInvites', '3': 16, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAddAccountsInvites', '9': 0, '10': 'getAddaccountsInvites'}, + {'1': 'redeemAdditionalCode', '3': 17, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemAdditionalCode', '9': 0, '10': 'redeemAdditionalCode'}, + {'1': 'removeAdditionalUser', '3': 18, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RemoveAdditionalUser', '9': 0, '10': 'removeAdditionalUser'}, + {'1': 'updatePlanOptions', '3': 19, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdatePlanOptions', '9': 0, '10': 'updatePlanOptions'}, + {'1': 'downloadDone', '3': 20, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadDone', '9': 0, '10': 'downloadDone'}, + {'1': 'getSignedPrekeyByUserid', '3': 22, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetSignedPreKeyByUserId', '9': 0, '10': 'getSignedPrekeyByUserid'}, + {'1': 'updateSignedPrekey', '3': 23, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateSignedPreKey', '9': 0, '10': 'updateSignedPrekey'}, + {'1': 'deleteAccount', '3': 24, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DeleteAccount', '9': 0, '10': 'deleteAccount'}, + {'1': 'reportUser', '3': 25, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ReportUser', '9': 0, '10': 'reportUser'}, + {'1': 'changeUsername', '3': 26, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ChangeUsername', '9': 0, '10': 'changeUsername'}, ], - '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_RedeemVoucher$json, ApplicationData_SwitchToPayedPlan$json, ApplicationData_UpdatePlanOptions$json, ApplicationData_CreateVoucher$json, ApplicationData_GetLocation$json, ApplicationData_GetVouchers$json, ApplicationData_GetAvailablePlans$json, ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, ApplicationData_RedeemAdditionalCode$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json, ApplicationData_UpdateSignedPreKey$json, ApplicationData_DownloadDone$json, ApplicationData_ReportUser$json, ApplicationData_DeleteAccount$json], + '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_ChangeUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_RedeemVoucher$json, ApplicationData_SwitchToPayedPlan$json, ApplicationData_UpdatePlanOptions$json, ApplicationData_CreateVoucher$json, ApplicationData_GetLocation$json, ApplicationData_GetVouchers$json, ApplicationData_GetAvailablePlans$json, ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, ApplicationData_RedeemAdditionalCode$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json, ApplicationData_UpdateSignedPreKey$json, ApplicationData_DownloadDone$json, ApplicationData_ReportUser$json, ApplicationData_DeleteAccount$json], '8': [ {'1': 'ApplicationData'}, ], @@ -188,6 +189,14 @@ const ApplicationData_GetUserByUsername$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_ChangeUsername$json = { + '1': 'ChangeUsername', + '2': [ + {'1': 'username', '3': 1, '4': 1, '5': 9, '10': 'username'}, + ], +}; + @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_UpdateGoogleFcmToken$json = { '1': 'UpdateGoogleFcmToken', @@ -329,64 +338,67 @@ const ApplicationData_DeleteAccount$json = { /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( - 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dG1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' - 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0bWVzc2FnZRJjChFnZXR1c2Vy' - 'Ynl1c2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' - 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lEmYKEmdldHByZWtleXNieXVzZXJp' + 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' + 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0TWVzc2FnZRJjChFnZXRVc2Vy' + 'QnlVc2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' + 'VzZXJCeVVzZXJuYW1lSABSEWdldFVzZXJCeVVzZXJuYW1lEmYKEmdldFByZWtleXNCeVVzZXJJ' 'ZBgDIAEoCzI0LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFByZWtleXNCeV' - 'VzZXJJZEgAUhJnZXRwcmVrZXlzYnl1c2VyaWQSUQoLZ2V0dXNlcmJ5aWQYBiABKAsyLS5jbGll' - 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXR1c2VyYnlpZB' - 'JsChR1cGRhdGVnb29nbGVmY210b2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' - 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZWdvb2dsZWZjbXRva2VuEl' - 'EKC2dldGxvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' - 'R2V0TG9jYXRpb25IAFILZ2V0bG9jYXRpb24SaQoTZ2V0Y3VycmVudHBsYW5pbmZvcxgKIAEoCz' + 'VzZXJJZEgAUhJnZXRQcmVrZXlzQnlVc2VySWQSUQoLZ2V0VXNlckJ5SWQYBiABKAsyLS5jbGll' + 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB' + 'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' + 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl' + 'EKC2dldExvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' + 'R2V0TG9jYXRpb25IAFILZ2V0TG9jYXRpb24SaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' 'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI' - 'AFITZ2V0Y3VycmVudHBsYW5pbmZvcxJXCg1yZWRlZW12b3VjaGVyGAsgASgLMi8uY2xpZW50X3' - 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW12b3VjaGVy' - 'EmMKEWdldGF2YWlsYWJsZXBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0YXZhaWxhYmxlcGxhbnMSVwoNY3JlYXRl' - 'dm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' - 'ZvdWNoZXJIAFINY3JlYXRldm91Y2hlchJRCgtnZXR2b3VjaGVycxgOIAEoCzItLmNsaWVudF90' - 'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldHZvdWNoZXJzEmMKEV' - 'N3aXRjaHRvcGF5ZWRwbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' - 'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRU3dpdGNodG9wYXllZHBsYW4SbwoVZ2V0YWRkYWNjb3' - 'VudHNpbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' - 'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldGFkZGFjY291bnRzaW52aXRlcxJsChRyZWRlZW1hZG' - 'RpdGlvbmFsY29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' - 'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbWFkZGl0aW9uYWxjb2RlEmwKFHJlbW92ZWFkZG' - 'l0aW9uYWx1c2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVt' - 'b3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlYWRkaXRpb25hbHVzZXISYwoRdXBkYXRlcGxhbm' + 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJXCg1yZWRlZW1Wb3VjaGVyGAsgASgLMi8uY2xpZW50X3' + 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW1Wb3VjaGVy' + 'EmMKEWdldEF2YWlsYWJsZVBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' + 'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0QXZhaWxhYmxlUGxhbnMSVwoNY3JlYXRl' + 'Vm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' + 'ZvdWNoZXJIAFINY3JlYXRlVm91Y2hlchJRCgtnZXRWb3VjaGVycxgOIAEoCzItLmNsaWVudF90' + 'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldFZvdWNoZXJzEmMKEX' + 'N3aXRjaHRvUGF5ZWRQbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' + 'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRc3dpdGNodG9QYXllZFBsYW4SbwoVZ2V0QWRkYWNjb3' + 'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' + 'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG' + 'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' + 'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmwKFHJlbW92ZUFkZG' + 'l0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVt' + 'b3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoRdXBkYXRlUGxhbk' '9wdGlvbnMYEyABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5VcGRhdGVQ' - 'bGFuT3B0aW9uc0gAUhF1cGRhdGVwbGFub3B0aW9ucxJUCgxkb3dubG9hZGRvbmUYFCABKAsyLi' + 'bGFuT3B0aW9uc0gAUhF1cGRhdGVQbGFuT3B0aW9ucxJUCgxkb3dubG9hZERvbmUYFCABKAsyLi' '5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5Eb3dubG9hZERvbmVIAFIMZG93bmxv' - 'YWRkb25lEnUKF2dldHNpZ25lZHByZWtleWJ5dXNlcmlkGBYgASgLMjkuY2xpZW50X3RvX3Nlcn' - 'Zlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIXZ2V0c2lnbmVk' - 'cHJla2V5Ynl1c2VyaWQSZgoSdXBkYXRlc2lnbmVkcHJla2V5GBcgASgLMjQuY2xpZW50X3RvX3' - 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVwZGF0ZXNpZ25l' - 'ZHByZWtleRJXCg1kZWxldGVhY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3NlcnZlci5BcHBsaW' - 'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVhY2NvdW50Ek4KCnJlcG9ydHVzZXIY' + 'YWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGBYgASgLMjkuY2xpZW50X3RvX3Nlcn' + 'Zlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIXZ2V0U2lnbmVk' + 'UHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2V5GBcgASgLMjQuY2xpZW50X3RvX3' + 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVwZGF0ZVNpZ25l' + 'ZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3NlcnZlci5BcHBsaW' + 'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY' 'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn' - 'JlcG9ydHVzZXIaagoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJv' - 'ZHkYAyABKAxSBGJvZHkSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdX' - 'NoX2RhdGEaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1l' - 'GjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbR' - 'omCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hl' - 'chIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW' - '5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxh' - 'dXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDG' - 'F1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1' - 'ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycx' - 'oTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3Vy' - 'cmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIA' - 'EoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgD' - 'UgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZB' - 'oyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEK' - 'ElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcm' - 'VrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3By' - 'ZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZE' - 'RvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNl' - 'chIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAi' - 'ABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50QhEKD0FwcGxpY2F0aW9uRGF0YQ=='); + 'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw' + 'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRpqCgtUZXh0TW' + 'Vzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglw' + 'dXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQn' + 'lVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUS' + 'GgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCm' + 'dvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEg' + 'ASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGn' + 'AKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9u' + 'dGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2' + 'FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3' + 'YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCg' + 'tHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRB' + 'ZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdG' + 'lvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRp' + 'dGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlck' + 'lkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIX' + 'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZW' + 'RfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgM' + 'UgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZF' + 'ByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxS' + 'DWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg' + '5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50' + 'QhEKD0FwcGxpY2F0aW9uRGF0YQ=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 4763a2f..baefcd7 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -1052,6 +1052,7 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { factory EncryptedContent_ContactUpdate({ EncryptedContent_ContactUpdate_Type? type, $core.List<$core.int>? avatarSvgCompressed, + $core.String? username, $core.String? displayName, }) { final $result = create(); @@ -1061,6 +1062,9 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { if (avatarSvgCompressed != null) { $result.avatarSvgCompressed = avatarSvgCompressed; } + if (username != null) { + $result.username = username; + } if (displayName != null) { $result.displayName = displayName; } @@ -1073,7 +1077,8 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactUpdate', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactUpdate_Type.REQUEST, valueOf: EncryptedContent_ContactUpdate_Type.valueOf, enumValues: EncryptedContent_ContactUpdate_Type.values) ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY, protoName: 'avatarSvgCompressed') - ..aOS(3, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') + ..aOS(3, _omitFieldNames ? '' : 'username') + ..aOS(4, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') ..hasRequiredFields = false ; @@ -1117,13 +1122,22 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { void clearAvatarSvgCompressed() => clearField(2); @$pb.TagNumber(3) - $core.String get displayName => $_getSZ(2); + $core.String get username => $_getSZ(2); @$pb.TagNumber(3) - set displayName($core.String v) { $_setString(2, v); } + set username($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) - $core.bool hasDisplayName() => $_has(2); + $core.bool hasUsername() => $_has(2); @$pb.TagNumber(3) - void clearDisplayName() => clearField(3); + void clearUsername() => clearField(3); + + @$pb.TagNumber(4) + $core.String get displayName => $_getSZ(3); + @$pb.TagNumber(4) + set displayName($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasDisplayName() => $_has(3); + @$pb.TagNumber(4) + void clearDisplayName() => clearField(4); } class EncryptedContent_PushKeys extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index f07d909..fc171fe 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -310,11 +310,13 @@ const EncryptedContent_ContactUpdate$json = { '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactUpdate.Type', '10': 'type'}, {'1': 'avatarSvgCompressed', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'avatarSvgCompressed', '17': true}, - {'1': 'displayName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'displayName', '17': true}, + {'1': 'username', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'username', '17': true}, + {'1': 'displayName', '3': 4, '4': 1, '5': 9, '9': 2, '10': 'displayName', '17': true}, ], '4': [EncryptedContent_ContactUpdate_Type$json], '8': [ {'1': '_avatarSvgCompressed'}, + {'1': '_username'}, {'1': '_displayName'}, ], }; @@ -418,21 +420,22 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'IoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJF' 'T1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZX' 'F1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5' - 'cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa8A' - 'EKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFj' + 'cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIang' + 'IKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFj' 'dFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdG' - 'FyU3ZnQ29tcHJlc3NlZIgBARIlCgtkaXNwbGF5TmFtZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgB' - 'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' - 'NlZEIOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0' - 'ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQ' - 'ESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0' - 'iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5Qg' - 'wKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1l' - 'Q291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudG' - 'VyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCDwoN' - 'X2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdG' - 'VCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFj' - 'dFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZX' - 'h0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVC' - 'FwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5'); + 'FyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIlCgtk' + 'aXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQAB' + 'IKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEILCglfdXNlcm5hbWVCDgoMX2Rp' + 'c3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC' + '5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgD' + 'IAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeX' + 'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRl' + 'ZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNg' + 'oWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIe' + 'CgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdE' + 'NoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRp' + 'YUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0Qg' + 'wKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VC' + 'DgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbm' + 'RHcm91cFB1YmxpY0tleQ=='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index dc21e61..96264fc 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -148,7 +148,8 @@ message EncryptedContent { Type type = 1; optional bytes avatarSvgCompressed = 2; - optional string displayName = 3; + optional string username = 3; + optional string displayName = 4; } message PushKeys { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index ae7ff3b..41d7498 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -490,7 +490,7 @@ class ApiService { Future getUserById(int userId) async { final get = ApplicationData_GetUserById()..userId = Int64(userId); - final appData = ApplicationData()..getuserbyid = get; + final appData = ApplicationData()..getUserById = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -504,21 +504,21 @@ class ApiService { Future downloadDone(List token) async { final get = ApplicationData_DownloadDone()..downloadToken = token; - final appData = ApplicationData()..downloaddone = get; + final appData = ApplicationData()..downloadDone = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, ensureRetransmission: true); } Future getCurrentLocation() async { final get = ApplicationData_GetLocation(); - final appData = ApplicationData()..getlocation = get; + final appData = ApplicationData()..getLocation = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future getUserData(String username) async { final get = ApplicationData_GetUserByUsername()..username = username; - final appData = ApplicationData()..getuserbyusername = get; + final appData = ApplicationData()..getUserByUsername = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -532,7 +532,7 @@ class ApiService { Future getPlanBallance() async { final get = ApplicationData_GetCurrentPlanInfos(); - final appData = ApplicationData()..getcurrentplaninfos = get; + final appData = ApplicationData()..getCurrentPlanInfos = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -546,7 +546,7 @@ class ApiService { Future getVoucherList() async { final get = ApplicationData_GetVouchers(); - final appData = ApplicationData()..getvouchers = get; + final appData = ApplicationData()..getVouchers = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -560,7 +560,7 @@ class ApiService { Future?> getAdditionalUserInvites() async { final get = ApplicationData_GetAddAccountsInvites(); - final appData = ApplicationData()..getaddaccountsinvites = get; + final appData = ApplicationData()..getAddaccountsInvites = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -574,21 +574,21 @@ class ApiService { Future updatePlanOptions(bool autoRenewal) async { final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal; - final appData = ApplicationData()..updateplanoptions = get; + final appData = ApplicationData()..updatePlanOptions = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future removeAdditionalUser(Int64 userId) async { final get = ApplicationData_RemoveAdditionalUser()..userId = userId; - final appData = ApplicationData()..removeadditionaluser = get; + final appData = ApplicationData()..removeAdditionalUser = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, contactId: userId.toInt()); } Future buyVoucher(int valueInCents) async { final get = ApplicationData_CreateVoucher()..valueCents = valueInCents; - final appData = ApplicationData()..createvoucher = get; + final appData = ApplicationData()..createVoucher = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -602,14 +602,14 @@ class ApiService { ..planId = planId ..payMonthly = payMonthly ..autoRenewal = autoRenewal; - final appData = ApplicationData()..switchtopayedplan = get; + final appData = ApplicationData()..switchtoPayedPlan = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future redeemVoucher(String voucher) async { final get = ApplicationData_RedeemVoucher()..voucher = voucher; - final appData = ApplicationData()..redeemvoucher = get; + final appData = ApplicationData()..redeemVoucher = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -618,28 +618,35 @@ class ApiService { final get = ApplicationData_ReportUser() ..reportedUserId = Int64(userId) ..reason = reason; - final appData = ApplicationData()..reportuser = get; + final appData = ApplicationData()..reportUser = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future deleteAccount() async { final get = ApplicationData_DeleteAccount(); - final appData = ApplicationData()..deleteaccount = get; + final appData = ApplicationData()..deleteAccount = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future redeemUserInviteCode(String inviteCode) async { final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode; - final appData = ApplicationData()..redeemadditionalcode = get; + final appData = ApplicationData()..redeemAdditionalCode = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future updateFCMToken(String googleFcm) async { final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; - final appData = ApplicationData()..updategooglefcmtoken = get; + final appData = ApplicationData()..updateGoogleFcmToken = get; + final req = createClientToServerFromApplicationData(appData); + return sendRequestSync(req); + } + + Future changeUsername(String username) async { + final get = ApplicationData_ChangeUsername()..username = username; + final appData = ApplicationData()..changeUsername = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -653,7 +660,7 @@ class ApiService { ..signedPrekeyId = Int64(signedPreKeyId) ..signedPrekey = signedPreKey ..signedPrekeySignature = signedPreKeySignature; - final appData = ApplicationData()..updatesignedprekey = get; + final appData = ApplicationData()..updateSignedPrekey = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -661,7 +668,7 @@ class ApiService { Future getSignedKeyByUserId(int userId) async { final get = ApplicationData_GetSignedPreKeyByUserId() ..userId = Int64(userId); - final appData = ApplicationData()..getsignedprekeybyuserid = get; + final appData = ApplicationData()..getSignedPrekeyByUserid = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req, contactId: userId); if (res.isSuccess) { @@ -675,7 +682,7 @@ class ApiService { Future getPreKeysByUserId(int userId) async { final get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId); - final appData = ApplicationData()..getprekeysbyuserid = get; + final appData = ApplicationData()..getPrekeysByUserId = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req, contactId: userId); if (res.isSuccess) { @@ -709,8 +716,7 @@ class ApiService { if (pushData != null) { testMessage.pushData = pushData; } - - final appData = ApplicationData()..textmessage = testMessage; + final appData = ApplicationData()..textMessage = testMessage; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, contactId: target); } diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 61376c4..607c872 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -99,6 +99,7 @@ Future handleContactUpdate( Log.info('Got a contact update $fromUserId'); if (contactUpdate.hasAvatarSvgCompressed() && contactUpdate.hasDisplayName() && + contactUpdate.hasUsername() && senderProfileCounter != null) { await twonlyDB.contactsDao.updateContact( fromUserId, @@ -106,6 +107,7 @@ Future handleContactUpdate( avatarSvgCompressed: Value(Uint8List.fromList(contactUpdate.avatarSvgCompressed)), displayName: Value(contactUpdate.displayName), + username: Value(contactUpdate.username), senderProfileCounter: Value(senderProfileCounter), ), ); @@ -153,9 +155,11 @@ Future checkForProfileUpdate( if (contact.senderProfileCounter < senderProfileCounter) { await sendCipherText( fromUserId, - EncryptedContent() - ..contactUpdate = (EncryptedContent_ContactUpdate() - ..type = EncryptedContent_ContactUpdate_Type.REQUEST), + EncryptedContent( + contactUpdate: EncryptedContent_ContactUpdate( + type: EncryptedContent_ContactUpdate_Type.REQUEST, + ), + ), ); } } diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 39f5eb7..d4fc7c1 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -88,7 +88,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); for (final message in messages) { final contacts = - await twonlyDB.groupsDao.getGroupMembers(message.groupId); + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); for (final contact in contacts) { await twonlyDB.messagesDao.handleMessageAckByServer( contact.contactId, diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 8b5aea9..52b3f53 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -159,7 +159,7 @@ Future _createUploadRequest(MediaFileService media) async { for (final message in messages) { final groupMembers = - await twonlyDB.groupsDao.getGroupMembers(message.groupId); + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); if (media.mediaFile.reuploadRequestedBy == null) { await twonlyDB.groupsDao.incFlameCounter( diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 8a7497e..b2503fc 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -199,7 +199,7 @@ Future sendCipherTextToGroup( pb.EncryptedContent encryptedContent, { String? messageId, }) async { - final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); + final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); @@ -291,6 +291,7 @@ Future notifyContactsAboutProfileChange({int? onlyToContact}) async { type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, avatarSvgCompressed: gzip.encode(utf8.encode(gUser.avatarSvg!)), displayName: gUser.displayName, + username: gUser.username, ), ); diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index c28d59e..9f05b1c 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -36,7 +36,7 @@ Future syncFlameCounters() async { if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue; final groupMembers = - await twonlyDB.groupsDao.getGroupMembers(group.groupId); + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); if (groupMembers.length != 1) { continue; // flame sync is only done for groups of two } diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 3c3f5de..a80d22b 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -353,7 +353,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { ); var currentGroupMembers = - await twonlyDB.groupsDao.getGroupMembers(group.groupId); + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); // First find and insert NEW members for (final memberId in memberIds) { @@ -407,7 +407,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { // update the current members list currentGroupMembers = - await twonlyDB.groupsDao.getGroupMembers(group.groupId); + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); for (final member in currentGroupMembers) { // Member is not any more in the members list diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 5b48640..cbeffcc 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -295,8 +295,9 @@ class _ChatMessagesViewState extends State { onTap: () async { if (group.isDirectChat) { final member = - await twonlyDB.groupsDao.getGroupMembers(group.groupId); + await twonlyDB.groupsDao.getAllGroupMembers(group.groupId); if (!context.mounted) return; + if (member.isEmpty) return; await Navigator.push( context, MaterialPageRoute( diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 8659f75..56eefe5 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -1,13 +1,15 @@ import 'dart:async'; - import 'package:avatar_maker/avatar_maker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart'; class ProfileView extends StatefulWidget { @@ -18,19 +20,12 @@ class ProfileView extends StatefulWidget { } class _ProfileViewState extends State { - UserData? user; final AvatarMakerController _avatarMakerController = PersistentAvatarMakerController(customizedPropertyCategories: []); @override void initState() { super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - user = await getUser(); - setState(() {}); } Future updateUserDisplayName(String displayName) async { @@ -40,9 +35,38 @@ class _ProfileViewState extends State { ..avatarCounter = user.avatarCounter + 1; return user; }); - await notifyContactsAboutProfileChange(); - await initAsync(); + setState(() {}); // gUser has updated + } + + Future _updateUsername(String username) async { + final result = await apiService.changeUsername(username); + if (result.isError) { + if (!mounted) return; + + if (result.error == ErrorCode.UsernameAlreadyTaken) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.errorUsernameAlreadyTaken), + duration: const Duration(seconds: 3), + ), + ); + return; + } + + showNetworkIssue(context); + + return; + } + + await updateUserdata((user) { + user + ..username = username + ..avatarCounter = user.avatarCounter + 1; + return user; + }); + await notifyContactsAboutProfileChange(); + setState(() {}); // gUser has updated } @override @@ -82,13 +106,45 @@ class _ProfileViewState extends State { ), const SizedBox(height: 20), const Divider(), + BetterListTile( + leading: const Padding( + padding: EdgeInsets.only(right: 5, left: 1), + child: FaIcon( + FontAwesomeIcons.at, + size: 20, + ), + ), + text: context.lang.registerUsernameDecoration, + subtitle: Text(gUser.username), + onTap: () async { + final username = await showDisplayNameChangeDialog( + context, + gUser.username, + context.lang.registerUsernameDecoration, + context.lang.registerUsernameDecoration, + maxLength: 12, + inputFormatters: [ + LengthLimitingTextInputFormatter(12), + FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')), + ], + ); + if (context.mounted && username != null && username != '') { + await _updateUsername(username); + } + }, + ), BetterListTile( icon: FontAwesomeIcons.userPen, text: context.lang.settingsProfileEditDisplayName, - subtitle: (user == null) ? null : Text(user!.displayName), + subtitle: Text(gUser.displayName), onTap: () async { - final displayName = - await showDisplayNameChangeDialog(context, user!.displayName); + final displayName = await showDisplayNameChangeDialog( + context, + gUser.displayName, + context.lang.settingsProfileEditDisplayName, + context.lang.settingsProfileEditDisplayNameNew, + maxLength: 30, + ); if (context.mounted && displayName != null && displayName != '') { await updateUserDisplayName(displayName); } @@ -103,34 +159,38 @@ class _ProfileViewState extends State { Future showDisplayNameChangeDialog( BuildContext context, String currentName, -) { + String title, + String hintText, { + List? inputFormatters, + int? maxLength, +}) { final controller = TextEditingController(text: currentName); return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: Text(context.lang.settingsProfileEditDisplayName), + title: Text(title), content: TextField( controller: controller, autofocus: true, - maxLength: 30, + inputFormatters: inputFormatters, + maxLength: maxLength, decoration: InputDecoration( - hintText: context.lang.settingsProfileEditDisplayNameNew, + hintText: hintText, ), ), actions: [ TextButton( child: Text(context.lang.cancel), onPressed: () { - Navigator.of(context).pop(); // Close the dialog + Navigator.of(context).pop(); }, ), TextButton( child: Text(context.lang.ok), onPressed: () { - Navigator.of(context) - .pop(controller.text); // Return the input text + Navigator.of(context).pop(controller.text); }, ), ], From 8d45c8e9ce2286350c147c30aa95bd8260c06576 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 5 Nov 2025 23:12:10 +0100 Subject: [PATCH 64/76] fix #289 --- .../NotificationService.swift | 7 +- lib/src/database/daos/groups.dao.dart | 12 +- lib/src/database/tables/groups.table.dart | 3 + lib/src/database/twonly.db.g.dart | 98 +++++++- lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 4 +- .../generated/app_localizations.dart | 12 + .../generated/app_localizations_de.dart | 10 + .../generated/app_localizations_en.dart | 10 + .../client/generated/messages.pb.dart | 14 ++ .../client/generated/messages.pbjson.dart | 98 ++++---- lib/src/model/protobuf/client/messages.proto | 1 + .../api/client2client/groups.c2c.dart | 10 + .../twonly_safe/common.twonly_safe.dart | 6 +- .../image_editor/modules/all_emojis.dart | 102 ++++---- lib/src/views/chats/chat_messages.view.dart | 1 - .../all_reactions.bottom_sheet.dart | 2 +- .../chat_group_action.dart | 9 + lib/src/views/chats/media_viewer.view.dart | 5 + .../emoji_reactions_row.component.dart | 103 ++++---- .../reaction_buttons.component.dart | 227 +++++++++++++++++- .../components/avatar_icon.component.dart | 10 +- .../group_context_menu.component.dart | 15 +- .../views/settings/profile/profile.view.dart | 6 + 24 files changed, 588 insertions(+), 181 deletions(-) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index d3e095e..718417c 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -229,7 +229,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", .response: "hat dir{inGroup} geantwortet.", - .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt." + .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", ] } else { // Default to English pushNotificationText = [ @@ -247,7 +247,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToText: "has reacted with {{content}} to your text.", .reactionToImage: "has reacted with {{content}} to your image.", .response: "has responded{inGroup}.", - .addedToGroup: "has added you to \"{{content}}\"" + .addedToGroup: "has added you to \"{{content}}\"", ] } @@ -257,9 +257,10 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str content.replace("{{content}}", with: pushNotification.additionalContent) content.replace("{inGroup}", with: " in {inGroup}") content.replace("{inGroup}", with: pushNotification.additionalContent) + } else { + content.replace("{inGroup}", with: "") } // Return the corresponding message or an empty string if not found return (content, title) } - diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index c3920b5..92986e4 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -29,6 +29,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return entry != null; } + Future deleteGroup(String groupId) async { + await (delete(groups)..where((t) => t.groupId.equals(groupId))).go(); + } + Future updateGroup( String groupId, GroupsCompanion updates, @@ -42,7 +46,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ..where( (t) => t.groupId.equals(groupId) & - t.memberState.equals(MemberState.leftGroup.name).not(), + (t.memberState.equals(MemberState.leftGroup.name).not() | + t.memberState.isNull()), )) .get(); } @@ -131,8 +136,9 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { Future _insertGroup(GroupsCompanion group) async { try { - final rowId = await into(groups).insert(group); - return await (select(groups)..where((t) => t.rowId.equals(rowId))) + await into(groups).insert(group); + return await (select(groups) + ..where((t) => t.groupId.equals(group.groupId.value))) .getSingle(); } catch (e) { Log.error('Could not insert group: $e'); diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index e33cafe..32bbc1c 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -77,6 +77,7 @@ enum GroupActionType { promoteToAdmin, demoteToMember, updatedGroupName, + changeDisplayMaxTime, } @DataClassName('GroupHistory') @@ -94,6 +95,8 @@ class GroupHistories extends Table { TextColumn get oldGroupName => text().nullable()(); TextColumn get newGroupName => text().nullable()(); + IntColumn get newDeleteMessagesAfterMilliseconds => integer().nullable()(); + TextColumn get type => textEnum()(); DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 101d2ba..f3e9a93 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -7038,6 +7038,13 @@ class $GroupHistoriesTable extends GroupHistories late final GeneratedColumn newGroupName = GeneratedColumn( 'new_group_name', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _newDeleteMessagesAfterMillisecondsMeta = + const VerificationMeta('newDeleteMessagesAfterMilliseconds'); + @override + late final GeneratedColumn newDeleteMessagesAfterMilliseconds = + GeneratedColumn( + 'new_delete_messages_after_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); @override late final GeneratedColumnWithTypeConverter type = GeneratedColumn('type', aliasedName, false, @@ -7059,6 +7066,7 @@ class $GroupHistoriesTable extends GroupHistories affectedContactId, oldGroupName, newGroupName, + newDeleteMessagesAfterMilliseconds, type, actionAt ]; @@ -7108,6 +7116,13 @@ class $GroupHistoriesTable extends GroupHistories newGroupName.isAcceptableOrUnknown( data['new_group_name']!, _newGroupNameMeta)); } + if (data.containsKey('new_delete_messages_after_milliseconds')) { + context.handle( + _newDeleteMessagesAfterMillisecondsMeta, + newDeleteMessagesAfterMilliseconds.isAcceptableOrUnknown( + data['new_delete_messages_after_milliseconds']!, + _newDeleteMessagesAfterMillisecondsMeta)); + } if (data.containsKey('action_at')) { context.handle(_actionAtMeta, actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta)); @@ -7133,6 +7148,9 @@ class $GroupHistoriesTable extends GroupHistories .read(DriftSqlType.string, data['${effectivePrefix}old_group_name']), newGroupName: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}new_group_name']), + newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}new_delete_messages_after_milliseconds']), type: $GroupHistoriesTable.$convertertype.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}type'])!), @@ -7157,6 +7175,7 @@ class GroupHistory extends DataClass implements Insertable { final int? affectedContactId; final String? oldGroupName; final String? newGroupName; + final int? newDeleteMessagesAfterMilliseconds; final GroupActionType type; final DateTime actionAt; const GroupHistory( @@ -7166,6 +7185,7 @@ class GroupHistory extends DataClass implements Insertable { this.affectedContactId, this.oldGroupName, this.newGroupName, + this.newDeleteMessagesAfterMilliseconds, required this.type, required this.actionAt}); @override @@ -7185,6 +7205,10 @@ class GroupHistory extends DataClass implements Insertable { if (!nullToAbsent || newGroupName != null) { map['new_group_name'] = Variable(newGroupName); } + if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) { + map['new_delete_messages_after_milliseconds'] = + Variable(newDeleteMessagesAfterMilliseconds); + } { map['type'] = Variable($GroupHistoriesTable.$convertertype.toSql(type)); @@ -7209,6 +7233,10 @@ class GroupHistory extends DataClass implements Insertable { newGroupName: newGroupName == null && nullToAbsent ? const Value.absent() : Value(newGroupName), + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(newDeleteMessagesAfterMilliseconds), type: Value(type), actionAt: Value(actionAt), ); @@ -7224,6 +7252,8 @@ class GroupHistory extends DataClass implements Insertable { affectedContactId: serializer.fromJson(json['affectedContactId']), oldGroupName: serializer.fromJson(json['oldGroupName']), newGroupName: serializer.fromJson(json['newGroupName']), + newDeleteMessagesAfterMilliseconds: + serializer.fromJson(json['newDeleteMessagesAfterMilliseconds']), type: $GroupHistoriesTable.$convertertype .fromJson(serializer.fromJson(json['type'])), actionAt: serializer.fromJson(json['actionAt']), @@ -7239,6 +7269,8 @@ class GroupHistory extends DataClass implements Insertable { 'affectedContactId': serializer.toJson(affectedContactId), 'oldGroupName': serializer.toJson(oldGroupName), 'newGroupName': serializer.toJson(newGroupName), + 'newDeleteMessagesAfterMilliseconds': + serializer.toJson(newDeleteMessagesAfterMilliseconds), 'type': serializer .toJson($GroupHistoriesTable.$convertertype.toJson(type)), 'actionAt': serializer.toJson(actionAt), @@ -7252,6 +7284,7 @@ class GroupHistory extends DataClass implements Insertable { Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = const Value.absent(), GroupActionType? type, DateTime? actionAt}) => GroupHistory( @@ -7265,6 +7298,10 @@ class GroupHistory extends DataClass implements Insertable { oldGroupName.present ? oldGroupName.value : this.oldGroupName, newGroupName: newGroupName.present ? newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds.present + ? newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, type: type ?? this.type, actionAt: actionAt ?? this.actionAt, ); @@ -7284,6 +7321,10 @@ class GroupHistory extends DataClass implements Insertable { newGroupName: data.newGroupName.present ? data.newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + data.newDeleteMessagesAfterMilliseconds.present + ? data.newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, type: data.type.present ? data.type.value : this.type, actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, ); @@ -7298,6 +7339,8 @@ class GroupHistory extends DataClass implements Insertable { ..write('affectedContactId: $affectedContactId, ') ..write('oldGroupName: $oldGroupName, ') ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') ..write('type: $type, ') ..write('actionAt: $actionAt') ..write(')')) @@ -7305,8 +7348,16 @@ class GroupHistory extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(groupHistoryId, groupId, contactId, - affectedContactId, oldGroupName, newGroupName, type, actionAt); + int get hashCode => Object.hash( + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt); @override bool operator ==(Object other) => identical(this, other) || @@ -7317,6 +7368,8 @@ class GroupHistory extends DataClass implements Insertable { other.affectedContactId == this.affectedContactId && other.oldGroupName == this.oldGroupName && other.newGroupName == this.newGroupName && + other.newDeleteMessagesAfterMilliseconds == + this.newDeleteMessagesAfterMilliseconds && other.type == this.type && other.actionAt == this.actionAt); } @@ -7328,6 +7381,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { final Value affectedContactId; final Value oldGroupName; final Value newGroupName; + final Value newDeleteMessagesAfterMilliseconds; final Value type; final Value actionAt; final Value rowid; @@ -7338,6 +7392,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { this.affectedContactId = const Value.absent(), this.oldGroupName = const Value.absent(), this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), this.type = const Value.absent(), this.actionAt = const Value.absent(), this.rowid = const Value.absent(), @@ -7349,6 +7404,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { this.affectedContactId = const Value.absent(), this.oldGroupName = const Value.absent(), this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), required GroupActionType type, this.actionAt = const Value.absent(), this.rowid = const Value.absent(), @@ -7362,6 +7418,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { Expression? affectedContactId, Expression? oldGroupName, Expression? newGroupName, + Expression? newDeleteMessagesAfterMilliseconds, Expression? type, Expression? actionAt, Expression? rowid, @@ -7373,6 +7430,9 @@ class GroupHistoriesCompanion extends UpdateCompanion { if (affectedContactId != null) 'affected_contact_id': affectedContactId, if (oldGroupName != null) 'old_group_name': oldGroupName, if (newGroupName != null) 'new_group_name': newGroupName, + if (newDeleteMessagesAfterMilliseconds != null) + 'new_delete_messages_after_milliseconds': + newDeleteMessagesAfterMilliseconds, if (type != null) 'type': type, if (actionAt != null) 'action_at': actionAt, if (rowid != null) 'rowid': rowid, @@ -7386,6 +7446,7 @@ class GroupHistoriesCompanion extends UpdateCompanion { Value? affectedContactId, Value? oldGroupName, Value? newGroupName, + Value? newDeleteMessagesAfterMilliseconds, Value? type, Value? actionAt, Value? rowid}) { @@ -7396,6 +7457,8 @@ class GroupHistoriesCompanion extends UpdateCompanion { affectedContactId: affectedContactId ?? this.affectedContactId, oldGroupName: oldGroupName ?? this.oldGroupName, newGroupName: newGroupName ?? this.newGroupName, + newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds ?? + this.newDeleteMessagesAfterMilliseconds, type: type ?? this.type, actionAt: actionAt ?? this.actionAt, rowid: rowid ?? this.rowid, @@ -7423,6 +7486,10 @@ class GroupHistoriesCompanion extends UpdateCompanion { if (newGroupName.present) { map['new_group_name'] = Variable(newGroupName.value); } + if (newDeleteMessagesAfterMilliseconds.present) { + map['new_delete_messages_after_milliseconds'] = + Variable(newDeleteMessagesAfterMilliseconds.value); + } if (type.present) { map['type'] = Variable( $GroupHistoriesTable.$convertertype.toSql(type.value)); @@ -7445,6 +7512,8 @@ class GroupHistoriesCompanion extends UpdateCompanion { ..write('affectedContactId: $affectedContactId, ') ..write('oldGroupName: $oldGroupName, ') ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') ..write('type: $type, ') ..write('actionAt: $actionAt, ') ..write('rowid: $rowid') @@ -13359,6 +13428,7 @@ typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion Value affectedContactId, Value oldGroupName, Value newGroupName, + Value newDeleteMessagesAfterMilliseconds, required GroupActionType type, Value actionAt, Value rowid, @@ -13371,6 +13441,7 @@ typedef $$GroupHistoriesTableUpdateCompanionBuilder = GroupHistoriesCompanion Value affectedContactId, Value oldGroupName, Value newGroupName, + Value newDeleteMessagesAfterMilliseconds, Value type, Value actionAt, Value rowid, @@ -13445,6 +13516,11 @@ class $$GroupHistoriesTableFilterComposer ColumnFilters get newGroupName => $composableBuilder( column: $table.newGroupName, builder: (column) => ColumnFilters(column)); + ColumnFilters get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters get type => $composableBuilder( column: $table.type, @@ -13535,6 +13611,11 @@ class $$GroupHistoriesTableOrderingComposer column: $table.newGroupName, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); @@ -13620,6 +13701,11 @@ class $$GroupHistoriesTableAnnotationComposer GeneratedColumn get newGroupName => $composableBuilder( column: $table.newGroupName, builder: (column) => column); + GeneratedColumn get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => column); + GeneratedColumnWithTypeConverter get type => $composableBuilder(column: $table.type, builder: (column) => column); @@ -13717,6 +13803,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = + const Value.absent(), Value type = const Value.absent(), Value actionAt = const Value.absent(), Value rowid = const Value.absent(), @@ -13728,6 +13816,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< affectedContactId: affectedContactId, oldGroupName: oldGroupName, newGroupName: newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds, type: type, actionAt: actionAt, rowid: rowid, @@ -13739,6 +13829,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< Value affectedContactId = const Value.absent(), Value oldGroupName = const Value.absent(), Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = + const Value.absent(), required GroupActionType type, Value actionAt = const Value.absent(), Value rowid = const Value.absent(), @@ -13750,6 +13842,8 @@ class $$GroupHistoriesTableTableManager extends RootTableManager< affectedContactId: affectedContactId, oldGroupName: oldGroupName, newGroupName: newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds, type: type, actionAt: actionAt, rowid: rowid, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 279201e..3c1f557 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -804,5 +804,7 @@ "leaveGroupSelectOtherAdminBody": "Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.", "leaveGroupSureTitle": "Gruppe verlassen", "leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?", - "leaveGroupSureOkBtn": "Gruppe verlassen" + "leaveGroupSureOkBtn": "Gruppe verlassen", + "changeDisplayMaxTime": "{username} hat das Zeitlimit für verschwindende Nachrichten auf {time}.", + "youChangedDisplayMaxTime": "Du hat das Zeitlimit für verschwindende Nachrichten auf {time}." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 909fa8a..bb5db7b 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -582,5 +582,7 @@ "leaveGroupSelectOtherAdminBody": "To leave the group, you must first select a new administrator.", "leaveGroupSureTitle": "Leave group", "leaveGroupSureBody": "Do you really want to leave the group?", - "leaveGroupSureOkBtn": "Leave group" + "leaveGroupSureOkBtn": "Leave group", + "changeDisplayMaxTime": "{username} has set the time limit for disappearing messages to {time}.", + "youChangedDisplayMaxTime": "You have set the time limit for disappearing messages to {time}." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d238c71..5567323 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2599,6 +2599,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Leave group'** String get leaveGroupSureOkBtn; + + /// No description provided for @changeDisplayMaxTime. + /// + /// In en, this message translates to: + /// **'{username} has set the time limit for disappearing messages to {time}.'** + String changeDisplayMaxTime(Object time, Object username); + + /// No description provided for @youChangedDisplayMaxTime. + /// + /// In en, this message translates to: + /// **'You have set the time limit for disappearing messages to {time}.'** + String youChangedDisplayMaxTime(Object time); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 37b0129..adc25c5 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1421,4 +1421,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get leaveGroupSureOkBtn => 'Gruppe verlassen'; + + @override + String changeDisplayMaxTime(Object time, Object username) { + return '$username hat das Zeitlimit für verschwindende Nachrichten auf $time.'; + } + + @override + String youChangedDisplayMaxTime(Object time) { + return 'Du hat das Zeitlimit für verschwindende Nachrichten auf $time.'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 913445b..19de928 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1411,4 +1411,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get leaveGroupSureOkBtn => 'Leave group'; + + @override + String changeDisplayMaxTime(Object time, Object username) { + return '$username has set the time limit for disappearing messages to $time.'; + } + + @override + String youChangedDisplayMaxTime(Object time) { + return 'You have set the time limit for disappearing messages to $time.'; + } } diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index baefcd7..2586ee4 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -415,6 +415,7 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { $core.String? groupActionType, $fixnum.Int64? affectedContactId, $core.String? newGroupName, + $fixnum.Int64? newDeleteMessagesAfterMilliseconds, }) { final $result = create(); if (groupActionType != null) { @@ -426,6 +427,9 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { if (newGroupName != null) { $result.newGroupName = newGroupName; } + if (newDeleteMessagesAfterMilliseconds != null) { + $result.newDeleteMessagesAfterMilliseconds = newDeleteMessagesAfterMilliseconds; + } return $result; } EncryptedContent_GroupUpdate._() : super(); @@ -436,6 +440,7 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { ..aOS(1, _omitFieldNames ? '' : 'groupActionType', protoName: 'groupActionType') ..aInt64(2, _omitFieldNames ? '' : 'affectedContactId', protoName: 'affectedContactId') ..aOS(3, _omitFieldNames ? '' : 'newGroupName', protoName: 'newGroupName') + ..aInt64(4, _omitFieldNames ? '' : 'newDeleteMessagesAfterMilliseconds', protoName: 'newDeleteMessagesAfterMilliseconds') ..hasRequiredFields = false ; @@ -486,6 +491,15 @@ class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { $core.bool hasNewGroupName() => $_has(2); @$pb.TagNumber(3) void clearNewGroupName() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get newDeleteMessagesAfterMilliseconds => $_getI64(3); + @$pb.TagNumber(4) + set newDeleteMessagesAfterMilliseconds($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasNewDeleteMessagesAfterMilliseconds() => $_has(3); + @$pb.TagNumber(4) + void clearNewDeleteMessagesAfterMilliseconds() => clearField(4); } class EncryptedContent_TextMessage extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index fc171fe..37824d4 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -170,10 +170,12 @@ const EncryptedContent_GroupUpdate$json = { {'1': 'groupActionType', '3': 1, '4': 1, '5': 9, '10': 'groupActionType'}, {'1': 'affectedContactId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'affectedContactId', '17': true}, {'1': 'newGroupName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'newGroupName', '17': true}, + {'1': 'newDeleteMessagesAfterMilliseconds', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'newDeleteMessagesAfterMilliseconds', '17': true}, ], '8': [ {'1': '_affectedContactId'}, {'1': '_newGroupName'}, + {'1': '_newDeleteMessagesAfterMilliseconds'}, ], }; @@ -389,53 +391,55 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' 'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ' 'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym' - 'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkaugEK' + 'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkatgIK' 'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj' 'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l' - 'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQFCFAoSX2FmZmVjdGVkQ29udGFjdE' - 'lkQg8KDV9uZXdHcm91cE5hbWUaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgB' - 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGA' - 'MgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdl' - 'SWSIAQFCEQoPX3F1b3RlTWVzc2FnZUlkGmIKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZB' - 'gBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVlbW9qaRIWCgZyZW1vdmUY' - 'AyABKAhSBnJlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdG' - 'VkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIg' - 'ASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAy' - 'ADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQES' - 'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRE' - 'lUX1RFWFQQARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVN' - 'ZWRpYRIoCg9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGA' - 'IgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1p' - 'dEluTWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEj' - 'YKFnJlcXVpcmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24S' - 'HAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAV' - 'IOcXVvdGVNZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRv' - 'a2VuiAEBEikKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbm' - 'NyeXB0aW9uTWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNl' - 'GAogASgMSAVSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU' - '1BR0UQARIJCgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k' - 'c0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZX' - 'lCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUS' - 'NgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZR' - 'IoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJF' - 'T1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZX' - 'F1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5' - 'cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIang' - 'IKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFj' - 'dFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdG' - 'FyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIlCgtk' - 'aXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQAB' - 'IKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEILCglfdXNlcm5hbWVCDgoMX2Rp' - 'c3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC' - '5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgD' - 'IAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeX' - 'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRl' - 'ZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNg' - 'oWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIe' - 'CgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdE' - 'NoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRp' - 'YUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0Qg' - 'wKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VC' - 'DgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbm' - 'RHcm91cFB1YmxpY0tleQ=='); + 'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESUwoibmV3RGVsZXRlTWVzc2FnZX' + 'NBZnRlck1pbGxpc2Vjb25kcxgEIAEoA0gCUiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlz' + 'ZWNvbmRziAEBQhQKEl9hZmZlY3RlZENvbnRhY3RJZEIPCg1fbmV3R3JvdXBOYW1lQiUKI19uZX' + 'dEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGqkBCgtUZXh0TWVzc2FnZRIoCg9zZW5k' + 'ZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh' + 'wKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJSABS' + 'DnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg90YX' + 'JnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1v' + 'amkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIA' + 'EoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRl' + 'ck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZX' + 'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo' + 'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE' + 'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH' + 'CgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' + 'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD' + 'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG' + 'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1' + 'dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2' + 'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI' + 'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2' + '5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu' + 'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRV' + 'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0' + 'SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl' + '9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEK' + 'C01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYX' + 'RlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQi' + 'NgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAh' + 'p4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250' + 'YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEg' + 'oKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRD' + 'b250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGA' + 'IgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNl' + 'cm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZR' + 'ILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3Vz' + 'ZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3' + 'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ' + 'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' + 'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' + 'ZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' + 'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' + 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZE' + 'IPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVw' + 'ZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb2' + '50YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoM' + 'X3RleHRNZXNzYWdlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZG' + 'F0ZUIXChVfcmVzZW5kR3JvdXBQdWJsaWNLZXk='); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 96264fc..72b3230 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -72,6 +72,7 @@ message EncryptedContent { string groupActionType = 1; // GroupActionType.name optional int64 affectedContactId = 2; optional string newGroupName = 3; + optional int64 newDeleteMessagesAfterMilliseconds = 4; } message TextMessage { diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index ca5b906..f193911 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -128,6 +128,16 @@ Future handleGroupUpdate( contactId: Value(fromUserId), ), ); + case GroupActionType.changeDisplayMaxTime: + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + newDeleteMessagesAfterMilliseconds: + Value(update.newDeleteMessagesAfterMilliseconds.toInt()), + contactId: Value(fromUserId), + ), + ); case GroupActionType.removedMember: case GroupActionType.addMember: case GroupActionType.leftGroup: diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 5e022b4..530328a 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -24,7 +24,7 @@ Future enableTwonlySafe(String password) async { unawaited(performTwonlySafeBackup(force: true)); } -Future disableTwonlySafe() async { +Future removeTwonlySafeFromServer() async { final serverUrl = await getTwonlySafeBackupUrl(); if (serverUrl != null) { try { @@ -40,10 +40,6 @@ Future disableTwonlySafe() async { Log.error('Could not connect to the server.'); } } - await updateUserdata((user) { - user.twonlySafeBackup = null; - return user; - }); } Future<(Uint8List, Uint8List)> getMasterKey( diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index 3ab1180..7926084 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -26,61 +26,63 @@ class EmojiPickerBottom extends StatelessWidget { ), ], ), - child: Column( - children: [ - Container( - margin: const EdgeInsets.all(30), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - color: Colors.grey, + child: SafeArea( + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, ), - height: 3, - width: 60, - ), - Expanded( - child: EmojiPicker( - onEmojiSelected: (category, emoji) { - Navigator.pop( - context, - EmojiLayerData( - text: emoji.emoji, + Expanded( + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + Navigator.pop( + context, + EmojiLayerData( + text: emoji.emoji, + ), + ); + }, + // textEditingController: _textFieldController, + config: Config( + height: 400, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, ), - ); - }, - // textEditingController: _textFieldController, - config: Config( - height: 400, - locale: Localizations.localeOf(context), - viewOrderConfig: const ViewOrderConfig( - top: EmojiPickerItem.searchBar, - // middle: EmojiPickerItem.emojiView, - bottom: EmojiPickerItem.categoryBar, - ), - emojiTextStyle: - TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), - emojiViewConfig: EmojiViewConfig( - backgroundColor: context.color.surfaceContainer, - ), - searchViewConfig: SearchViewConfig( - backgroundColor: context.color.surfaceContainer, - buttonIconColor: Colors.white, - ), - categoryViewConfig: CategoryViewConfig( - backgroundColor: context.color.surfaceContainer, - dividerColor: Colors.white, - indicatorColor: context.color.primary, - iconColorSelected: context.color.primary, - iconColor: context.color.secondary, - ), - bottomActionBarConfig: BottomActionBarConfig( - backgroundColor: context.color.surfaceContainer, - buttonColor: context.color.surfaceContainer, - buttonIconColor: context.color.secondary, ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index cbeffcc..abd8a04 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -114,7 +114,6 @@ class _ChatMessagesViewState extends State { groupActionsSub?.cancel(); lastOpenedMessageByContactSub?.cancel(); tutorial?.cancel(); - textFieldFocus.dispose(); super.dispose(); } diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index a01b66b..8bd31d3 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -64,7 +64,7 @@ class _AllReactionsViewState extends State { ), ), ); - if (mounted) Navigator.pop(context); + // if (mounted) Navigator.pop(context); } @override diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index f2c2826..e547d6e 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -55,6 +55,15 @@ class _ChatGroupActionState extends State { final maker = (contact == null) ? '' : getContactDisplayName(contact!); switch (widget.action.type) { + case GroupActionType.changeDisplayMaxTime: + final time = formatDuration( + context, + (widget.action.newDeleteMessagesAfterMilliseconds ?? 0 / 1000) as int, + ); + text = (contact == null) + ? context.lang.youChangedDisplayMaxTime(time) + : context.lang.changeDisplayMaxTime(maker, time); + icon = FontAwesomeIcons.pencil; case GroupActionType.updatedGroupName: text = (contact == null) ? context.lang.youChangedGroupName(widget.action.newGroupName!) diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 5350601..e81347c 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -55,6 +55,7 @@ class _MediaViewerViewState extends State { bool imageSaved = false; bool imageSaving = false; bool displayTwonlyPresent = true; + final emojiKey = GlobalKey(); StreamSubscription? downloadStateListener; @@ -634,6 +635,7 @@ class _MediaViewerViewState extends State { mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, groupId: widget.group.groupId, messageId: currentMessage!.messageId, + emojiKey: emojiKey, hide: () { setState(() { showShortReactions = false; @@ -641,6 +643,9 @@ class _MediaViewerViewState extends State { }); }, ), + Positioned.fill( + child: EmojiFloatWidget(key: emojiKey), + ), ], ), ), diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart index d6fc8d3..2f3a871 100644 --- a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -1,11 +1,46 @@ -// ignore_for_file: avoid_dynamic_calls - import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; +Offset getGlobalOffset(GlobalKey targetKey) { + final ctx = targetKey.currentContext; + if (ctx == null) { + return Offset.zero; + } + final renderObject = ctx.findRenderObject(); + if (renderObject is RenderBox) { + return renderObject.localToGlobal( + Offset(renderObject.size.width / 2, renderObject.size.height / 2), + ); + } + return Offset.zero; +} + +Future sendReaction( + String groupId, + String messageId, + String emoji, +) async { + await twonlyDB.reactionsDao.updateMyReaction( + messageId, + emoji, + false, + ); + await sendCipherTextToGroup( + groupId, + EncryptedContent( + reaction: EncryptedContent_Reaction( + targetMessageId: messageId, + emoji: emoji, + remove: false, + ), + ), + ); +} + class EmojiReactionWidget extends StatefulWidget { const EmojiReactionWidget({ required this.messageId, @@ -13,74 +48,46 @@ class EmojiReactionWidget extends StatefulWidget { required this.hide, required this.show, required this.emoji, + required this.emojiKey, super.key, }); final String messageId; final String groupId; - final Function hide; + final void Function() hide; final bool show; final String emoji; + final GlobalKey emojiKey; @override State createState() => _EmojiReactionWidgetState(); } class _EmojiReactionWidgetState extends State { - int selectedShortReaction = -1; + final GlobalKey _targetKey = GlobalKey(); @override Widget build(BuildContext context) { return AnimatedSize( + key: _targetKey, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut, child: GestureDetector( onTap: () async { - await twonlyDB.reactionsDao - .updateMyReaction(widget.messageId, widget.emoji, false); - - await sendCipherTextToGroup( - widget.groupId, - EncryptedContent( - reaction: EncryptedContent_Reaction( - targetMessageId: widget.messageId, - emoji: widget.emoji, - remove: false, - ), - ), + await sendReaction(widget.groupId, widget.messageId, widget.emoji); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_targetKey), + widget.emoji, ); - - setState(() { - selectedShortReaction = 0; // Assuming index is 0 for this example - }); - Future.delayed(const Duration(milliseconds: 300), () { - if (mounted) { - setState(() { - widget.hide(); - selectedShortReaction = -1; - }); - } - }); + widget.hide(); }, - child: (selectedShortReaction == - 0) // Assuming index is 0 for this example - ? EmojiAnimationFlying( - emoji: widget.emoji, - duration: const Duration(milliseconds: 300), - startPosition: 0, - size: (widget.show) ? 40 : 10, - ) - : AnimatedOpacity( - opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: SizedBox( - width: widget.show ? 40 : 10, - child: Center( - child: EmojiAnimation( - emoji: widget.emoji, - ), - ), - ), - ), + child: SizedBox( + width: widget.show ? 40 : 10, + child: Center( + child: EmojiAnimation( + emoji: widget.emoji, + ), + ), + ), ), ); } diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart index d5678fa..5da9f3d 100644 --- a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -1,6 +1,12 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -11,6 +17,7 @@ class ReactionButtons extends StatefulWidget { required this.mediaViewerDistanceFromBottom, required this.messageId, required this.groupId, + required this.emojiKey, required this.hide, super.key, }); @@ -18,6 +25,7 @@ class ReactionButtons extends StatefulWidget { final double mediaViewerDistanceFromBottom; final bool show; final bool textInputFocused; + final GlobalKey emojiKey; final String messageId; final String groupId; final void Function() hide; @@ -28,6 +36,7 @@ class ReactionButtons extends StatefulWidget { class _ReactionButtonsState extends State { int selectedShortReaction = -1; + final GlobalKey _keyEmojiPicker = GlobalKey(); List selectedEmojis = EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); @@ -82,6 +91,7 @@ class _ReactionButtonsState extends State { hide: widget.hide, show: widget.show, emoji: emoji as String, + emojiKey: widget.emojiKey, ), ) .toList(), @@ -90,17 +100,53 @@ class _ReactionButtonsState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, - children: firstRowEmojis - .map( - (emoji) => EmojiReactionWidget( - messageId: widget.messageId, - groupId: widget.groupId, - hide: widget.hide, - show: widget.show, - emoji: emoji, + children: [ + ...firstRowEmojis.map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: widget.show, + emoji: emoji, + emojiKey: widget.emojiKey, + ), + ), + GestureDetector( + key: _keyEmojiPicker, + onTap: () async { + // ignore: inference_failure_on_function_invocation + final layer = await showModalBottomSheet( + context: context, + backgroundColor: context.color.surface, + builder: (BuildContext context) { + return const EmojiPickerBottom(); + }, + ) as EmojiLayerData?; + if (layer == null) return; + await sendReaction( + widget.groupId, + widget.messageId, + layer.text, + ); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_keyEmojiPicker), + layer.text, + ); + widget.hide(); + }, + child: Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer.withAlpha(100), + borderRadius: BorderRadius.circular(12), ), - ) - .toList(), + padding: const EdgeInsets.all(8), + child: const FaIcon( + FontAwesomeIcons.ellipsisVertical, + size: 24, + ), + ), + ), + ], ), ], ), @@ -109,3 +155,164 @@ class _ReactionButtonsState extends State { ); } } + +class EmojiFloatWidget extends StatefulWidget { + const EmojiFloatWidget({ + super.key, + }); + @override + EmojiFloatWidgetState createState() => EmojiFloatWidgetState(); +} + +class EmojiFloatWidgetState extends State + with SingleTickerProviderStateMixin { + final List<_Particle> _particles = []; + late final Ticker _ticker; + final Random _rnd = Random(); + Duration _lastTick = Duration.zero; + + @override + void initState() { + super.initState(); + _ticker = createTicker(_tick)..start(); + } + + void _tick(Duration elapsed) { + final dt = (_lastTick == Duration.zero) + ? 0.016 + : (elapsed - _lastTick).inMicroseconds / 1e6; + _lastTick = elapsed; + + for (final p in List<_Particle>.from(_particles)) { + p.update(dt); + if (p.isDead) _particles.remove(p); + } + if (mounted) setState(() {}); + } + + @override + void dispose() { + _ticker.dispose(); + super.dispose(); + } + + /// Call this to spawn the emoji animation from a global screen position. + void spawn(Offset globalPosition, String emoji) { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return; + final local = box.globalToLocal(globalPosition); + const spawnCount = 10; + final life = const Duration(milliseconds: 2000).inMilliseconds / 1000.0; + + for (var i = 0; i < spawnCount; i++) { + final dx = (_rnd.nextDouble() - 0.5) * 220; + final vx = dx; + final vy = -(100 + _rnd.nextDouble() * 80); + final rot = (_rnd.nextDouble() - 0.5) * 2; + final scale = 0.9 + _rnd.nextDouble() * 0.6; + + _particles.add( + _Particle( + emoji: emoji, + x: local.dx, + y: local.dy, + vx: vx, + vy: vy, + rotation: rot, + lifetime: life, + scale: scale, + ), + ); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: CustomPaint( + painter: _ParticlePainter(List<_Particle>.from(_particles)), + size: Size.infinite, + ), + ); + } +} + +class _Particle { + _Particle({ + required this.emoji, + required this.x, + required this.y, + required this.vx, + required this.vy, + required this.rotation, + required this.lifetime, + required this.scale, + }); + final String emoji; + double x; + double y; + double vx; + double vy; + double rotation; + double age = 0; + final double lifetime; + final double scale; + bool get isDead => age >= lifetime; + + void update(double dt) { + age += dt; + // vertical-only motion emphasis: mild gravity slows ascent then gently pulls down + vy += 100 * dt; // gravity (positive = down) + // slight horizontal drag to reduce sideways drift + vx *= 1 - 3.0 * dt; + // integrate position + x += vx * dt; + y += vy * dt; + // slow rotation decay + rotation *= 1 - 1.5 * dt; + } + + double get progress => (age / lifetime).clamp(0.0, 1.0); + + // opacity falls from 1 -> 0 as particle ages + double get opacity => (1.0 - progress).clamp(0.0, 1.0); + + // scale can gently grow then shrink; here we slightly increase early + double get currentScale { + final p = progress; + if (p < 0.5) return scale * (1.0 + 0.3 * (p / 0.5)); + return scale * (1.3 - 0.3 * ((p - 0.5) / 0.5)); + } +} + +class _ParticlePainter extends CustomPainter { + _ParticlePainter(this.particles); + final List<_Particle> particles; + + @override + void paint(Canvas canvas, Size size) { + final textPainter = TextPainter(textDirection: TextDirection.ltr); + for (final p in particles) { + final tp = TextSpan( + text: p.emoji, + style: TextStyle( + fontSize: 24 * p.currentScale, + color: Colors.black.withValues(alpha: p.opacity), + ), + ); + textPainter + ..text = tp + ..layout(); + canvas + ..save() + ..translate(p.x - textPainter.width / 2, p.y - textPainter.height / 2) + ..rotate(p.rotation); + textPainter.paint(canvas, Offset.zero); + canvas.restore(); + } + } + + @override + bool shouldRepaint(covariant _ParticlePainter old) => true; +} diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart index f82bbbc..0eca6dd 100644 --- a/lib/src/views/components/avatar_icon.component.dart +++ b/lib/src/views/components/avatar_icon.component.dart @@ -72,10 +72,16 @@ class _AvatarIconState extends State { _globalUserDataCallBackId = 'avatar_${getRandomString(10)}'; globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () { setState(() { - _avatarSVGs = [gUser.avatarSvg!]; + if (gUser.avatarSvg != null) { + _avatarSVGs = [gUser.avatarSvg!]; + } else { + _avatarSVGs = []; + } }); }; - _avatarSVGs.add(gUser.avatarSvg!); + if (gUser.avatarSvg != null) { + _avatarSVGs = [gUser.avatarSvg!]; + } } else if (widget.contactId != null) { contactStream = twonlyDB.contactsDao .watchContact(widget.contactId!) diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index 6dca019..45af6a7 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -82,13 +82,14 @@ class GroupContextMenu extends StatelessWidget { context.lang.groupContextMenuDeleteGroup, ); if (ok) { - await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); - await twonlyDB.groupsDao.updateGroup( - group.groupId, - const GroupsCompanion( - deletedContent: Value(true), - ), - ); + // await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + await twonlyDB.groupsDao.deleteGroup(group.groupId); + // await twonlyDB.groupsDao.updateGroup( + // group.groupId, + // const GroupsCompanion( + // deletedContent: Value(true), + // ), + // ); } }, ), diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 56eefe5..02aa643 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -6,6 +6,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; +import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; @@ -59,6 +61,10 @@ class _ProfileViewState extends State { return; } + // as the username has changes, remove the old from the server and then upload it again. + await removeTwonlySafeFromServer(); + unawaited(performTwonlySafeBackup(force: true)); + await updateUserdata((user) { user ..username = username From 0c6099cd9205dfedc54735b9a7b3e502ba7f0fde Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Nov 2025 00:23:29 +0100 Subject: [PATCH 65/76] fix #284 --- lib/src/database/daos/groups.dao.dart | 6 + lib/src/localization/app_de.arb | 15 +- lib/src/localization/app_en.arb | 13 +- .../generated/app_localizations.dart | 50 +++++- .../generated/app_localizations_de.dart | 37 ++++- .../generated/app_localizations_en.dart | 35 +++- .../api/client2client/groups.c2c.dart | 13 +- lib/src/services/group.services.dart | 42 +++++ lib/src/utils/misc.dart | 2 +- .../all_reactions.bottom_sheet.dart | 1 - .../chat_group_action.dart | 6 +- .../views/components/better_list_title.dart | 2 +- .../select_chat_deletion_time.comp.dart | 156 ++++++++++++++++++ lib/src/views/contact/contact.view.dart | 31 ++-- lib/src/views/groups/group.view.dart | 8 +- 15 files changed, 369 insertions(+), 48 deletions(-) create mode 100644 lib/src/views/components/select_chat_deletion_time.comp.dart diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 92986e4..3565b03 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -198,6 +198,12 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .watchSingleOrNull(); } + Stream watchDirectChat(int contactId) { + final groupId = getUUIDforDirectChat(contactId, gUser.userId); + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .watchSingleOrNull(); + } + Stream> watchGroupsForChatList() { return (select(groups) ..where((t) => t.deletedContent.equals(false)) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 3c1f557..b2f5f4b 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -689,9 +689,9 @@ "@durationShortSecond": {}, "durationShortMinute": "Min.", "@durationShortMinute": {}, - "durationShortHour": "Std", + "durationShortHour": "Std.", "@durationShortHour": {}, - "durationShortDays": "Tagen", + "durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}", "@durationShortDays": {}, "contacts": "Kontakte", "groups": "Gruppen", @@ -805,6 +805,13 @@ "leaveGroupSureTitle": "Gruppe verlassen", "leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?", "leaveGroupSureOkBtn": "Gruppe verlassen", - "changeDisplayMaxTime": "{username} hat das Zeitlimit für verschwindende Nachrichten auf {time}.", - "youChangedDisplayMaxTime": "Du hat das Zeitlimit für verschwindende Nachrichten auf {time}." + "changeDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht ({username}).", + "youChangedDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht.", + "userGotReported": "Benutzer wurde gemeldet.", + "deleteChatAfter": "Chat löschen nach...", + "deleteChatAfterAnHour": "einer Stunde.", + "deleteChatAfterADay": "einem Tag.", + "deleteChatAfterAWeek": "einer Woche.", + "deleteChatAfterAMonth": "einem Monat.", + "deleteChatAfterAYear": "einem Jahr." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index bb5db7b..c014f78 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -516,7 +516,7 @@ "durationShortSecond": "Sec.", "durationShortMinute": "Min.", "durationShortHour": "Hrs.", - "durationShortDays": "Days", + "durationShortDays": "{count, plural, =1{1 Day} other{{count} Days}}", "contacts": "Contacts", "groups": "Groups", "newGroup": "New group", @@ -583,6 +583,13 @@ "leaveGroupSureTitle": "Leave group", "leaveGroupSureBody": "Do you really want to leave the group?", "leaveGroupSureOkBtn": "Leave group", - "changeDisplayMaxTime": "{username} has set the time limit for disappearing messages to {time}.", - "youChangedDisplayMaxTime": "You have set the time limit for disappearing messages to {time}." + "changeDisplayMaxTime": "Chats will now be deleted after {time} ({username}).", + "youChangedDisplayMaxTime": "Chats will now be deleted after {time}.", + "userGotReported": "User has been reported.", + "deleteChatAfter": "Delete chat after...", + "deleteChatAfterAnHour": "one hour.", + "deleteChatAfterADay": "one day.", + "deleteChatAfterAWeek": "one week.", + "deleteChatAfterAMonth": "one month.", + "deleteChatAfterAYear": "one year." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 5567323..f329ac9 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2201,8 +2201,8 @@ abstract class AppLocalizations { /// No description provided for @durationShortDays. /// /// In en, this message translates to: - /// **'Days'** - String get durationShortDays; + /// **'{count, plural, =1{1 Day} other{{count} Days}}'** + String durationShortDays(num count); /// No description provided for @contacts. /// @@ -2603,14 +2603,56 @@ abstract class AppLocalizations { /// No description provided for @changeDisplayMaxTime. /// /// In en, this message translates to: - /// **'{username} has set the time limit for disappearing messages to {time}.'** + /// **'Chats will now be deleted after {time} ({username}).'** String changeDisplayMaxTime(Object time, Object username); /// No description provided for @youChangedDisplayMaxTime. /// /// In en, this message translates to: - /// **'You have set the time limit for disappearing messages to {time}.'** + /// **'Chats will now be deleted after {time}.'** String youChangedDisplayMaxTime(Object time); + + /// No description provided for @userGotReported. + /// + /// In en, this message translates to: + /// **'User has been reported.'** + String get userGotReported; + + /// No description provided for @deleteChatAfter. + /// + /// In en, this message translates to: + /// **'Delete chat after...'** + String get deleteChatAfter; + + /// No description provided for @deleteChatAfterAnHour. + /// + /// In en, this message translates to: + /// **'one hour.'** + String get deleteChatAfterAnHour; + + /// No description provided for @deleteChatAfterADay. + /// + /// In en, this message translates to: + /// **'one day.'** + String get deleteChatAfterADay; + + /// No description provided for @deleteChatAfterAWeek. + /// + /// In en, this message translates to: + /// **'one week.'** + String get deleteChatAfterAWeek; + + /// No description provided for @deleteChatAfterAMonth. + /// + /// In en, this message translates to: + /// **'one month.'** + String get deleteChatAfterAMonth; + + /// No description provided for @deleteChatAfterAYear. + /// + /// In en, this message translates to: + /// **'one year.'** + String get deleteChatAfterAYear; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index adc25c5..a58f148 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1164,10 +1164,18 @@ class AppLocalizationsDe extends AppLocalizations { String get durationShortMinute => 'Min.'; @override - String get durationShortHour => 'Std'; + String get durationShortHour => 'Std.'; @override - String get durationShortDays => 'Tagen'; + String durationShortDays(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Tage', + one: '1 Tag', + ); + return '$_temp0'; + } @override String get contacts => 'Kontakte'; @@ -1424,11 +1432,32 @@ class AppLocalizationsDe extends AppLocalizations { @override String changeDisplayMaxTime(Object time, Object username) { - return '$username hat das Zeitlimit für verschwindende Nachrichten auf $time.'; + return 'Chats werden ab jetzt nach $time gelöscht ($username).'; } @override String youChangedDisplayMaxTime(Object time) { - return 'Du hat das Zeitlimit für verschwindende Nachrichten auf $time.'; + return 'Chats werden ab jetzt nach $time gelöscht.'; } + + @override + String get userGotReported => 'Benutzer wurde gemeldet.'; + + @override + String get deleteChatAfter => 'Chat löschen nach...'; + + @override + String get deleteChatAfterAnHour => 'einer Stunde.'; + + @override + String get deleteChatAfterADay => 'einem Tag.'; + + @override + String get deleteChatAfterAWeek => 'einer Woche.'; + + @override + String get deleteChatAfterAMonth => 'einem Monat.'; + + @override + String get deleteChatAfterAYear => 'einem Jahr.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 19de928..ff1a8e0 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1159,7 +1159,15 @@ class AppLocalizationsEn extends AppLocalizations { String get durationShortHour => 'Hrs.'; @override - String get durationShortDays => 'Days'; + String durationShortDays(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Days', + one: '1 Day', + ); + return '$_temp0'; + } @override String get contacts => 'Contacts'; @@ -1414,11 +1422,32 @@ class AppLocalizationsEn extends AppLocalizations { @override String changeDisplayMaxTime(Object time, Object username) { - return '$username has set the time limit for disappearing messages to $time.'; + return 'Chats will now be deleted after $time ($username).'; } @override String youChangedDisplayMaxTime(Object time) { - return 'You have set the time limit for disappearing messages to $time.'; + return 'Chats will now be deleted after $time.'; } + + @override + String get userGotReported => 'User has been reported.'; + + @override + String get deleteChatAfter => 'Delete chat after...'; + + @override + String get deleteChatAfterAnHour => 'one hour.'; + + @override + String get deleteChatAfterADay => 'one day.'; + + @override + String get deleteChatAfterAWeek => 'one week.'; + + @override + String get deleteChatAfterAMonth => 'one month.'; + + @override + String get deleteChatAfterAYear => 'one year.'; } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index f193911..a20b27f 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -138,6 +138,15 @@ Future handleGroupUpdate( contactId: Value(fromUserId), ), ); + if (group.isDirectChat) { + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + deleteMessagesAfterMilliseconds: + Value(update.newDeleteMessagesAfterMilliseconds.toInt()), + ), + ); + } case GroupActionType.removedMember: case GroupActionType.addMember: case GroupActionType.leftGroup: @@ -165,7 +174,9 @@ Future handleGroupUpdate( break; } - unawaited(fetchGroupState(group)); + if (!group.isDirectChat) { + unawaited(fetchGroupState(group)); + } } Future handleGroupJoin( diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index a80d22b..3d81340 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -654,6 +654,48 @@ Future updateGroupName(Group group, String groupName) async { return (await fetchGroupState(group)) != null; } +Future updateChatDeletionTime( + Group group, + int deleteMessagesAfterMilliseconds, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + state.deleteMessagesAfterMilliseconds = + Int64(deleteMessagesAfterMilliseconds); + + // send new state to the server + if (!await _updateGroupState(group, state)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.changeDisplayMaxTime.name, + newDeleteMessagesAfterMilliseconds: Int64( + deleteMessagesAfterMilliseconds, + ), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.changeDisplayMaxTime), + newDeleteMessagesAfterMilliseconds: + Value(deleteMessagesAfterMilliseconds), + ), + ); + + // Updates the groupName :) + return (await fetchGroupState(group)) != null; +} + Future addNewGroupMembers( Group group, List newGroupMemberIds, diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 5a5dd35..b121035 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -113,7 +113,7 @@ String formatDuration(BuildContext context, int seconds) { return '$hours ${context.lang.durationShortHour}'; } else { final days = seconds ~/ 86400; - return '$days ${context.lang.durationShortDays}'; + return context.lang.durationShortDays(days); } } diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 8bd31d3..011c294 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -64,7 +64,6 @@ class _AllReactionsViewState extends State { ), ), ); - // if (mounted) Navigator.pop(context); } @override diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index e547d6e..e711ded 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -58,12 +58,12 @@ class _ChatGroupActionState extends State { case GroupActionType.changeDisplayMaxTime: final time = formatDuration( context, - (widget.action.newDeleteMessagesAfterMilliseconds ?? 0 / 1000) as int, + (widget.action.newDeleteMessagesAfterMilliseconds ?? 0) ~/ 1000, ); text = (contact == null) ? context.lang.youChangedDisplayMaxTime(time) - : context.lang.changeDisplayMaxTime(maker, time); - icon = FontAwesomeIcons.pencil; + : context.lang.changeDisplayMaxTime(time, maker); + icon = FontAwesomeIcons.stopwatch20; case GroupActionType.updatedGroupName: text = (contact == null) ? context.lang.youChangedGroupName(widget.action.newGroupName!) diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index 45aadfa..fe408a5 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class BetterListTile extends StatelessWidget { const BetterListTile({ required this.text, - required this.onTap, + this.onTap, this.icon, this.leading, super.key, diff --git a/lib/src/views/components/select_chat_deletion_time.comp.dart b/lib/src/views/components/select_chat_deletion_time.comp.dart new file mode 100644 index 0000000..279a4c7 --- /dev/null +++ b/lib/src/views/components/select_chat_deletion_time.comp.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:drift/drift.dart' show Value; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; + +class SelectChatDeletionTimeListTitle extends StatefulWidget { + const SelectChatDeletionTimeListTitle({ + required this.groupId, + this.disabled = false, + super.key, + }); + + final String groupId; + final bool disabled; + + @override + State createState() => + _SelectChatDeletionTimeListTitleState(); +} + +class _SelectChatDeletionTimeListTitleState + extends State { + Group? group; + + late StreamSubscription groupSub; + int _selectedDeletionTime = 0; + + @override + void initState() { + groupSub = twonlyDB.groupsDao.watchGroup(widget.groupId).listen((update) { + if (update == null) return; + group = update; + + final selected = _getOptions().indexWhere( + (t) => t.$1 == update.deleteMessagesAfterMilliseconds, + ); + setState(() { + _selectedDeletionTime = selected % _getOptions().length; + }); + }); + super.initState(); + } + + @override + void dispose() { + groupSub.cancel(); + super.dispose(); + } + + Future _showDialog(Widget child) async { + await showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6), + // The Bottom margin is provided to align the popup above the system navigation bar. + margin: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + // Provide a background color for the popup. + color: CupertinoColors.systemBackground.resolveFrom(context), + // Use a SafeArea widget to avoid system overlaps. + child: SafeArea(top: false, child: child), + ), + ); + + if (group == null) return; + + final selected = _getOptions()[_selectedDeletionTime].$1; + + if (group!.deleteMessagesAfterMilliseconds != selected) { + if (group!.isDirectChat) { + await twonlyDB.groupsDao.updateGroup( + group!.groupId, + GroupsCompanion( + deleteMessagesAfterMilliseconds: Value(selected), + ), + ); + await sendCipherTextToGroup( + group!.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.changeDisplayMaxTime.name, + newDeleteMessagesAfterMilliseconds: Int64(selected), + ), + ), + ); + } else { + if (!await updateChatDeletionTime(group!, selected)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + } + } + + List<(int, String)> _getOptions() { + return getOptions(context); + } + + static List<(int, String)> getOptions(BuildContext context) { + return <(int, String)>[ + (1000 * 60 * 60, context.lang.deleteChatAfterAnHour), + (1000 * 60 * 60 * 24, context.lang.deleteChatAfterADay), + (1000 * 60 * 60 * 24 * 7, context.lang.deleteChatAfterAWeek), + (1000 * 60 * 60 * 24 * 30, context.lang.deleteChatAfterAMonth), + (1000 * 60 * 60 * 24 * 365, context.lang.deleteChatAfterAYear), + ]; + } + + @override + Widget build(BuildContext context) { + return BetterListTile( + icon: FontAwesomeIcons.stopwatch20, + text: context.lang.deleteChatAfter, + trailing: Text(_getOptions()[_selectedDeletionTime].$2), + onTap: widget.disabled + ? null + : () => _showDialog( + CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + itemExtent: 32, + // This sets the initial item. + scrollController: FixedExtentScrollController( + initialItem: _selectedDeletionTime, + ), + // This is called when selected item is changed. + onSelectedItemChanged: (int selectedItem) { + setState(() { + _selectedDeletionTime = selectedItem; + }); + }, + children: + List.generate(_getOptions().length, (int index) { + return Center( + child: Text(_getOptions()[index].$2), + ); + }), + ), + ), + ); + } +} diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index c37f514..960bbbe 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -9,8 +10,10 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -68,30 +71,14 @@ class _ContactViewState extends State { if (!mounted) return; if (res.isSuccess) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Benutzer wurde gemeldet.'), - duration: Duration(seconds: 3), + SnackBar( + content: Text(context.lang.userGotReported), + duration: const Duration(seconds: 3), ), ); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.', - ), - duration: Duration(seconds: 3), - ), - ); + showNetworkIssue(context); } - // if (block) { - // const update = ContactsCompanion(blocked: Value(true)); - // if (context.mounted) { - // await twonlyDB.contactsDao.updateContact(contact.userId, update); - // } - // if (mounted) { - // Navigator.popUntil(context, (route) => route.isFirst); - // } - // } } @override @@ -155,6 +142,10 @@ class _ContactViewState extends State { }, ), const Divider(), + SelectChatDeletionTimeListTitle( + groupId: getUUIDforDirectChat(widget.userId, gUser.userId), + ), + const Divider(), BetterListTile( icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 851d5ca..4d495ac 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; @@ -148,9 +149,6 @@ class _GroupViewState extends State { return; } } - // If not admin -> append to the server state - - // -> Inform the other users } @override @@ -188,6 +186,10 @@ class _GroupViewState extends State { text: context.lang.groupNameInput, onTap: _updateGroupName, ), + SelectChatDeletionTimeListTitle( + groupId: widget.group.groupId, + disabled: !group.isGroupAdmin, + ), const Divider(), ListTile( title: Padding( From 620d5f51f94471e2fd3ce7b9f288097434f4074b Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Nov 2025 00:24:34 +0100 Subject: [PATCH 66/76] remove invalid test --- test/unit_test.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/unit_test.dart b/test/unit_test.dart index bf6ffea..dafea36 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; - import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -63,20 +60,5 @@ void main() { test('Reject values > 0x7fffffff', () { expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError); }); - - test('sign and verify', () { - final keyPair = generateIdentityKeyPair(); - final message = Uint8List(10); - - final random = getRandomUint8List(32); - - final signature = - sign(keyPair.getPrivateKey().serialize(), message, random); - - expect( - verifySig(keyPair.getPublicKey().serialize(), message, signature), - true, - ); - }); }); } From a99f42c5c8a1dfc4f3171b855efd59455744bea1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Nov 2025 17:15:57 +0100 Subject: [PATCH 67/76] fix #291 --- .../message_input.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index d989a62..a0768dd 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -40,7 +40,6 @@ class _MessageInputState extends State { ); _textFieldController.clear(); - _emojiShowing = false; widget.onMessageSend(); setState(() {}); } @@ -101,18 +100,21 @@ class _MessageInputState extends State { } }); }, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 12, - right: 8, - ), - child: FaIcon( - size: 20, - _emojiShowing - ? FontAwesomeIcons.keyboard - : FontAwesomeIcons.faceSmile, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, + ), ), ), ), From 0ab03031636afd66a22e811900c7df6f9bd5a860 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Nov 2025 18:40:22 +0100 Subject: [PATCH 68/76] fix #283 --- lib/app.dart | 13 +-- lib/globals.dart | 4 +- lib/src/database/daos/groups.dao.dart | 4 +- lib/src/database/daos/receipts.dao.dart | 10 -- lib/src/localization/app_de.arb | 2 +- .../generated/app_localizations_de.dart | 2 +- .../client/generated/messages.pb.dart | 14 +++ .../client/generated/messages.pbjson.dart | 16 +-- lib/src/model/protobuf/client/messages.proto | 1 + lib/src/providers/connection.provider.dart | 5 +- lib/src/services/api.service.dart | 3 +- .../api/client2client/contact.c2c.dart | 12 ++- lib/src/services/flame.service.dart | 9 +- lib/src/services/subscription.service.dart | 35 ++++++ lib/src/utils/storage.dart | 12 ++- lib/src/views/chats/chat_list.view.dart | 7 +- .../group_context_menu.component.dart | 16 +-- .../components/max_flame_list_title.dart | 102 ++++++++++++++++++ lib/src/views/contact/contact.view.dart | 4 + .../settings/subscription/checkout.view.dart | 11 +- .../manage_subscription.view.dart | 12 +-- .../subscription/select_payment.view.dart | 15 +-- .../subscription/subscription.view.dart | 76 +++++++------ 23 files changed, 281 insertions(+), 104 deletions(-) create mode 100644 lib/src/services/subscription.service.dart create mode 100644 lib/src/views/components/max_flame_list_title.dart diff --git a/lib/app.dart b/lib/app.dart index 5b430a7..aa43d90 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; @@ -36,8 +37,8 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); }; - globalCallbackUpdatePlan = (String planId) async { - await context.read().updatePlan(planId); + globalCallbackUpdatePlan = (SubscriptionPlan plan) async { + await context.read().updatePlan(plan); }; unawaited(initAsync()); @@ -47,9 +48,9 @@ class _AppState extends State with WidgetsBindingObserver { final user = await getUser(); if (user != null && mounted) { if (mounted) { - await context - .read() - .updatePlan(user.subscriptionPlan); + await context.read().updatePlan( + planFromString(user.subscriptionPlan), + ); } } } @@ -79,7 +80,7 @@ class _AppState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); globalCallbackConnectionState = ({required bool isConnected}) {}; - globalCallbackUpdatePlan = (String planId) {}; + globalCallbackUpdatePlan = (SubscriptionPlan planId) {}; super.dispose(); } diff --git a/lib/globals.dart b/lib/globals.dart index 3bf3b22..5a9acbf 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -4,6 +4,7 @@ import 'package:camera/camera.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; late ApiService apiService; @@ -26,7 +27,8 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({ }) {}; void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackNewDeviceRegistered = () {}; -void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; +void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = + (SubscriptionPlan plan) {}; Map globalUserDataChangedCallBack = {}; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 3565b03..0dd04e8 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -321,8 +321,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { if (updateFlame) { flameCounter += 1; lastFlameCounterChange = Value(timestamp); - if (flameCounter > maxFlameCounter) { - maxFlameCounter = flameCounter; + if ((flameCounter + 1) >= maxFlameCounter) { + maxFlameCounter = flameCounter + 1; maxFlameCounterFrom = DateTime.now(); } } diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index ecbfeee..8b44397 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -105,16 +105,6 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { ..where((t) => t.receiptId.equals(receiptId))) .getSingleOrNull() != null; - // try { - // return await (select() - // ..where( - // (t) => t.receiptId.equals(receiptId), - // )) - // .getSingleOrNull(); - // } catch (e) { - // Log.error(e); - // return null; - // } } Future gotReceipt(String receiptId) async { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index b2f5f4b..182be3d 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -411,7 +411,7 @@ "@proFeature1": {}, "proFeature2": "1 zusätzlicher Plus Benutzer", "@proFeature2": {}, - "proFeature3": "Zusatzfunktionen (coming-soon)", + "proFeature3": "Flammen wiederherstellen", "@proFeature3": {}, "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", "@proFeature4": {}, diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index a58f148..bc98dea 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -705,7 +705,7 @@ class AppLocalizationsDe extends AppLocalizations { String get proFeature2 => '1 zusätzlicher Plus Benutzer'; @override - String get proFeature3 => 'Zusatzfunktionen (coming-soon)'; + String get proFeature3 => 'Flammen wiederherstellen'; @override String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)'; diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 2586ee4..3babd94 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -1251,6 +1251,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { $fixnum.Int64? flameCounter, $fixnum.Int64? lastFlameCounterChange, $core.bool? bestFriend, + $core.bool? forceUpdate, }) { final $result = create(); if (flameCounter != null) { @@ -1262,6 +1263,9 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { if (bestFriend != null) { $result.bestFriend = bestFriend; } + if (forceUpdate != null) { + $result.forceUpdate = forceUpdate; + } return $result; } EncryptedContent_FlameSync._() : super(); @@ -1272,6 +1276,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { ..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter') ..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange') ..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend') + ..aOB(4, _omitFieldNames ? '' : 'forceUpdate', protoName: 'forceUpdate') ..hasRequiredFields = false ; @@ -1322,6 +1327,15 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { $core.bool hasBestFriend() => $_has(2); @$pb.TagNumber(3) void clearBestFriend() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get forceUpdate => $_getBF(3); + @$pb.TagNumber(4) + set forceUpdate($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasForceUpdate() => $_has(3); + @$pb.TagNumber(4) + void clearForceUpdate() => clearField(4); } class EncryptedContent extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 37824d4..a0fd2af 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -365,6 +365,7 @@ const EncryptedContent_FlameSync$json = { {'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'}, {'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'}, {'1': 'bestFriend', '3': 3, '4': 1, '5': 8, '10': 'bestFriend'}, + {'1': 'forceUpdate', '3': 4, '4': 1, '5': 8, '10': 'forceUpdate'}, ], }; @@ -434,12 +435,13 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ' 'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' 'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' - 'ZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' + 'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' 'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' - 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZE' - 'IPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVw' - 'ZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb2' - '50YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoM' - 'X3RleHRNZXNzYWdlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZG' - 'F0ZUIXChVfcmVzZW5kR3JvdXBQdWJsaWNLZXk='); + 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG' + 'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf' + 'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW' + 'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l' + 'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX' + 'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi' + 'bGljS2V5'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 72b3230..301ac2e 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -169,6 +169,7 @@ message EncryptedContent { int64 flameCounter = 1; int64 lastFlameCounterChange = 2; bool bestFriend = 3; + bool forceUpdate = 4; } } \ No newline at end of file diff --git a/lib/src/providers/connection.provider.dart b/lib/src/providers/connection.provider.dart index 5b63969..f09472c 100644 --- a/lib/src/providers/connection.provider.dart +++ b/lib/src/providers/connection.provider.dart @@ -1,15 +1,16 @@ import 'package:flutter/foundation.dart'; +import 'package:twonly/src/services/subscription.service.dart'; class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { bool _isConnected = false; bool get isConnected => _isConnected; - String plan = 'Free'; + SubscriptionPlan plan = SubscriptionPlan.Free; Future updateConnectionState(bool update) async { _isConnected = update; notifyListeners(); } - Future updatePlan(String newPlan) async { + Future updatePlan(SubscriptionPlan newPlan) async { plan = newPlan; notifyListeners(); } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 41d7498..0d8a829 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -35,6 +35,7 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -384,7 +385,7 @@ class ApiService { user.subscriptionPlan = authenticated.plan; return user; }); - globalCallbackUpdatePlan(authenticated.plan); + globalCallbackUpdatePlan(planFromString(authenticated.plan)); } Log.info('websocket is authenticated'); unawaited(onAuthenticated()); diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 607c872..5a63668 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -123,18 +123,24 @@ Future handleFlameSync( Log.info('Got a flameSync from $contactId'); final group = await twonlyDB.groupsDao.getDirectChat(contactId); - if (group == null || group.lastFlameCounterChange != null) return; + if (group == null || group.lastFlameCounterChange == null) return; var updates = GroupsCompanion( alsoBestFriend: Value(flameSync.bestFriend), ); if (isToday(group.lastFlameCounterChange!) && - isToday(fromTimestamp(flameSync.lastFlameCounterChange))) { + isToday(fromTimestamp(flameSync.lastFlameCounterChange)) || + flameSync.forceUpdate) { if (flameSync.flameCounter > group.flameCounter) { - updates = GroupsCompanion( + updates = updates.copyWith( flameCounter: Value(flameSync.flameCounter.toInt()), ); } + if (flameSync.flameCounter > group.maxFlameCounter) { + updates = updates.copyWith( + maxFlameCounter: Value(flameSync.flameCounter.toInt()), + ); + } } await twonlyDB.groupsDao.updateGroup(group.groupId, updates); } diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 9f05b1c..8eff7fb 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -Future syncFlameCounters() async { +Future syncFlameCounters({String? forceForGroup}) async { final groups = await twonlyDB.groupsDao.getAllDirectChats(); if (groups.isEmpty) return; final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; @@ -26,8 +26,10 @@ Future syncFlameCounters() async { for (final group in groups) { if (group.lastFlameCounterChange == null) continue; if (!isToday(group.lastFlameCounterChange!)) continue; - if (group.lastFlameSync != null) { - if (isToday(group.lastFlameSync!)) continue; + if (forceForGroup == null || group.groupId != forceForGroup) { + if (group.lastFlameSync != null) { + if (isToday(group.lastFlameSync!)) continue; + } } final flameCounter = getFlameCounterFromGroup(group) - 1; @@ -49,6 +51,7 @@ Future syncFlameCounters() async { lastFlameCounterChange: Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), bestFriend: group.groupId == bestFriend.groupId, + forceUpdate: group.groupId == forceForGroup, ), ), ); diff --git a/lib/src/services/subscription.service.dart b/lib/src/services/subscription.service.dart new file mode 100644 index 0000000..c36797a --- /dev/null +++ b/lib/src/services/subscription.service.dart @@ -0,0 +1,35 @@ +// ignore_for_file: constant_identifier_names + +import 'package:twonly/globals.dart'; + +enum SubscriptionPlan { + Free, + Tester, + Family, + Pro, + Plus, +} + +bool isAdditionalAccount(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus; +} + +bool isPayingUser(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Family || + plan == SubscriptionPlan.Pro || + plan == SubscriptionPlan.Tester; +} + +SubscriptionPlan planFromString(String value) { + final input = value.trim().toLowerCase(); + for (final v in SubscriptionPlan.values) { + final name = v.name; + final compareName = name.toLowerCase(); + if (compareName == input) return v; + } + return SubscriptionPlan.Free; +} + +SubscriptionPlan getCurrentPlan() { + return planFromString(gUser.subscriptionPlan); +} diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 1a9f3a5..9466769 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -8,6 +8,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; Future isUserCreated() async { @@ -35,16 +36,19 @@ Future getUser() async { } } -Future updateUsersPlan(BuildContext context, String planId) async { - context.read().plan = planId; +Future updateUsersPlan( + BuildContext context, + SubscriptionPlan plan, +) async { + context.read().plan = plan; await updateUserdata((user) { - user.subscriptionPlan = planId; + user.subscriptionPlan = plan.name; return user; }); if (!context.mounted) return; - await context.read().updatePlan(planId); + await context.read().updatePlan(plan); } Mutex updateProtection = Mutex(); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 1ba8dec..c1956c9 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; @@ -104,7 +105,7 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { final isConnected = context.watch().isConnected; - final planId = context.watch().plan; + final plan = context.watch().plan; return Scaffold( appBar: AppBar( title: Row( @@ -130,7 +131,7 @@ class _ChatListViewState extends State { ), const SizedBox(width: 10), const Text('twonly '), - if (planId != 'Free') + if (plan != SubscriptionPlan.Free) GestureDetector( onTap: () { Navigator.push( @@ -150,7 +151,7 @@ class _ChatListViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: Text( - planId, + plan.name, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index 45af6a7..28808a1 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -82,14 +82,14 @@ class GroupContextMenu extends StatelessWidget { context.lang.groupContextMenuDeleteGroup, ); if (ok) { - // await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); - await twonlyDB.groupsDao.deleteGroup(group.groupId); - // await twonlyDB.groupsDao.updateGroup( - // group.groupId, - // const GroupsCompanion( - // deletedContent: Value(true), - // ), - // ); + await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + // await twonlyDB.groupsDao.deleteGroup(group.groupId); + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + deletedContent: Value(true), + ), + ); } }, ), diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart new file mode 100644 index 0000000..4c863b7 --- /dev/null +++ b/lib/src/views/components/max_flame_list_title.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; + +class MaxFlameListTitle extends StatefulWidget { + const MaxFlameListTitle({ + required this.contactId, + super.key, + }); + final int contactId; + + @override + State createState() => _MaxFlameListTitleState(); +} + +class _MaxFlameListTitleState extends State { + int _flameCounter = 0; + Group? _directChat; + late String _groupId; + + late StreamSubscription _flameCounterSub; + late StreamSubscription _groupSub; + + @override + void initState() { + _groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); + final stream = twonlyDB.groupsDao.watchFlameCounter(_groupId); + _flameCounterSub = stream.listen((counter) { + if (mounted) { + setState(() { + _flameCounter = counter; + }); + } + }); + final stream2 = twonlyDB.groupsDao.watchGroup(_groupId); + _groupSub = stream2.listen((update) { + if (mounted) { + setState(() { + _directChat = update; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _flameCounterSub.cancel(); + _groupSub.cancel(); + super.dispose(); + } + + Future _restoreFlames() async { + if (!isPayingUser(getCurrentPlan())) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + return; + } + await twonlyDB.groupsDao.updateGroup( + _groupId, + GroupsCompanion( + flameCounter: Value(_directChat!.maxFlameCounter - 1), + lastFlameCounterChange: Value(DateTime.now()), + ), + ); + await syncFlameCounters(forceForGroup: _groupId); + } + + @override + Widget build(BuildContext context) { + if (_directChat == null || + _flameCounter >= (_directChat!.maxFlameCounter + 1) || + _directChat!.lastFlameCounterChange! + .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { + return Container(); + } + return BetterListTile( + onTap: _restoreFlames, + leading: const SizedBox( + width: 24, + child: EmojiAnimation( + emoji: '🔥', + ), + ), + text: 'Restore your ${_directChat!.maxFlameCounter} lost flames', + ); + } +} diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 960bbbe..d911a1b 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/max_flame_list_title.dart'; import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; @@ -146,6 +147,9 @@ class _ContactViewState extends State { groupId: getUUIDforDirectChat(widget.userId, gUser.userId), ), const Divider(), + MaxFlameListTitle( + contactId: widget.userId, + ), BetterListTile( icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, diff --git a/lib/src/views/settings/subscription/checkout.view.dart b/lib/src/views/settings/subscription/checkout.view.dart index 174c2d2..83804a5 100644 --- a/lib/src/views/settings/subscription/checkout.view.dart +++ b/lib/src/views/settings/subscription/checkout.view.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/select_payment.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class CheckoutView extends StatefulWidget { const CheckoutView({ - required this.planId, + required this.plan, super.key, this.refund, this.disableMonthlyOption, }); - final String planId; + final SubscriptionPlan plan; final int? refund; final bool? disableMonthlyOption; @@ -31,7 +32,7 @@ class _CheckoutViewState extends State { } void setCheckout({bool init = false}) { - checkoutInCents = getPlanPrice(widget.planId, paidMonthly: paidMonthly); + checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly); if (!init) { setState(() {}); } @@ -52,7 +53,7 @@ class _CheckoutViewState extends State { Expanded( child: ListView( children: [ - PlanCard(planId: widget.planId), + PlanCard(plan: widget.plan), if (widget.disableMonthlyOption == null || !widget.disableMonthlyOption!) Padding( @@ -129,7 +130,7 @@ class _CheckoutViewState extends State { MaterialPageRoute( builder: (context) { return SelectPaymentView( - planId: widget.planId, + plan: widget.plan, payMonthly: paidMonthly, refund: widget.refund, ); diff --git a/lib/src/views/settings/subscription/manage_subscription.view.dart b/lib/src/views/settings/subscription/manage_subscription.view.dart index 7fb96ab..6c887d4 100644 --- a/lib/src/views/settings/subscription/manage_subscription.view.dart +++ b/lib/src/views/settings/subscription/manage_subscription.view.dart @@ -7,6 +7,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -64,25 +65,24 @@ class _ManageSubscriptionViewState extends State { @override Widget build(BuildContext context) { - final planId = context.read().plan; + final plan = context.read().plan; final myLocale = Localizations.localeOf(context); final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS; - final isPayingUser = planId == 'Family' || planId == 'Pro'; return Scaffold( appBar: AppBar( title: Text(context.lang.manageSubscription), ), body: ListView( children: [ - PlanCard(planId: planId, paidMonthly: paidMonthly), - if (isPayingUser) const SizedBox(height: 20), - if (widget.nextPayment != null && isPayingUser) + PlanCard(plan: plan, paidMonthly: paidMonthly), + if (isPayingUser(plan)) const SizedBox(height: 20), + if (widget.nextPayment != null && isPayingUser(plan)) ListTile( title: Text( '${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}', ), ), - if (autoRenewal != null && isPayingUser) + if (autoRenewal != null && isPayingUser(plan)) ListTile( title: Text(context.lang.autoRenewal), subtitle: Text( diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index c92f115..4ae1ae2 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -13,13 +14,13 @@ import 'package:url_launcher/url_launcher.dart'; class SelectPaymentView extends StatefulWidget { const SelectPaymentView({ super.key, - this.planId, + this.plan, this.payMonthly, this.valueInCents, this.refund, }); - final String? planId; + final SubscriptionPlan? plan; final bool? payMonthly; final int? valueInCents; final int? refund; @@ -62,9 +63,9 @@ class _SelectPaymentViewState extends State { void setCheckout(bool init) { if (widget.valueInCents != null && widget.valueInCents! > 0) { checkoutInCents = widget.valueInCents!; - } else if (widget.planId != null) { + } else if (widget.plan != null) { checkoutInCents = - getPlanPrice(widget.planId!, paidMonthly: widget.payMonthly!); + getPlanPrice(widget.plan!, paidMonthly: widget.payMonthly!); } else { /// Nothing to checkout for... Navigator.pop(context); @@ -77,7 +78,7 @@ class _SelectPaymentViewState extends State { @override Widget build(BuildContext context) { - final totalPrice = (widget.planId != null && widget.payMonthly != null) + final totalPrice = (widget.plan != null && widget.payMonthly != null) ? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}' : localePrizing(context, checkoutInCents); final canPay = paymentMethods == PaymentMethods.twonlyCredit && @@ -239,13 +240,13 @@ class _SelectPaymentViewState extends State { onPressed: canPay ? () async { final res = await apiService.switchToPayedPlan( - widget.planId!, + widget.plan!.name, widget.payMonthly!, tryAutoRenewal, ); if (!context.mounted) return; if (res.isSuccess) { - await updateUsersPlan(context, widget.planId!); + await updateUsersPlan(context, widget.plan!); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index 4c0c8fc..6d1ccbf 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -64,7 +65,7 @@ const int MONTHLY_PAYMENT_DAYS = 30; const int YEARLY_PAYMENT_DAYS = 365; int calculateRefund(Response_PlanBallance current) { - var refund = getPlanPrice('Pro', paidMonthly: true); + var refund = getPlanPrice(SubscriptionPlan.Pro, paidMonthly: true); if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) { final elapsedDays = DateTime.now() @@ -81,7 +82,7 @@ int calculateRefund(Response_PlanBallance current) { // => 5€ refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) * - getPlanPrice('Pro', paidMonthly: false) / + getPlanPrice(SubscriptionPlan.Pro, paidMonthly: false) / 100) .ceil() * 100; @@ -144,13 +145,12 @@ class _SubscriptionViewState extends State { String? formattedBalance; DateTime? nextPayment; final currentPlan = context.read().plan; - final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro'; if (ballance != null) { final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch( ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000, ); - if (isPayingUser) { + if (isPayingUser(currentPlan)) { nextPayment = lastPaymentDateTime .add(Duration(days: ballance!.paymentPeriodDays.toInt())); } @@ -164,7 +164,7 @@ class _SubscriptionViewState extends State { } var refund = 0; - if (currentPlan == 'Pro' && ballance != null) { + if (currentPlan == SubscriptionPlan.Pro && ballance != null) { refund = calculateRefund(ballance!); } @@ -202,7 +202,7 @@ class _SubscriptionViewState extends State { ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Text( - currentPlan, + currentPlan.name, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, @@ -220,7 +220,7 @@ class _SubscriptionViewState extends State { style: const TextStyle(color: Colors.orange), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan)) Center( child: Padding( padding: const EdgeInsets.all(18), @@ -231,16 +231,17 @@ class _SubscriptionViewState extends State { ), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) PlanCard( - planId: 'Pro', + plan: SubscriptionPlan.Pro, onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { return const CheckoutView( - planId: 'Pro', + plan: SubscriptionPlan.Pro, ); }, ), @@ -248,9 +249,9 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (currentPlan != 'Family') + if (currentPlan != SubscriptionPlan.Family) PlanCard( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: refund, onTap: () async { await Navigator.push( @@ -258,11 +259,12 @@ class _SubscriptionViewState extends State { MaterialPageRoute( builder: (context) { return CheckoutView( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: (refund > 0) ? refund : null, - disableMonthlyOption: currentPlan == 'Pro' && - ballance!.paymentPeriodDays.toInt() == - YEARLY_PAYMENT_DAYS, + disableMonthlyOption: + currentPlan == SubscriptionPlan.Pro && + ballance!.paymentPeriodDays.toInt() == + YEARLY_PAYMENT_DAYS, ); }, ), @@ -270,7 +272,7 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (!isPayingUser) ...[ + if (!isPayingUser(currentPlan)) ...[ const SizedBox(height: 10), Center( child: Padding( @@ -284,15 +286,15 @@ class _SubscriptionViewState extends State { ), const SizedBox(height: 10), PlanCard( - planId: 'Plus', + plan: SubscriptionPlan.Plus, onTap: () async { - await redeemUserInviteCode(context, 'Plus'); + await redeemUserInviteCode(context, SubscriptionPlan.Plus.name); await initAsync(); }, ), ], const SizedBox(height: 10), - if (currentPlan != 'Family') const Divider(), + if (currentPlan != SubscriptionPlan.Family) const Divider(), BetterListTile( icon: FontAwesomeIcons.gears, text: context.lang.manageSubscription, @@ -337,7 +339,8 @@ class _SubscriptionViewState extends State { ); }, ), - if (isPayingUser || currentPlan == 'Tester') + if (isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) BetterListTile( icon: FontAwesomeIcons.userPlus, text: context.lang.manageAdditionalUsers, @@ -378,36 +381,38 @@ class _SubscriptionViewState extends State { } } -int getPlanPrice(String planId, {required bool paidMonthly}) { - switch (planId) { - case 'Pro': +int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) { + switch (plan) { + case SubscriptionPlan.Pro: return paidMonthly ? 100 : 1000; - case 'Family': + case SubscriptionPlan.Family: return paidMonthly ? 200 : 2000; + // ignore: no_default_cases + default: + return 0; } - return 0; } class PlanCard extends StatelessWidget { const PlanCard({ - required this.planId, + required this.plan, super.key, this.refund, this.onTap, this.paidMonthly, }); - final String planId; + final SubscriptionPlan plan; final void Function()? onTap; final int? refund; final bool? paidMonthly; @override Widget build(BuildContext context) { - final yearlyPrice = getPlanPrice(planId, paidMonthly: false); - final monthlyPrice = getPlanPrice(planId, paidMonthly: true); + final yearlyPrice = getPlanPrice(plan, paidMonthly: false); + final monthlyPrice = getPlanPrice(plan, paidMonthly: true); var features = []; - switch (planId) { + switch (plan.name) { case 'Free': features = [context.lang.freeFeature1]; case 'Plus': @@ -447,7 +452,7 @@ class PlanCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( - planId, + plan.name, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, @@ -519,9 +524,12 @@ class PlanCard extends StatelessWidget { padding: const EdgeInsets.only(top: 10), child: FilledButton.icon( onPressed: onTap, - label: (planId == 'Free' || planId == 'Plus') + label: (plan == SubscriptionPlan.Free || + plan == SubscriptionPlan.Plus) ? Text(context.lang.redeemUserInviteCodeTitle) - : Text(context.lang.upgradeToPaidPlanButton(planId)), + : Text( + context.lang.upgradeToPaidPlanButton(plan.name), + ), ), ), ], From 3706a36cf9bb04267f0ec3f1931fa92e29d79d92 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Nov 2025 20:50:22 +0100 Subject: [PATCH 69/76] implementing pow #253 --- lib/app.dart | 16 +++ .../api/websocket/client_to_server.pb.dart | 110 ++++++++++++++---- .../websocket/client_to_server.pbjson.dart | 49 ++++---- .../protobuf/api/websocket/error.pbenum.dart | 4 + .../protobuf/api/websocket/error.pbjson.dart | 5 +- .../api/websocket/server_to_client.pb.dart | 84 ++++++++++++- .../websocket/server_to_client.pbjson.dart | 54 +++++---- lib/src/services/api.service.dart | 22 +++- lib/src/utils/pow.dart | 3 +- lib/src/views/onboarding/register.view.dart | 34 +++++- test/unit_test.dart | 3 +- 11 files changed, 310 insertions(+), 74 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index aa43d90..52371c3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -7,6 +7,8 @@ import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; @@ -154,6 +156,8 @@ class _AppMainWidgetState extends State { bool _showOnboarding = true; bool _isLoaded = false; + Future? _proofOfWork; + @override void initState() { initAsync(); @@ -169,6 +173,17 @@ class _AppMainWidgetState extends State { } } + if (!_isUserCreated && !_showDatabaseMigration) { + // This means the user is in the onboarding screen, so start with the Proof of Work. + + final proof = await apiService.getProofOfWork(); + if (proof != null) { + Log.info('Starting with proof of work calculation.'); + // Starting with the proof of work. + _proofOfWork = calculatePoW(proof.prefix, proof.difficulty.toInt()); + } + } + setState(() { _isLoaded = true; }); @@ -205,6 +220,7 @@ class _AppMainWidgetState extends State { } else { child = RegisterView( callbackOnSuccess: initAsync, + proofOfWork: _proofOfWork, ); } diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index c986530..db2849b 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -196,6 +196,38 @@ class V0 extends $pb.GeneratedMessage { Response ensureResponse() => $_ensure(3); } +class Handshake_RequestPOW extends $pb.GeneratedMessage { + factory Handshake_RequestPOW() => create(); + Handshake_RequestPOW._() : super(); + factory Handshake_RequestPOW.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Handshake_RequestPOW.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Handshake.RequestPOW', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Handshake_RequestPOW clone() => Handshake_RequestPOW()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Handshake_RequestPOW copyWith(void Function(Handshake_RequestPOW) updates) => super.copyWith((message) => updates(message as Handshake_RequestPOW)) as Handshake_RequestPOW; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_RequestPOW create() => Handshake_RequestPOW._(); + Handshake_RequestPOW createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Handshake_RequestPOW getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Handshake_RequestPOW? _defaultInstance; +} + class Handshake_Register extends $pb.GeneratedMessage { factory Handshake_Register({ $core.String? username, @@ -207,6 +239,7 @@ class Handshake_Register extends $pb.GeneratedMessage { $fixnum.Int64? registrationId, $core.bool? isIos, $core.String? langCode, + $fixnum.Int64? proofOfWork, }) { final $result = create(); if (username != null) { @@ -236,6 +269,9 @@ class Handshake_Register extends $pb.GeneratedMessage { if (langCode != null) { $result.langCode = langCode; } + if (proofOfWork != null) { + $result.proofOfWork = proofOfWork; + } return $result; } Handshake_Register._() : super(); @@ -252,6 +288,7 @@ class Handshake_Register extends $pb.GeneratedMessage { ..aInt64(7, _omitFieldNames ? '' : 'registrationId') ..aOB(8, _omitFieldNames ? '' : 'isIos') ..aOS(9, _omitFieldNames ? '' : 'langCode') + ..aInt64(10, _omitFieldNames ? '' : 'proofOfWork') ..hasRequiredFields = false ; @@ -356,6 +393,15 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool hasLangCode() => $_has(8); @$pb.TagNumber(9) void clearLangCode() => clearField(9); + + @$pb.TagNumber(10) + $fixnum.Int64 get proofOfWork => $_getI64(9); + @$pb.TagNumber(10) + set proofOfWork($fixnum.Int64 v) { $_setInt64(9, v); } + @$pb.TagNumber(10) + $core.bool hasProofOfWork() => $_has(9); + @$pb.TagNumber(10) + void clearProofOfWork() => clearField(10); } class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { @@ -548,32 +594,37 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { enum Handshake_Handshake { register, - getauthchallenge, - getauthtoken, + getAuthChallenge, + getAuthToken, authenticate, + requestPOW, notSet } class Handshake extends $pb.GeneratedMessage { factory Handshake({ Handshake_Register? register, - Handshake_GetAuthChallenge? getauthchallenge, - Handshake_GetAuthToken? getauthtoken, + Handshake_GetAuthChallenge? getAuthChallenge, + Handshake_GetAuthToken? getAuthToken, Handshake_Authenticate? authenticate, + Handshake_RequestPOW? requestPOW, }) { final $result = create(); if (register != null) { $result.register = register; } - if (getauthchallenge != null) { - $result.getauthchallenge = getauthchallenge; + if (getAuthChallenge != null) { + $result.getAuthChallenge = getAuthChallenge; } - if (getauthtoken != null) { - $result.getauthtoken = getauthtoken; + if (getAuthToken != null) { + $result.getAuthToken = getAuthToken; } if (authenticate != null) { $result.authenticate = authenticate; } + if (requestPOW != null) { + $result.requestPOW = requestPOW; + } return $result; } Handshake._() : super(); @@ -582,17 +633,19 @@ class Handshake extends $pb.GeneratedMessage { static const $core.Map<$core.int, Handshake_Handshake> _Handshake_HandshakeByTag = { 1 : Handshake_Handshake.register, - 2 : Handshake_Handshake.getauthchallenge, - 3 : Handshake_Handshake.getauthtoken, + 2 : Handshake_Handshake.getAuthChallenge, + 3 : Handshake_Handshake.getAuthToken, 4 : Handshake_Handshake.authenticate, + 5 : Handshake_Handshake.requestPOW, 0 : Handshake_Handshake.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Handshake', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4]) + ..oo(0, [1, 2, 3, 4, 5]) ..aOM(1, _omitFieldNames ? '' : 'register', subBuilder: Handshake_Register.create) - ..aOM(2, _omitFieldNames ? '' : 'getauthchallenge', subBuilder: Handshake_GetAuthChallenge.create) - ..aOM(3, _omitFieldNames ? '' : 'getauthtoken', subBuilder: Handshake_GetAuthToken.create) + ..aOM(2, _omitFieldNames ? '' : 'getAuthChallenge', protoName: 'getAuthChallenge', subBuilder: Handshake_GetAuthChallenge.create) + ..aOM(3, _omitFieldNames ? '' : 'getAuthToken', protoName: 'getAuthToken', subBuilder: Handshake_GetAuthToken.create) ..aOM(4, _omitFieldNames ? '' : 'authenticate', subBuilder: Handshake_Authenticate.create) + ..aOM(5, _omitFieldNames ? '' : 'requestPOW', protoName: 'requestPOW', subBuilder: Handshake_RequestPOW.create) ..hasRequiredFields = false ; @@ -632,26 +685,26 @@ class Handshake extends $pb.GeneratedMessage { Handshake_Register ensureRegister() => $_ensure(0); @$pb.TagNumber(2) - Handshake_GetAuthChallenge get getauthchallenge => $_getN(1); + Handshake_GetAuthChallenge get getAuthChallenge => $_getN(1); @$pb.TagNumber(2) - set getauthchallenge(Handshake_GetAuthChallenge v) { setField(2, v); } + set getAuthChallenge(Handshake_GetAuthChallenge v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasGetauthchallenge() => $_has(1); + $core.bool hasGetAuthChallenge() => $_has(1); @$pb.TagNumber(2) - void clearGetauthchallenge() => clearField(2); + void clearGetAuthChallenge() => clearField(2); @$pb.TagNumber(2) - Handshake_GetAuthChallenge ensureGetauthchallenge() => $_ensure(1); + Handshake_GetAuthChallenge ensureGetAuthChallenge() => $_ensure(1); @$pb.TagNumber(3) - Handshake_GetAuthToken get getauthtoken => $_getN(2); + Handshake_GetAuthToken get getAuthToken => $_getN(2); @$pb.TagNumber(3) - set getauthtoken(Handshake_GetAuthToken v) { setField(3, v); } + set getAuthToken(Handshake_GetAuthToken v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasGetauthtoken() => $_has(2); + $core.bool hasGetAuthToken() => $_has(2); @$pb.TagNumber(3) - void clearGetauthtoken() => clearField(3); + void clearGetAuthToken() => clearField(3); @$pb.TagNumber(3) - Handshake_GetAuthToken ensureGetauthtoken() => $_ensure(2); + Handshake_GetAuthToken ensureGetAuthToken() => $_ensure(2); @$pb.TagNumber(4) Handshake_Authenticate get authenticate => $_getN(3); @@ -663,6 +716,17 @@ class Handshake extends $pb.GeneratedMessage { void clearAuthenticate() => clearField(4); @$pb.TagNumber(4) Handshake_Authenticate ensureAuthenticate() => $_ensure(3); + + @$pb.TagNumber(5) + Handshake_RequestPOW get requestPOW => $_getN(4); + @$pb.TagNumber(5) + set requestPOW(Handshake_RequestPOW v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasRequestPOW() => $_has(4); + @$pb.TagNumber(5) + void clearRequestPOW() => clearField(5); + @$pb.TagNumber(5) + Handshake_RequestPOW ensureRequestPOW() => $_ensure(4); } class ApplicationData_TextMessage extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index c95798c..b6cfd08 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -56,16 +56,22 @@ const Handshake$json = { '1': 'Handshake', '2': [ {'1': 'register', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.Handshake.Register', '9': 0, '10': 'register'}, - {'1': 'getauthchallenge', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthChallenge', '9': 0, '10': 'getauthchallenge'}, - {'1': 'getauthtoken', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthToken', '9': 0, '10': 'getauthtoken'}, + {'1': 'getAuthChallenge', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthChallenge', '9': 0, '10': 'getAuthChallenge'}, + {'1': 'getAuthToken', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthToken', '9': 0, '10': 'getAuthToken'}, {'1': 'authenticate', '3': 4, '4': 1, '5': 11, '6': '.client_to_server.Handshake.Authenticate', '9': 0, '10': 'authenticate'}, + {'1': 'requestPOW', '3': 5, '4': 1, '5': 11, '6': '.client_to_server.Handshake.RequestPOW', '9': 0, '10': 'requestPOW'}, ], - '3': [Handshake_Register$json, Handshake_GetAuthChallenge$json, Handshake_GetAuthToken$json, Handshake_Authenticate$json], + '3': [Handshake_RequestPOW$json, Handshake_Register$json, Handshake_GetAuthChallenge$json, Handshake_GetAuthToken$json, Handshake_Authenticate$json], '8': [ {'1': 'Handshake'}, ], }; +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_RequestPOW$json = { + '1': 'RequestPOW', +}; + @$core.Deprecated('Use handshakeDescriptor instead') const Handshake_Register$json = { '1': 'Register', @@ -79,6 +85,7 @@ const Handshake_Register$json = { {'1': 'registration_id', '3': 7, '4': 1, '5': 3, '10': 'registrationId'}, {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, + {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, ], '8': [ {'1': '_invite_code'}, @@ -117,23 +124,25 @@ const Handshake_Authenticate$json = { /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' - 'FrZS5SZWdpc3RlckgAUghyZWdpc3RlchJaChBnZXRhdXRoY2hhbGxlbmdlGAIgASgLMiwuY2xp' - 'ZW50X3RvX3NlcnZlci5IYW5kc2hha2UuR2V0QXV0aENoYWxsZW5nZUgAUhBnZXRhdXRoY2hhbG' - 'xlbmdlEk4KDGdldGF1dGh0b2tlbhgDIAEoCzIoLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtl' - 'LkdldEF1dGhUb2tlbkgAUgxnZXRhdXRodG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' - 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRrw' - 'AgoIUmVnaXN0ZXISGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGA' - 'IgASgJSABSCmludml0ZUNvZGWIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVi' - 'bGljSWRlbnRpdHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3' - 'NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQ' - 'c2lnbmVkX3ByZWtleV9pZBgGIAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2' - 'lkGAcgASgDUg5yZWdpc3RyYXRpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdf' - 'Y29kZRgJIAEoCVIIbGFuZ0NvZGVCDgoMX2ludml0ZV9jb2RlGhIKEEdldEF1dGhDaGFsbGVuZ2' - 'UaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIaCghyZXNwb25zZRgC' - 'IAEoDFIIcmVzcG9uc2UarAEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW' - 'QSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJzaW9uGAMgASgJSABS' - 'CmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZUlkiAEBQg4KDF9hcH' - 'BfdmVyc2lvbkIMCgpfZGV2aWNlX2lkQgsKCUhhbmRzaGFrZQ=='); + 'FrZS5SZWdpc3RlckgAUghyZWdpc3RlchJaChBnZXRBdXRoQ2hhbGxlbmdlGAIgASgLMiwuY2xp' + 'ZW50X3RvX3NlcnZlci5IYW5kc2hha2UuR2V0QXV0aENoYWxsZW5nZUgAUhBnZXRBdXRoQ2hhbG' + 'xlbmdlEk4KDGdldEF1dGhUb2tlbhgDIAEoCzIoLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtl' + 'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' + 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI' + 'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF' + 'BPV0gAUgpyZXF1ZXN0UE9XGgwKClJlcXVlc3RQT1calAMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1l' + 'GAEgASgJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi' + '4KE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25l' + 'ZF9wcmVrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZR' + 'gFIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANS' + 'DnNpZ25lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSW' + 'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK' + 'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX' + 'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS' + 'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGqwBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB' + 'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy' + 'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2' + 'VJZIgBAUIOCgxfYXBwX3ZlcnNpb25CDAoKX2RldmljZV9pZEILCglIYW5kc2hha2U='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { diff --git a/lib/src/model/protobuf/api/websocket/error.pbenum.dart b/lib/src/model/protobuf/api/websocket/error.pbenum.dart index 3e9c5e0..1c5331e 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbenum.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbenum.dart @@ -48,6 +48,8 @@ class ErrorCode extends $pb.ProtobufEnum { static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken'); static const ErrorCode AppVersionOutdated = ErrorCode._(1030, _omitEnumNames ? '' : 'AppVersionOutdated'); static const ErrorCode NewDeviceRegistered = ErrorCode._(1031, _omitEnumNames ? '' : 'NewDeviceRegistered'); + static const ErrorCode InvalidProofOfWork = ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork'); + static const ErrorCode RegistrationDisabled = ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled'); static const $core.List values = [ Unknown, @@ -84,6 +86,8 @@ class ErrorCode extends $pb.ProtobufEnum { UserIdAlreadyTaken, AppVersionOutdated, NewDeviceRegistered, + InvalidProofOfWork, + RegistrationDisabled, ]; static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/api/websocket/error.pbjson.dart b/lib/src/model/protobuf/api/websocket/error.pbjson.dart index fad129d..2e937dd 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbjson.dart @@ -51,6 +51,8 @@ const ErrorCode$json = { {'1': 'UserIdAlreadyTaken', '2': 1029}, {'1': 'AppVersionOutdated', '2': 1030}, {'1': 'NewDeviceRegistered', '2': 1031}, + {'1': 'InvalidProofOfWork', '2': 1032}, + {'1': 'RegistrationDisabled', '2': 1033}, ], }; @@ -70,5 +72,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode( 'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' - 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcI'); + 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh' + 'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA=='); diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 8d67382..381c100 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -1536,6 +1536,70 @@ class Response_DownloadTokens extends $pb.GeneratedMessage { $core.List<$core.List<$core.int>> get downloadTokens => $_getList(0); } +class Response_ProofOfWork extends $pb.GeneratedMessage { + factory Response_ProofOfWork({ + $core.String? prefix, + $fixnum.Int64? difficulty, + }) { + final $result = create(); + if (prefix != null) { + $result.prefix = prefix; + } + if (difficulty != null) { + $result.difficulty = difficulty; + } + return $result; + } + Response_ProofOfWork._() : super(); + factory Response_ProofOfWork.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Response_ProofOfWork.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Response.ProofOfWork', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'prefix') + ..aInt64(2, _omitFieldNames ? '' : 'difficulty') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Response_ProofOfWork clone() => Response_ProofOfWork()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Response_ProofOfWork copyWith(void Function(Response_ProofOfWork) updates) => super.copyWith((message) => updates(message as Response_ProofOfWork)) as Response_ProofOfWork; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_ProofOfWork create() => Response_ProofOfWork._(); + Response_ProofOfWork createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Response_ProofOfWork getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Response_ProofOfWork? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get prefix => $_getSZ(0); + @$pb.TagNumber(1) + set prefix($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasPrefix() => $_has(0); + @$pb.TagNumber(1) + void clearPrefix() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get difficulty => $_getI64(1); + @$pb.TagNumber(2) + set difficulty($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasDifficulty() => $_has(1); + @$pb.TagNumber(2) + void clearDifficulty() => clearField(2); +} + enum Response_Ok_Ok { none, userid, @@ -1551,6 +1615,7 @@ enum Response_Ok_Ok { addaccountsinvites, downloadtokens, signedprekey, + proofOfWork, notSet } @@ -1570,6 +1635,7 @@ class Response_Ok extends $pb.GeneratedMessage { Response_AddAccountsInvites? addaccountsinvites, Response_DownloadTokens? downloadtokens, Response_SignedPreKey? signedprekey, + Response_ProofOfWork? proofOfWork, }) { final $result = create(); if (none != null) { @@ -1614,6 +1680,9 @@ class Response_Ok extends $pb.GeneratedMessage { if (signedprekey != null) { $result.signedprekey = signedprekey; } + if (proofOfWork != null) { + $result.proofOfWork = proofOfWork; + } return $result; } Response_Ok._() : super(); @@ -1635,10 +1704,11 @@ class Response_Ok extends $pb.GeneratedMessage { 12 : Response_Ok_Ok.addaccountsinvites, 13 : Response_Ok_Ok.downloadtokens, 14 : Response_Ok_Ok.signedprekey, + 15 : Response_Ok_Ok.proofOfWork, 0 : Response_Ok_Ok.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Response.Ok', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aInt64(2, _omitFieldNames ? '' : 'userid') ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'authchallenge', $pb.PbFieldType.OY) @@ -1653,6 +1723,7 @@ class Response_Ok extends $pb.GeneratedMessage { ..aOM(12, _omitFieldNames ? '' : 'addaccountsinvites', subBuilder: Response_AddAccountsInvites.create) ..aOM(13, _omitFieldNames ? '' : 'downloadtokens', subBuilder: Response_DownloadTokens.create) ..aOM(14, _omitFieldNames ? '' : 'signedprekey', subBuilder: Response_SignedPreKey.create) + ..aOM(15, _omitFieldNames ? '' : 'proofOfWork', protoName: 'proofOfWork', subBuilder: Response_ProofOfWork.create) ..hasRequiredFields = false ; @@ -1825,6 +1896,17 @@ class Response_Ok extends $pb.GeneratedMessage { void clearSignedprekey() => clearField(14); @$pb.TagNumber(14) Response_SignedPreKey ensureSignedprekey() => $_ensure(13); + + @$pb.TagNumber(15) + Response_ProofOfWork get proofOfWork => $_getN(14); + @$pb.TagNumber(15) + set proofOfWork(Response_ProofOfWork v) { setField(15, v); } + @$pb.TagNumber(15) + $core.bool hasProofOfWork() => $_has(14); + @$pb.TagNumber(15) + void clearProofOfWork() => clearField(15); + @$pb.TagNumber(15) + Response_ProofOfWork ensureProofOfWork() => $_ensure(14); } enum Response_Response { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index eabade8..441503e 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -73,7 +73,7 @@ const Response$json = { {'1': 'ok', '3': 1, '4': 1, '5': 11, '6': '.server_to_client.Response.Ok', '9': 0, '10': 'ok'}, {'1': 'error', '3': 2, '4': 1, '5': 14, '6': '.error.ErrorCode', '9': 0, '10': 'error'}, ], - '3': [Response_Authenticated$json, Response_Plan$json, Response_Plans$json, Response_AddAccountsInvite$json, Response_AddAccountsInvites$json, Response_Transaction$json, Response_AdditionalAccount$json, Response_Voucher$json, Response_Vouchers$json, Response_PlanBallance$json, Response_Location$json, Response_PreKey$json, Response_SignedPreKey$json, Response_UserData$json, Response_UploadToken$json, Response_DownloadTokens$json, Response_Ok$json], + '3': [Response_Authenticated$json, Response_Plan$json, Response_Plans$json, Response_AddAccountsInvite$json, Response_AddAccountsInvites$json, Response_Transaction$json, Response_AdditionalAccount$json, Response_Voucher$json, Response_Vouchers$json, Response_PlanBallance$json, Response_Location$json, Response_PreKey$json, Response_SignedPreKey$json, Response_UserData$json, Response_UploadToken$json, Response_DownloadTokens$json, Response_ProofOfWork$json, Response_Ok$json], '4': [Response_TransactionTypes$json], '8': [ {'1': 'Response'}, @@ -257,6 +257,15 @@ const Response_DownloadTokens$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_ProofOfWork$json = { + '1': 'ProofOfWork', + '2': [ + {'1': 'prefix', '3': 1, '4': 1, '5': 9, '10': 'prefix'}, + {'1': 'difficulty', '3': 2, '4': 1, '5': 3, '10': 'difficulty'}, + ], +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_Ok$json = { '1': 'Ok', @@ -275,6 +284,7 @@ const Response_Ok$json = { {'1': 'addaccountsinvites', '3': 12, '4': 1, '5': 11, '6': '.server_to_client.Response.AddAccountsInvites', '9': 0, '10': 'addaccountsinvites'}, {'1': 'downloadtokens', '3': 13, '4': 1, '5': 11, '6': '.server_to_client.Response.DownloadTokens', '9': 0, '10': 'downloadtokens'}, {'1': 'signedprekey', '3': 14, '4': 1, '5': 11, '6': '.server_to_client.Response.SignedPreKey', '9': 0, '10': 'signedprekey'}, + {'1': 'proofOfWork', '3': 15, '4': 1, '5': 11, '6': '.server_to_client.Response.ProofOfWork', '9': 0, '10': 'proofOfWork'}, ], '8': [ {'1': 'Ok'}, @@ -352,24 +362,26 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( '9zaWduZWRfcHJla2V5X2lkGlkKC1VwbG9hZFRva2VuEiEKDHVwbG9hZF90b2tlbhgBIAEoDFIL' 'dXBsb2FkVG9rZW4SJwoPZG93bmxvYWRfdG9rZW5zGAIgAygMUg5kb3dubG9hZFRva2Vucxo5Cg' '5Eb3dubG9hZFRva2VucxInCg9kb3dubG9hZF90b2tlbnMYASADKAxSDmRvd25sb2FkVG9rZW5z' - 'GvcGCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZXJpZB' - 'ImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9rZW4Y' - 'BCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG9hZH' - 'Rva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Vc2Vy' - 'RGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkEKCGxvY2' - 'F0aW9uGAcgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Mb2NhdGlvbkgAUghsb2Nh' - 'dGlvbhJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS' - '5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJf' - 'dG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCz' - 'InLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5j' - 'ZRJBCgh2b3VjaGVycxgLIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVm91Y2hlcn' - 'NIAFIIdm91Y2hlcnMSXwoSYWRkYWNjb3VudHNpbnZpdGVzGAwgASgLMi0uc2VydmVyX3RvX2Ns' - 'aWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAFISYWRkYWNjb3VudHNpbnZpdGVzEl' - 'MKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Eb3du' - 'bG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaWduZWRwcmVrZXkYDiABKAsyJy5zZX' - 'J2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtleUgAUgxzaWduZWRwcmVrZXlCBAoC' - 'T2silgEKEFRyYW5zYWN0aW9uVHlwZXMSCgoGUmVmdW5kEAASEwoPVm91Y2hlclJlZGVlbWVkEA' - 'ESEgoOVm91Y2hlckNyZWF0ZWQQAhIICgRDYXNoEAMSDwoLUGxhblVwZ3JhZGUQBBILCgdVbmtu' - 'b3duEAUSFAoQVGhhbmtzRm9yVGVzdGluZxAGEg8KC0F1dG9SZW5ld2FsEAdCCgoIUmVzcG9uc2' - 'U='); + 'GkUKC1Byb29mT2ZXb3JrEhYKBnByZWZpeBgBIAEoCVIGcHJlZml4Eh4KCmRpZmZpY3VsdHkYAi' + 'ABKANSCmRpZmZpY3VsdHkawwcKAk9rEhQKBE5vbmUYASABKAhIAFIETm9uZRIYCgZ1c2VyaWQY' + 'AiABKANIAFIGdXNlcmlkEiYKDWF1dGhjaGFsbGVuZ2UYAyABKAxIAFINYXV0aGNoYWxsZW5nZR' + 'JKCgt1cGxvYWR0b2tlbhgEIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXBsb2Fk' + 'VG9rZW5IAFILdXBsb2FkdG9rZW4SQQoIdXNlcmRhdGEYBSABKAsyIy5zZXJ2ZXJfdG9fY2xpZW' + '50LlJlc3BvbnNlLlVzZXJEYXRhSABSCHVzZXJkYXRhEh4KCWF1dGh0b2tlbhgGIAEoDEgAUglh' + 'dXRodG9rZW4SQQoIbG9jYXRpb24YByABKAsyIy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk' + 'xvY2F0aW9uSABSCGxvY2F0aW9uElAKDWF1dGhlbnRpY2F0ZWQYCCABKAsyKC5zZXJ2ZXJfdG9f' + 'Y2xpZW50LlJlc3BvbnNlLkF1dGhlbnRpY2F0ZWRIAFINYXV0aGVudGljYXRlZBI4CgVwbGFucx' + 'gJIAEoCzIgLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbnNIAFIFcGxhbnMSTQoMcGxh' + 'bmJhbGxhbmNlGAogASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuQmFsbGFuY2' + 'VIAFIMcGxhbmJhbGxhbmNlEkEKCHZvdWNoZXJzGAsgASgLMiMuc2VydmVyX3RvX2NsaWVudC5S' + 'ZXNwb25zZS5Wb3VjaGVyc0gAUgh2b3VjaGVycxJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKA' + 'syLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRh' + 'Y2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW' + '50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHBy' + 'ZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDH' + 'NpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVz' + 'cG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2silgEKEFRyYW5zYWN0aW9uVH' + 'lwZXMSCgoGUmVmdW5kEAASEwoPVm91Y2hlclJlZGVlbWVkEAESEgoOVm91Y2hlckNyZWF0ZWQQ' + 'AhIICgRDYXNoEAMSDwoLUGxhblVwZ3JhZGUQBBILCgdVbmtub3duEAUSFAoQVGhhbmtzRm9yVG' + 'VzdGluZxAGEg8KC0F1dG9SZW5ld2FsEAdCCgoIUmVzcG9uc2U='); diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 0d8a829..a918d29 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -415,7 +415,7 @@ class ApiService { } final handshake = Handshake() - ..getauthchallenge = Handshake_GetAuthChallenge(); + ..getAuthChallenge = Handshake_GetAuthChallenge(); final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); @@ -436,7 +436,7 @@ class ApiService { ..response = signature ..userId = Int64(userData.userId); - final getauthtoken = Handshake()..getauthtoken = getAuthToken; + final getauthtoken = Handshake()..getAuthToken = getAuthToken; final req2 = createClientToServerFromHandshake(getauthtoken); @@ -458,7 +458,11 @@ class ApiService { await tryAuthenticateWithToken(userData.userId); } - Future register(String username, String? inviteCode) async { + Future register( + String username, + String? inviteCode, + int proofOfWorkResult, + ) async { final signalIdentity = await getSignalIdentity(); if (signalIdentity == null) { return Result.error(ErrorCode.InternalError); @@ -477,6 +481,7 @@ class ApiService { ..signedPrekeySignature = signedPreKey.signature ..signedPrekeyId = Int64(signedPreKey.id) ..langCode = ui.PlatformDispatcher.instance.locale.languageCode + ..proofOfWork = Int64(proofOfWorkResult) ..isIos = Platform.isIOS; if (inviteCode != null && inviteCode != '') { @@ -503,6 +508,17 @@ class ApiService { return null; } + Future getProofOfWork() async { + final handshake = Handshake()..requestPOW = Handshake_RequestPOW(); + final req = createClientToServerFromHandshake(handshake); + final result = await sendRequestSync(req, authenticated: false); + if (result.isError) { + Log.error('could not request proof of work params', result); + return null; + } + return result.value.proofOfWork as Response_ProofOfWork; + } + Future downloadDone(List token) async { final get = ApplicationData_DownloadDone()..downloadToken = token; final appData = ApplicationData()..downloadDone = get; diff --git a/lib/src/utils/pow.dart b/lib/src/utils/pow.dart index 67d2ebe..b932a79 100644 --- a/lib/src/utils/pow.dart +++ b/lib/src/utils/pow.dart @@ -1,12 +1,11 @@ import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; bool isValid(int difficulty, List digest) { final bits = digest.map((i) => i.toRadixString(2).padLeft(8, '0')).join(); return bits.startsWith('0' * difficulty); } -Future calculatePoW(Uint8List prefix, int difficulty) async { +Future calculatePoW(String prefix, int difficulty) async { var i = 0; while (true) { i++; diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index e652f63..d06df15 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -13,14 +13,21 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/onboarding/recover.view.dart'; class RegisterView extends StatefulWidget { - const RegisterView({required this.callbackOnSuccess, super.key}); + const RegisterView({ + required this.callbackOnSuccess, + required this.proofOfWork, + super.key, + }); final Function callbackOnSuccess; + final Future? proofOfWork; @override State createState() => _RegisterViewState(); } @@ -48,11 +55,29 @@ class _RegisterViewState extends State { _showUserNameError = false; }); + late int proof; + + if (widget.proofOfWork != null) { + proof = await widget.proofOfWork!; + } else { + final pow = await apiService.getProofOfWork(); + if (pow == null) { + if (mounted) { + showNetworkIssue(context); + } + return; + // Starting with the proof of work. + } + proof = await calculatePoW(pow.prefix, pow.difficulty.toInt()); + } + + Log.info('The result of the POW is $proof'); + await createIfNotExistsSignalIdentity(); var userId = 0; - final res = await apiService.register(username, inviteCode); + final res = await apiService.register(username, inviteCode, proof); if (res.isSuccess) { Log.info('Got user_id ${res.value} from server'); userId = res.value.userid.toInt() as int; @@ -62,6 +87,11 @@ class _RegisterViewState extends State { await deleteLocalUserData(); return createNewUser(); } + if (res.error == ErrorCode.InvalidProofOfWork) { + Log.error('Proof of Work is invalid. Try again.'); + await deleteLocalUserData(); + return createNewUser(); + } if (mounted) { setState(() { _isTryingToRegister = false; diff --git a/test/unit_test.dart b/test/unit_test.dart index dafea36..cb4229d 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -15,7 +15,8 @@ void main() { }); test('test proof-of-work simple', () async { - expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33); + // ignore: prefer_single_quotes + expect(await calculatePoW("testing", 10), 783); }); test('encode hex', () async { From 95c9db86d674549ea7a7d7d5605325c85780f324 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 7 Nov 2025 00:31:46 +0100 Subject: [PATCH 70/76] implementing voice messages #251 --- .../NotificationService.swift | 4 + .../push_notification.pb.swift | 16 +- ios/Podfile.lock | 6 + lib/src/database/daos/mediafiles.dao.dart | 1 + lib/src/database/daos/messages.dao.dart | 1 + lib/src/database/tables/mediafiles.table.dart | 1 + lib/src/localization/app_de.arb | 3 + lib/src/localization/app_en.arb | 3 + .../generated/app_localizations.dart | 18 ++ .../generated/app_localizations_de.dart | 14 + .../generated/app_localizations_en.dart | 14 + .../client/generated/messages.pbenum.dart | 2 + .../client/generated/messages.pbjson.dart | 57 +++-- .../generated/push_notification.pbenum.dart | 6 +- .../generated/push_notification.pbjson.dart | 7 +- lib/src/model/protobuf/client/messages.proto | 1 + .../protobuf/client/push_notification.proto | 4 +- .../services/api/client2client/media.c2c.dart | 2 + .../api/mediafiles/download.service.dart | 31 ++- .../mediafiles/media_background.service.dart | 40 +-- .../api/mediafiles/upload.service.dart | 18 +- .../mediafiles/mediafile.service.dart | 7 + .../background.notifications.dart | 3 + .../notifications/pushkeys.notifications.dart | 11 +- lib/src/utils/misc.dart | 2 + .../chat_list_components/group_list_item.dart | 34 +-- lib/src/views/chats/chat_messages.view.dart | 3 +- .../chat_list_entry.dart | 31 ++- .../chat_text_entry.dart | 221 ---------------- .../entries/chat_audio_entry.dart | 232 +++++++++++++++++ .../{ => entries}/chat_date_chip.dart | 0 .../{ => entries}/chat_media_entry.dart | 0 .../entries/chat_text_entry.dart | 96 +++++++ .../entries/common.dart | 101 ++++++++ .../entries/friendly_message_time.comp.dart | 77 ++++++ .../message_input.dart | 239 ++++++++++++++--- .../message_send_state_icon.dart | 55 ++-- .../response_container.dart | 16 +- .../views/chats/chat_messages_components/test | 242 ++++++++++++++++++ .../components/max_flame_list_title.dart | 3 +- pubspec.lock | 8 + pubspec.yaml | 1 + 42 files changed, 1252 insertions(+), 379 deletions(-) delete mode 100644 lib/src/views/chats/chat_messages_components/chat_text_entry.dart create mode 100644 lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart rename lib/src/views/chats/chat_messages_components/{ => entries}/chat_date_chip.dart (100%) rename lib/src/views/chats/chat_messages_components/{ => entries}/chat_media_entry.dart (100%) create mode 100644 lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart create mode 100644 lib/src/views/chats/chat_messages_components/entries/common.dart create mode 100644 lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart create mode 100644 lib/src/views/chats/chat_messages_components/test diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 718417c..9fb06e1 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -219,6 +219,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .twonly: "hat ein twonly{inGroup} gesendet.", .video: "hat ein Video{inGroup} gesendet.", .image: "hat ein Bild{inGroup} gesendet.", + .audio: "hat eine Sprachnachricht{inGroup} gesendet.", .contactRequest: "möchte sich mit dir vernetzen.", .acceptRequest: "ist jetzt mit dir vernetzt.", .storedMediaFile: "hat dein Bild gespeichert.", @@ -228,6 +229,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", + .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.", .response: "hat dir{inGroup} geantwortet.", .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", ] @@ -237,6 +239,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .twonly: "sent a twonly{inGroup}.", .video: "sent a video{inGroup}.", .image: "sent a image{inGroup}.", + .audio: "sent a voice message{inGroup}.", .contactRequest: "wants to connect with you.", .acceptRequest: "is now connected with you.", .storedMediaFile: "has stored your image.", @@ -246,6 +249,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str .reactionToVideo: "has reacted with {{content}} to your video.", .reactionToText: "has reacted with {{content}} to your text.", .reactionToImage: "has reacted with {{content}} to your image.", + .reactionToAudio: "has reacted with {{content}} to your voice message.", .response: "has responded{inGroup}.", .addedToGroup: "has added you to \"{{content}}\"", ] diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index d3dee0c..88baecf 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -37,7 +37,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case reactionToVideo // = 11 case reactionToText // = 12 case reactionToImage // = 13 - case addedToGroup // = 14 + case reactionToAudio // = 14 + case addedToGroup // = 15 + case audio // = 16 case UNRECOGNIZED(Int) init() { @@ -60,7 +62,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case 11: self = .reactionToVideo case 12: self = .reactionToText case 13: self = .reactionToImage - case 14: self = .addedToGroup + case 14: self = .reactionToAudio + case 15: self = .addedToGroup + case 16: self = .audio default: self = .UNRECOGNIZED(rawValue) } } @@ -81,7 +85,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case .reactionToVideo: return 11 case .reactionToText: return 12 case .reactionToImage: return 13 - case .addedToGroup: return 14 + case .reactionToAudio: return 14 + case .addedToGroup: return 15 + case .audio: return 16 case .UNRECOGNIZED(let i): return i } } @@ -102,7 +108,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { .reactionToVideo, .reactionToText, .reactionToImage, + .reactionToAudio, .addedToGroup, + .audio, ] } @@ -218,7 +226,7 @@ struct PushKey: Sendable { // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PushKind: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}addedToGroup\0") + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}reactionToAudio\0\u{1}addedToGroup\0\u{1}audio\0") } extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3c00a85..57db739 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audio_waveforms (0.0.1): + - Flutter - background_downloader (0.0.1): - Flutter - camera_avfoundation (0.0.1): @@ -247,6 +249,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) @@ -307,6 +310,8 @@ SPEC REPOS: - SwiftProtobuf EXTERNAL SOURCES: + audio_waveforms: + :path: ".symlinks/plugins/audio_waveforms/ios" background_downloader: :path: ".symlinks/plugins/background_downloader/ios" camera_avfoundation: @@ -369,6 +374,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: + audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index b5da4a1..03fae40 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -89,6 +89,7 @@ class MediaFilesDao extends DatabaseAccessor ..where( (t) => t.uploadState.equals(UploadState.initialized.name) | + t.uploadState.equals(UploadState.uploadLimitReached.name) | t.uploadState.equals(UploadState.preprocessing.name), )) .get(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 993ea08..3576abe 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -50,6 +50,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { mediaFiles.downloadState .equals(DownloadState.reuploadRequested.name) .not() & + mediaFiles.type.equals(MediaType.audio.name).not() & messages.openedAt.isNull() & messages.groupId.equals(groupId) & messages.mediaId.isNotNull() & diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 585b49d..6651485 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -5,6 +5,7 @@ enum MediaType { image, video, gif, + audio, } enum UploadState { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 182be3d..f14986a 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -648,6 +648,7 @@ "appOutdatedBtn": "Jetzt aktualisieren.", "@appOutdatedBtn": {}, "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", + "uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.", "@doubleClickToReopen": {}, "retransmissionRequested": "Wird erneut versucht.", "@retransmissionRequested": {}, @@ -784,6 +785,7 @@ "notificationTwonly": "hat ein twonly{inGroup} gesendet.", "notificationVideo": "hat ein Video{inGroup} gesendet.", "notificationImage": "hat ein Bild{inGroup} gesendet.", + "notificationAudio": "hat eine Sprachnachricht{inGroup} gesendet.", "notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.", "notificationContactRequest": "möchte sich mit dir vernetzen.", "notificationAcceptRequest": "ist jetzt mit dir vernetzt.", @@ -793,6 +795,7 @@ "notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.", "notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.", "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", + "notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.", "notificationResponse": "hat dir{inGroup} geantwortet.", "notificationTitleUnknownUser": "Jemand", "notificationCategoryMessageTitle": "Nachrichten", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c014f78..08f553c 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -495,6 +495,7 @@ "appOutdated": "Your version of twonly is out of date.", "appOutdatedBtn": "Update Now", "doubleClickToReopen": "Double-click\nto open again", + "uploadLimitReached": "The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.", "retransmissionRequested": "Retransmission requested", "testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!", "openChangeLog": "Open changelog automatically", @@ -562,6 +563,7 @@ "notificationTwonly": "sent a twonly{inGroup}.", "notificationVideo": "sent a video{inGroup}.", "notificationImage": "sent a image{inGroup}.", + "notificationAudio": "sent a voice message{inGroup}.", "notificationAddedToGroup": "has added you to \"{groupname}\"", "notificationContactRequest": "wants to connect with you.", "notificationAcceptRequest": "is now connected with you.", @@ -571,6 +573,7 @@ "notificationReactionToVideo": "has reacted with {reaction} to your video.", "notificationReactionToText": "has reacted with {reaction} to your message.", "notificationReactionToImage": "has reacted with {reaction} to your image.", + "notificationReactionToAudio": "has reacted with {reaction} to your audio message.", "notificationResponse": "has responded{inGroup}.", "notificationTitleUnknownUser": "Someone", "notificationCategoryMessageTitle": "Messages", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index f329ac9..140bc39 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2072,6 +2072,12 @@ abstract class AppLocalizations { /// **'Double-click\nto open again'** String get doubleClickToReopen; + /// No description provided for @uploadLimitReached. + /// + /// In en, this message translates to: + /// **'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.'** + String get uploadLimitReached; + /// No description provided for @retransmissionRequested. /// /// In en, this message translates to: @@ -2474,6 +2480,12 @@ abstract class AppLocalizations { /// **'sent a image{inGroup}.'** String notificationImage(Object inGroup); + /// No description provided for @notificationAudio. + /// + /// In en, this message translates to: + /// **'sent a voice message{inGroup}.'** + String notificationAudio(Object inGroup); + /// No description provided for @notificationAddedToGroup. /// /// In en, this message translates to: @@ -2528,6 +2540,12 @@ abstract class AppLocalizations { /// **'has reacted with {reaction} to your image.'** String notificationReactionToImage(Object reaction); + /// No description provided for @notificationReactionToAudio. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your audio message.'** + String notificationReactionToAudio(Object reaction); + /// No description provided for @notificationResponse. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index bc98dea..2d8c516 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1098,6 +1098,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get doubleClickToReopen => 'Doppelklicken zum\nerneuten Öffnen.'; + @override + String get uploadLimitReached => + 'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.'; + @override String get retransmissionRequested => 'Wird erneut versucht.'; @@ -1352,6 +1356,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'hat ein Bild$inGroup gesendet.'; } + @override + String notificationAudio(Object inGroup) { + return 'hat eine Sprachnachricht$inGroup gesendet.'; + } + @override String notificationAddedToGroup(Object groupname) { return 'hat dich zu \"$groupname\" hinzugefügt.'; @@ -1387,6 +1396,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'hat mit $reaction auf dein Bild reagiert.'; } + @override + String notificationReactionToAudio(Object reaction) { + return 'hat mit $reaction auf deine Sprachnachricht reagiert.'; + } + @override String notificationResponse(Object inGroup) { return 'hat dir$inGroup geantwortet.'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index ff1a8e0..9834524 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1091,6 +1091,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get doubleClickToReopen => 'Double-click\nto open again'; + @override + String get uploadLimitReached => + 'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.'; + @override String get retransmissionRequested => 'Retransmission requested'; @@ -1344,6 +1348,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'sent a image$inGroup.'; } + @override + String notificationAudio(Object inGroup) { + return 'sent a voice message$inGroup.'; + } + @override String notificationAddedToGroup(Object groupname) { return 'has added you to \"$groupname\"'; @@ -1379,6 +1388,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'has reacted with $reaction to your image.'; } + @override + String notificationReactionToAudio(Object reaction) { + return 'has reacted with $reaction to your audio message.'; + } + @override String notificationResponse(Object inGroup) { return 'has responded$inGroup.'; diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart index 4651437..11a6701 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -71,12 +71,14 @@ class EncryptedContent_Media_Type extends $pb.ProtobufEnum { static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE'); static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO'); static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(3, _omitEnumNames ? '' : 'GIF'); + static const EncryptedContent_Media_Type AUDIO = EncryptedContent_Media_Type._(4, _omitEnumNames ? '' : 'AUDIO'); static const $core.List values = [ REUPLOAD, IMAGE, VIDEO, GIF, + AUDIO, ]; static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index a0fd2af..08c49e4 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -264,6 +264,7 @@ const EncryptedContent_Media_Type$json = { {'1': 'IMAGE', '2': 1}, {'1': 'VIDEO', '2': 2}, {'1': 'GIF', '2': 3}, + {'1': 'AUDIO', '2': 4}, ], }; @@ -409,7 +410,7 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo' 'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE' 'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH' - 'CgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' + 'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' 'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD' 'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG' 'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1' @@ -417,31 +418,31 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI' 'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2' '5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu' - 'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRV' - 'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0' - 'SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl' - '9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEK' - 'C01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYX' - 'RlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQi' - 'NgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAh' - 'p4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250' - 'YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEg' - 'oKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRD' - 'b250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGA' - 'IgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNl' - 'cm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZR' - 'ILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3Vz' - 'ZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3' - 'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ' - 'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' - 'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' - 'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' - 'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' - 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG' - 'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf' - 'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW' - 'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l' - 'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX' - 'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi' - 'bGljS2V5'); + 'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV' + 'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k' + 'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2' + 'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp' + 'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn' + 'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl' + 'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE' + 'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD' + 'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg' + 'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk' + 'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0' + 'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD' + 'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY' + 'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl' + 'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA' + 'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg' + 'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS' + 'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf' + 'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG' + 'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW' + 'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi' + 'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl' + 'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW' + 'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz' + 'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW' + 'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz' + 'ZW5kR3JvdXBQdWJsaWNLZXk='); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart index e2d05ee..2a2fbf7 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart @@ -28,7 +28,9 @@ class PushKind extends $pb.ProtobufEnum { static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo'); static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText'); static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage'); - static const PushKind addedToGroup = PushKind._(14, _omitEnumNames ? '' : 'addedToGroup'); + static const PushKind reactionToAudio = PushKind._(14, _omitEnumNames ? '' : 'reactionToAudio'); + static const PushKind addedToGroup = PushKind._(15, _omitEnumNames ? '' : 'addedToGroup'); + static const PushKind audio = PushKind._(16, _omitEnumNames ? '' : 'audio'); static const $core.List values = [ reaction, @@ -45,7 +47,9 @@ class PushKind extends $pb.ProtobufEnum { reactionToVideo, reactionToText, reactionToImage, + reactionToAudio, addedToGroup, + audio, ]; static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart index 9782873..4d576a1 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart @@ -31,7 +31,9 @@ const PushKind$json = { {'1': 'reactionToVideo', '2': 11}, {'1': 'reactionToText', '2': 12}, {'1': 'reactionToImage', '2': 13}, - {'1': 'addedToGroup', '2': 14}, + {'1': 'reactionToAudio', '2': 14}, + {'1': 'addedToGroup', '2': 15}, + {'1': 'audio', '2': 16}, ], }; @@ -41,7 +43,8 @@ final $typed_data.Uint8List pushKindDescriptor = $convert.base64Decode( 'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw' 'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ' 'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0' - 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEAoMYWRkZWRUb0dyb3VwEA4='); + 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEwoPcmVhY3Rpb25Ub0F1ZGlvEA4SEAoMYWRkZWRUb0' + 'dyb3VwEA8SCQoFYXVkaW8QEA=='); @$core.Deprecated('Use encryptedPushNotificationDescriptor instead') const EncryptedPushNotification$json = { diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 301ac2e..61f6364 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -107,6 +107,7 @@ message EncryptedContent { IMAGE = 1; VIDEO = 2; GIF = 3; + AUDIO = 4; } string senderMessageId = 1; diff --git a/lib/src/model/protobuf/client/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto index c30d715..5e74e9c 100644 --- a/lib/src/model/protobuf/client/push_notification.proto +++ b/lib/src/model/protobuf/client/push_notification.proto @@ -22,7 +22,9 @@ enum PushKind { reactionToVideo = 11; reactionToText = 12; reactionToImage = 13; - addedToGroup = 14; + reactionToAudio = 14; + addedToGroup = 15; + audio = 16; }; message PushNotification { diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 10108b6..616086d 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -62,6 +62,8 @@ Future handleMedia( mediaType = MediaType.video; case EncryptedContent_Media_Type.GIF: mediaType = MediaType.gif; + case EncryptedContent_Media_Type.AUDIO: + mediaType = MediaType.audio; } final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 7c57699..ec8f659 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -30,38 +30,52 @@ Future tryDownloadAllMediaFiles({bool force = false}) async { enum DownloadMediaTypes { video, image, + audio, } Map> defaultAutoDownloadOptions = { - ConnectivityResult.mobile.name: [], + ConnectivityResult.mobile.name: [ + DownloadMediaTypes.audio.name, + ], ConnectivityResult.wifi.name: [ DownloadMediaTypes.video.name, DownloadMediaTypes.image.name, + DownloadMediaTypes.audio.name, ], }; -Future isAllowedToDownload({required bool isVideo}) async { +Future isAllowedToDownload(MediaType type) async { final connectivityResult = await Connectivity().checkConnectivity(); final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; if (connectivityResult.contains(ConnectivityResult.mobile)) { - if (isVideo) { + if (type == MediaType.video) { if (options[ConnectivityResult.mobile.name]! .contains(DownloadMediaTypes.video.name)) { return true; + } else if (type == MediaType.audio) { + if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.audio.name)) { + return true; + } + } else if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.image.name)) { + return true; } - } else if (options[ConnectivityResult.mobile.name]! - .contains(DownloadMediaTypes.image.name)) { - return true; } } if (connectivityResult.contains(ConnectivityResult.wifi)) { - if (isVideo) { + if (type == MediaType.video) { if (options[ConnectivityResult.wifi.name]! .contains(DownloadMediaTypes.video.name)) { return true; } + } else if (type == MediaType.audio) { + if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.audio.name)) { + return true; + } } else if (options[ConnectivityResult.wifi.name]! .contains(DownloadMediaTypes.image.name)) { return true; @@ -110,8 +124,7 @@ Future startDownloadMedia(MediaFile media, bool force) async { return; } - if (!force && - !await isAllowedToDownload(isVideo: media.type == MediaType.video)) { + if (!force && !await isAllowedToDownload(media.type)) { Log.warn( 'Download blocked for ${media.mediaId} because of network state.', ); diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index d4fc7c1..8d1283e 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -58,8 +58,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { final mediaId = update.task.taskId.replaceAll('upload_', ''); final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); - if (update.status == TaskStatus.enqueued || - update.status == TaskStatus.running) { + if (update.status == TaskStatus.running) { // Ignore these updates return; } @@ -103,25 +102,34 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { Log.error( 'Got HTTP error ${update.responseStatusCode} for $mediaId', ); + } - if (update.responseStatusCode == 429) { - await twonlyDB.mediaFilesDao.updateMedia( - mediaId, - const MediaFilesCompanion( - uploadState: Value(UploadState.uploadLimitReached), - ), - ); - return; - } + if (update.status == TaskStatus.notFound) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploadLimitReached), + ), + ); + Log.info( + 'Background upload failed for $mediaId with status ${update.responseStatusCode}. Not trying again.', + ); + return; } Log.info( - 'Background upload failed for $mediaId with status ${update.status}. Trying again.', + 'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ', ); - final mediaService = await MediaFileService.fromMedia(media); + if (update.status == TaskStatus.failed || + update.status == TaskStatus.canceled) { + Log.error( + 'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ', + ); + final mediaService = await MediaFileService.fromMedia(media); - await mediaService.setUploadState(UploadState.uploaded); - // In all other cases just try the upload again... - await startBackgroundMediaUpload(mediaService); + await mediaService.setUploadState(UploadState.uploaded); + // In all other cases just try the upload again... + await startBackgroundMediaUpload(mediaService); + } } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 52b3f53..d4a6783 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -115,7 +115,8 @@ Future startBackgroundMediaUpload(MediaFileService mediaService) async { } } - if (mediaService.mediaFile.uploadState == UploadState.uploading) { + if (mediaService.mediaFile.uploadState == UploadState.uploading || + mediaService.mediaFile.uploadState == UploadState.uploadLimitReached) { await _uploadUploadRequest(mediaService); } } @@ -180,11 +181,16 @@ Future _createUploadRequest(MediaFileService media) async { final downloadToken = getRandomUint8List(32); - var type = EncryptedContent_Media_Type.IMAGE; - if (media.mediaFile.type == MediaType.video) { - type = EncryptedContent_Media_Type.VIDEO; - } else if (media.mediaFile.type == MediaType.gif) { - type = EncryptedContent_Media_Type.GIF; + late EncryptedContent_Media_Type type; + switch (media.mediaFile.type) { + case MediaType.audio: + type = EncryptedContent_Media_Type.AUDIO; + case MediaType.image: + type = EncryptedContent_Media_Type.IMAGE; + case MediaType.gif: + type = EncryptedContent_Media_Type.GIF; + case MediaType.video: + type = EncryptedContent_Media_Type.VIDEO; } final notEncryptedContent = EncryptedContent( diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 5c09e10..27e2ea1 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -78,6 +78,9 @@ class MediaFileService { // Message was not yet opened, so do not remove it. delete = false; } + if (service.mediaFile.type == MediaType.audio) { + delete = false; // do not delete voice messages + } } } } @@ -162,6 +165,7 @@ class MediaFileService { } switch (mediaFile.type) { case MediaType.gif: + case MediaType.audio: case MediaType.image: // all images are already compress.. break; @@ -181,6 +185,7 @@ class MediaFileService { await compressImage(originalPath, tempPath); case MediaType.video: await compressAndOverlayVideo(this); + case MediaType.audio: case MediaType.gif: originalPath.copySync(tempPath.path); } @@ -267,6 +272,8 @@ class MediaFileService { extension = 'mp4'; case MediaType.gif: extension = 'gif'; + case MediaType.audio: + extension = 'm4a'; } } final mediaBaseDir = diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 935ea22..ac5a889 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -249,6 +249,7 @@ String getPushNotificationText(PushNotification pushNotification) { PushKind.twonly.name: lang.notificationTwonly(inGroup), PushKind.video.name: lang.notificationVideo(inGroup), PushKind.image.name: lang.notificationImage(inGroup), + PushKind.video.name: lang.notificationAudio(inGroup), PushKind.contactRequest.name: lang.notificationContactRequest, PushKind.acceptRequest.name: lang.notificationAcceptRequest, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, @@ -256,6 +257,8 @@ String getPushNotificationText(PushNotification pushNotification) { PushKind.reopenedMedia.name: lang.notificationReopenedMedia, PushKind.reactionToVideo.name: lang.notificationReactionToVideo(pushNotification.additionalContent), + PushKind.reactionToAudio.name: + lang.notificationReactionToAudio(pushNotification.additionalContent), PushKind.reactionToText.name: lang.notificationReactionToText(pushNotification.additionalContent), PushKind.reactionToImage.name: diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 033f2c3..64509a8 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -220,6 +220,8 @@ Future getPushNotificationFromEncryptedContent( switch (media.type) { case MediaType.image: kind = PushKind.reactionToImage; + case MediaType.audio: + kind = PushKind.reactionToAudio; case MediaType.video: kind = PushKind.reactionToVideo; case MediaType.gif: @@ -241,13 +243,16 @@ Future getPushNotificationFromEncryptedContent( } if (content.hasMedia()) { switch (content.media.type) { + case EncryptedContent_Media_Type.REUPLOAD: + return null; case EncryptedContent_Media_Type.IMAGE: kind = PushKind.image; case EncryptedContent_Media_Type.VIDEO: kind = PushKind.video; - // ignore: no_default_cases - default: - return null; + case EncryptedContent_Media_Type.GIF: + kind = PushKind.image; + case EncryptedContent_Media_Type.AUDIO: + kind = PushKind.audio; } if (content.media.requiresAuthentication) { kind = PushKind.twonly; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b121035..c41c306 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -293,6 +293,8 @@ Color getMessageColorFromType( } else { if (mediaFile.type == MediaType.video) { color = const Color.fromARGB(255, 243, 33, 208); + } else if (mediaFile.type == MediaType.audio) { + color = const Color.fromARGB(255, 252, 149, 85); } else { color = Colors.redAccent; } diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 4b1ce6c..6f1939a 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -176,22 +176,24 @@ class _UserListItem extends State { _previewMessages.where((x) => x.type == MessageType.media).toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); - if (mediaFile?.downloadState == null) return; - if (mediaFile!.downloadState! == DownloadState.pending) { - await startDownloadMedia(mediaFile, true); - return; - } - if (mediaFile.downloadState! == DownloadState.ready) { - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return MediaViewerView(widget.group); - }, - ), - ); - return; + if (mediaFile?.type != MediaType.audio) { + if (mediaFile?.downloadState == null) return; + if (mediaFile!.downloadState! == DownloadState.pending) { + await startDownloadMedia(mediaFile, true); + return; + } + if (mediaFile.downloadState! == DownloadState.ready) { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MediaViewerView(widget.group); + }, + ), + ); + return; + } } } if (!mounted) return; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index abd8a04..4526e8e 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mutex/mutex.dart'; @@ -11,9 +12,9 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 0c48305..496d97f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -2,14 +2,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart' hide MessageActions; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; @@ -136,13 +139,23 @@ class _ChatListEntryState extends State { ) : (mediaService == null) ? null - : ChatMediaEntry( - message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - minWidth: reactionsForWidth * 43, - ), + : (mediaService!.mediaFile.type == MediaType.audio) + ? ChatAudioEntry( + message: widget.message, + nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + mediaService: mediaService!, + userIdToContact: widget.userIdToContact, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + minWidth: reactionsForWidth * 43, + ), ), if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), ], diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart deleted file mode 100644 index 03940ab..0000000 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:intl/intl.dart' hide TextDirection; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/messages.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/components/animate_icon.dart'; -import 'package:twonly/src/views/components/better_text.dart'; - -class ChatTextEntry extends StatelessWidget { - const ChatTextEntry({ - required this.message, - required this.nextMessage, - required this.prevMessage, - required this.borderRadius, - required this.userIdToContact, - required this.minWidth, - super.key, - }); - - final Message message; - final Message? nextMessage; - final Message? prevMessage; - final Map? userIdToContact; - final BorderRadius borderRadius; - final double minWidth; - - @override - Widget build(BuildContext context) { - var text = message.content ?? ''; - var textColor = Colors.white; - - if (EmojiAnimation.supported(text)) { - return Container( - constraints: const BoxConstraints( - maxWidth: 100, - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 10, - ), - child: EmojiAnimation(emoji: text), - ); - } - - var displayTime = !combineTextMessageWithNext(message, nextMessage); - var displayUserName = ''; - if (message.senderId != null && - userIdToContact != null && - userIdToContact![message.senderId] != null) { - if (prevMessage == null) { - displayUserName = - getContactDisplayName(userIdToContact![message.senderId]!); - } else { - if (!combineTextMessageWithNext(prevMessage!, message)) { - displayUserName = - getContactDisplayName(userIdToContact![message.senderId]!); - } - } - } - - var spacerWidth = minWidth - measureTextWidth(text) - 53; - if (spacerWidth < 0) spacerWidth = 0; - - Color? color; - var expanded = false; - if (message.quotesMessageId == null) { - color = getMessageColor(message); - } - if (message.isDeletedFromSender) { - color = context.color.surfaceBright; - displayTime = false; - } else if (measureTextWidth(text) > 270) { - expanded = true; - } - - if (message.isDeletedFromSender) { - text = context.lang.messageWasDeleted; - color = isDarkMode(context) ? Colors.black : Colors.grey; - if (isDarkMode(context)) { - textColor = const Color.fromARGB(255, 99, 99, 99); - } - } - - return Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.8, - minWidth: minWidth, - ), - padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), - decoration: BoxDecoration( - color: color, - borderRadius: borderRadius, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (displayUserName != '') - Text( - displayUserName, - textAlign: TextAlign.left, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (expanded) - Expanded( - child: BetterText(text: text, textColor: textColor), - ) - else ...[ - BetterText(text: text, textColor: textColor), - SizedBox( - width: spacerWidth, - ), - ], - if (displayTime || message.modifiedAt != null) - Align( - alignment: AlignmentGeometry.centerRight, - child: Padding( - padding: const EdgeInsets.only(left: 6), - child: Row( - children: [ - if (message.modifiedAt != null) - Padding( - padding: const EdgeInsets.only(right: 5), - child: SizedBox( - height: 10, - child: FaIcon( - FontAwesomeIcons.pencil, - color: Colors.white.withAlpha(150), - size: 10, - ), - ), - ), - Text( - friendlyTime( - context, - (message.modifiedAt != null) - ? message.modifiedAt! - : message.createdAt, - ), - style: TextStyle( - fontSize: 10, - color: Colors.white.withAlpha(150), - decoration: TextDecoration.none, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - ), - ], - ), - ], - ), - ); - } -} - -double measureTextWidth( - String text, -) { - final tp = TextPainter( - text: TextSpan(text: text, style: const TextStyle(fontSize: 17)), - textDirection: TextDirection.ltr, - maxLines: 1, - )..layout(); - return tp.size.width; -} - -bool combineTextMessageWithNext(Message message, Message? nextMessage) { - if (nextMessage != null && nextMessage.content != null) { - if (nextMessage.senderId == message.senderId) { - if (nextMessage.type == MessageType.text && - message.type == MessageType.text) { - if (!EmojiAnimation.supported(nextMessage.content!)) { - final diff = - nextMessage.createdAt.difference(message.createdAt).inMinutes; - if (diff <= 1) { - return true; - } - } - } - } - } - return false; -} - -String friendlyTime(BuildContext context, DateTime dt) { - final now = DateTime.now(); - final diff = now.difference(dt); - - if (diff.inMinutes >= 0 && diff.inMinutes < 60) { - final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes; - if (minutes <= 1) { - return context.lang.now; - } - return '$minutes ${context.lang.minutesShort}'; - } - - // Determine 24h vs 12h from system/local settings - final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; - - if (!use24Hour) { - // 12-hour format with locale-aware AM/PM - final format = DateFormat.jm(Localizations.localeOf(context).toString()); - return format.format(dt); - } else { - // 24-hour HH:mm, locale-aware - final format = DateFormat.Hm(Localizations.localeOf(context).toString()); - return format.format(dt); - } -} diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart new file mode 100644 index 0000000..24267fb --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart @@ -0,0 +1,232 @@ +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatAudioEntry extends StatelessWidget { + const ChatAudioEntry({ + required this.message, + required this.nextMessage, + required this.mediaService, + required this.prevMessage, + required this.borderRadius, + required this.userIdToContact, + required this.minWidth, + super.key, + }); + + final Message message; + final MediaFileService mediaService; + final Message? nextMessage; + final Message? prevMessage; + final Map? userIdToContact; + final BorderRadius borderRadius; + final double minWidth; + + @override + Widget build(BuildContext context) { + if (!mediaService.tempPath.existsSync() && + !mediaService.originalPath.existsSync()) { + return Container(); // media file was purged + } + final info = getBubbleInfo( + context, + message, + nextMessage, + prevMessage, + userIdToContact, + minWidth, + ); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + minWidth: 250, + ), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: info.color, + borderRadius: borderRadius, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info.displayUserName != '') + Text( + info.displayUserName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (info.text != '') + Expanded( + child: BetterText(text: info.text, textColor: info.textColor), + ) + else ...[ + if (mediaService.mediaFile.downloadState == + DownloadState.ready || + mediaService.mediaFile.downloadState == null) + mediaService.tempPath.existsSync() + ? InChatAudioPlayer( + path: mediaService.tempPath.path, + message: message, + ) + : (mediaService.originalPath.existsSync()) + ? InChatAudioPlayer( + path: mediaService.originalPath.path, + message: message, + ) + : Container() + else + MessageSendStateIcon([message], [mediaService.mediaFile]), + ], + if (info.displayTime || message.modifiedAt != null) + FriendlyMessageTime(message: message), + ], + ), + ], + ), + ); + } +} + +class InChatAudioPlayer extends StatefulWidget { + const InChatAudioPlayer({ + required this.path, + required this.message, + super.key, + }); + + final String path; + final Message message; + + @override + State createState() => _InChatAudioPlayerState(); +} + +class _InChatAudioPlayerState extends State { + final PlayerController _playerController = PlayerController(); + int _displayDuration = 0; + int _maxDuration = 0; + + @override + void initState() { + super.initState(); + _playerController + ..preparePlayer(path: widget.path) + ..setFinishMode(finishMode: FinishMode.pause); + + _playerController.onCompletion.listen((_) { + if (mounted) { + setState(() { + _isPlaying = false; + _playerController.seekTo(0); + }); + } + }); + + _playerController.onCurrentDurationChanged.listen((duration) { + if (mounted) { + setState(() { + _displayDuration = _maxDuration - duration; + }); + } + }); + initAsync(); + } + + @override + void dispose() { + _playerController.dispose(); + super.dispose(); + } + + Future initAsync() async { + _displayDuration = await _playerController.getDuration(DurationType.max); + _maxDuration = _displayDuration; + if (!mounted) return; + setState(() {}); + } + + bool _isPlaying = false; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (_isPlaying) { + _playerController.pausePlayer(); + } else { + _playerController.startPlayer(); + if (widget.message.senderId != null && + widget.message.openedAt == null) { + notifyContactAboutOpeningMessage( + widget.message.senderId!, + [widget.message.messageId], + ); + } + } + setState(() { + _isPlaying = !_isPlaying; + }); + }, + child: Container( + padding: EdgeInsets.only( + left: _isPlaying ? 2 : 0, + top: 4, + bottom: 4, + ), + color: Colors.transparent, + child: FaIcon( + _isPlaying ? FontAwesomeIcons.pause : FontAwesomeIcons.play, + size: 20, + color: Colors.white, + ), + ), + ), + Text( + formatMsToMinSec(_displayDuration), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + AudioFileWaveforms( + playerController: _playerController, + size: const Size(150, 40), + ), + ], + ); + } +} + +String formatMsToMinSec(int milliseconds) { + final d = Duration(milliseconds: milliseconds); + final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; +} diff --git a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart b/lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart similarity index 100% rename from lib/src/views/chats/chat_messages_components/chat_date_chip.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart similarity index 100% rename from lib/src/views/chats/chat_messages_components/chat_media_entry.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart new file mode 100644 index 0000000..89c336e --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatTextEntry extends StatelessWidget { + const ChatTextEntry({ + required this.message, + required this.nextMessage, + required this.prevMessage, + required this.borderRadius, + required this.userIdToContact, + required this.minWidth, + super.key, + }); + + final Message message; + final Message? nextMessage; + final Message? prevMessage; + final Map? userIdToContact; + final BorderRadius borderRadius; + final double minWidth; + + @override + Widget build(BuildContext context) { + final text = message.content ?? ''; + + if (EmojiAnimation.supported(text)) { + return Container( + constraints: const BoxConstraints( + maxWidth: 100, + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 10, + ), + child: EmojiAnimation(emoji: text), + ); + } + + final info = getBubbleInfo( + context, + message, + nextMessage, + prevMessage, + userIdToContact, + minWidth, + ); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + minWidth: minWidth, + ), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: info.color, + borderRadius: borderRadius, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info.displayUserName != '') + Text( + info.displayUserName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (info.expanded) + Expanded( + child: BetterText(text: text, textColor: info.textColor), + ) + else ...[ + BetterText(text: text, textColor: info.textColor), + SizedBox( + width: info.spacerWidth, + ), + ], + if (info.displayTime || message.modifiedAt != null) + FriendlyMessageTime(message: message), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/common.dart b/lib/src/views/chats/chat_messages_components/entries/common.dart new file mode 100644 index 0000000..da56997 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/common.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +class BubbleInfo { + late String text; + late Color textColor; + late bool displayTime; + late String displayUserName; + late Color color; + late bool expanded; + late double spacerWidth; +} + +BubbleInfo getBubbleInfo( + BuildContext context, + Message message, + Message? nextMessage, + Message? prevMessage, + Map? userIdToContact, + double minWidth, +) { + final info = BubbleInfo() + ..text = message.content ?? '' + ..textColor = Colors.white + ..color = getMessageColor(message) + ..displayTime = !combineTextMessageWithNext(message, nextMessage) + ..displayUserName = ''; + + if (message.senderId != null && + userIdToContact != null && + userIdToContact[message.senderId] != null) { + if (prevMessage == null) { + info.displayUserName = + getContactDisplayName(userIdToContact[message.senderId]!); + } else { + if (!combineTextMessageWithNext(prevMessage, message)) { + info.displayUserName = + getContactDisplayName(userIdToContact[message.senderId]!); + } + } + } + + info.spacerWidth = minWidth - measureTextWidth(info.text) - 53; + if (info.spacerWidth < 0) info.spacerWidth = 0; + + info.expanded = false; + if (message.quotesMessageId == null) { + info.color = getMessageColor(message); + } + if (message.isDeletedFromSender) { + info + ..color = context.color.surfaceBright + ..displayTime = false; + } else if (measureTextWidth(info.text) > 270) { + info.expanded = true; + } + + if (message.isDeletedFromSender) { + info + ..text = context.lang.messageWasDeleted + ..color = isDarkMode(context) ? Colors.black : Colors.grey; + if (isDarkMode(context)) { + info.textColor = const Color.fromARGB(255, 99, 99, 99); + } + } + return info; +} + +double measureTextWidth( + String text, +) { + final tp = TextPainter( + text: TextSpan(text: text, style: const TextStyle(fontSize: 17)), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return tp.size.width; +} + +bool combineTextMessageWithNext(Message message, Message? nextMessage) { + if (nextMessage != null && nextMessage.content != null) { + if (nextMessage.senderId == message.senderId) { + if (nextMessage.type == MessageType.text && + message.type == MessageType.text) { + if (!EmojiAnimation.supported(nextMessage.content!)) { + final diff = + nextMessage.createdAt.difference(message.createdAt).inMinutes; + if (diff <= 1) { + return true; + } + } + } + } + } + return false; +} diff --git a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart new file mode 100644 index 0000000..1d793e0 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class FriendlyMessageTime extends StatelessWidget { + const FriendlyMessageTime({required this.message, super.key}); + + final Message message; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentGeometry.centerRight, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Row( + children: [ + if (message.modifiedAt != null) + Padding( + padding: const EdgeInsets.only(right: 5), + child: SizedBox( + height: 10, + child: FaIcon( + FontAwesomeIcons.pencil, + color: Colors.white.withAlpha(150), + size: 10, + ), + ), + ), + Text( + friendlyTime( + context, + (message.modifiedAt != null) + ? message.modifiedAt! + : message.createdAt, + ), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} + +String friendlyTime(BuildContext context, DateTime dt) { + final now = DateTime.now(); + final diff = now.difference(dt); + + if (diff.inMinutes >= 0 && diff.inMinutes < 60) { + final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes; + if (minutes <= 1) { + return context.lang.now; + } + return '$minutes ${context.lang.minutesShort}'; + } + + // Determine 24h vs 12h from system/local settings + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + if (!use24Hour) { + // 12-hour format with locale-aware AM/PM + final format = DateFormat.jm(Localizations.localeOf(context).toString()); + return format.format(dt); + } else { + // 24-hour HH:mm, locale-aware + final format = DateFormat.Hm(Localizations.localeOf(context).toString()); + return format.format(dt); + } +} diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index a0768dd..ec24342 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -1,8 +1,15 @@ +import 'dart:async'; import 'dart:io'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; @@ -25,10 +32,14 @@ class MessageInput extends StatefulWidget { State createState() => _MessageInputState(); } +enum RecordingState { none, recording, finished } + class _MessageInputState extends State { late final TextEditingController _textFieldController; + late final RecorderController recorderController; final bool isApple = Platform.isIOS; bool _emojiShowing = false; + RecordingState _recordingState = RecordingState.none; Future _sendMessage() async { if (_textFieldController.text == '') return; @@ -48,6 +59,7 @@ class _MessageInputState extends State { void initState() { _textFieldController = TextEditingController(); widget.textFieldFocus.addListener(_handleTextFocusChange); + _initializeControllers(); super.initState(); } @@ -58,6 +70,14 @@ class _MessageInputState extends State { super.dispose(); } + void _initializeControllers() { + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 44100; + } + void _handleTextFocusChange() { if (widget.textFieldFocus.hasFocus) { setState(() { @@ -66,6 +86,33 @@ class _MessageInputState extends State { } } + Future _stopAudioRecording() async { + await HapticFeedback.heavyImpact(); + setState(() { + _recordingState = RecordingState.none; + }); + + final audioTmpPath = await recorderController.stop(); + + if (audioTmpPath == null) return; + + final mediaFileService = await initializeMediaUpload( + MediaType.audio, + null, + ); + + if (mediaFileService == null) return; + + File(audioTmpPath) + ..copySync(mediaFileService.originalPath.path) + ..deleteSync(); + + await insertMediaFileInMessagesTable( + mediaFileService, + [widget.group.groupId], + ); + } + @override Widget build(BuildContext context) { return Column( @@ -89,56 +136,164 @@ class _MessageInputState extends State { ), child: Row( children: [ - GestureDetector( - onTap: () { - setState(() { - _emojiShowing = !_emojiShowing; - if (_emojiShowing) { - widget.textFieldFocus.unfocus(); - } else { - widget.textFieldFocus.requestFocus(); - } - }); - }, - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 12, - right: 8, - ), - child: FaIcon( - size: 20, - _emojiShowing - ? FontAwesomeIcons.keyboard - : FontAwesomeIcons.faceSmile, + if (_recordingState != RecordingState.recording) + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, + ), ), ), ), - ), Expanded( - child: TextField( - controller: _textFieldController, - focusNode: widget.textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - setState(() {}); + child: (_recordingState == RecordingState.recording) + ? AudioWaveforms( + enableGesture: true, + size: Size( + MediaQuery.of(context).size.width / 2, + 50, + ), + recorderController: recorderController, + waveStyle: WaveStyle( + waveColor: isDarkMode(context) + ? Colors.white + : Colors.black, + extendWaveform: true, + showMiddleLine: false, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.color.surfaceContainer, + ), + padding: const EdgeInsets.only(left: 18), + margin: + const EdgeInsets.symmetric(horizontal: 15), + ) + : TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + if (_textFieldController.text == '') + GestureDetector( + onLongPressStart: (a) async { + if (!await Permission.microphone.isGranted) { + final statuses = await [ + Permission.microphone, + ].request(); + if (statuses[Permission.microphone]! + .isPermanentlyDenied) { + await openAppSettings(); + return; + } + if (!await Permission.microphone.isGranted) { + return; + } + } + setState(() { + _recordingState = RecordingState.recording; + }); + await HapticFeedback.heavyImpact(); + final audioTmpPath = + '${(await getApplicationCacheDirectory()).path}/recording.m4a'; + unawaited( + recorderController.record( + path: audioTmpPath, + ), + ); }, - onSubmitted: (_) { - _sendMessage(); + onLongPressCancel: () async { + final path = await recorderController.stop(); + if (path == null) return; + if (File(path).existsSync()) { + File(path).deleteSync(); + } + setState(() { + _recordingState = RecordingState.none; + }); }, - style: const TextStyle(fontSize: 17), - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, + onLongPressEnd: (a) => _stopAudioRecording(), + child: Stack( + clipBehavior: Clip.none, + children: [ + if (_recordingState == RecordingState.recording) + 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, + ), + ), + 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, + ), + ), + ), + ], ), ), - ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index cb378a9..e147dbc 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; enum MessageSendState { received, @@ -85,6 +86,7 @@ class _MessageSendStateIconState extends State { final kindsAlreadyShown = HashSet(); var hasLoader = false; + GestureTapCallback? onTap; for (final message in widget.messages) { if (icons.length == 2) break; @@ -147,7 +149,27 @@ class _MessageSendStateIconState extends State { if (mediaFile != null) { if (mediaFile.uploadState == UploadState.uploadLimitReached) { - text = 'Upload Limit erreicht'; + icon = FaIcon( + FontAwesomeIcons.triangleExclamation, + size: 12, + color: color, + ); + + textWidget = Text( + context.lang.uploadLimitReached, + style: const TextStyle(fontSize: 9), + ); + + onTap = () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + }; } if (mediaFile.uploadState == UploadState.preprocessing) { text = 'Wird verarbeitet'; @@ -251,20 +273,23 @@ class _MessageSendStateIconState extends State { ); } - return Row( - mainAxisAlignment: widget.mainAxisAlignment, - children: [ - icon, - const SizedBox(width: 3), - if (textWidget != null) - textWidget - else - Text( - text, - style: const TextStyle(fontSize: 12), - ), - const SizedBox(width: 5), - ], + return GestureDetector( + onTap: onTap, + child: Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: [ + icon, + const SizedBox(width: 3), + if (textWidget != null) + textWidget + else + Text( + text, + style: const TextStyle(fontSize: 12), + ), + const SizedBox(width: 5), + ], + ), ); } } diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index a0b96da..1c27f90 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -175,9 +175,16 @@ class _ResponsePreviewState extends State { } } if (_message!.type == MessageType.media && _mediaService != null) { - subtitle = _mediaService!.mediaFile.type == MediaType.video - ? context.lang.video - : context.lang.image; + switch (_mediaService!.mediaFile.type) { + case MediaType.image: + subtitle = context.lang.image; + case MediaType.video: + subtitle = context.lang.video; + case MediaType.gif: + subtitle = 'Gif'; + case MediaType.audio: + subtitle = 'Audio'; + } } if (_message!.senderId == null) { @@ -241,7 +248,8 @@ class _ResponsePreviewState extends State { ], ), ), - if (_mediaService != null) + if (_mediaService != null && + _mediaService!.mediaFile.type != MediaType.audio) SizedBox( height: widget.showBorder ? 100 : 210, child: Image.file( diff --git a/lib/src/views/chats/chat_messages_components/test b/lib/src/views/chats/chat_messages_components/test new file mode 100644 index 0000000..db5158b --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/test @@ -0,0 +1,242 @@ +import 'dart:io'; + +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:audio_waveforms_example/chat_bubble.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Audio Waveforms', + debugShowCheckedModeBanner: false, + home: Home(), + ); + } +} + +class Home extends StatefulWidget { + const Home({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + late final RecorderController recorderController; + + String? path; + String? musicFile; + bool isRecording = false; + bool isRecordingCompleted = false; + bool isLoading = true; + late Directory appDirectory; + + @override + void initState() { + super.initState(); + _getDir(); + _initialiseControllers(); + } + + void _getDir() async { + appDirectory = await getApplicationDocumentsDirectory(); + path = "${appDirectory.path}/recording.m4a"; + isLoading = false; + setState(() {}); + } + + void _initialiseControllers() { + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 44100; + } + + void _pickFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + if (result != null) { + musicFile = result.files.single.path; + setState(() {}); + } else { + debugPrint("File not picked"); + } + } + + @override + void dispose() { + recorderController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF252331), + appBar: AppBar( + backgroundColor: const Color(0xFF252331), + elevation: 1, + centerTitle: true, + shadowColor: Colors.grey, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/logo.png', + scale: 1.5, + ), + const SizedBox(width: 10), + const Text( + 'Simform', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + body: isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : SafeArea( + child: Column( + children: [ + const SizedBox(height: 20), + Expanded( + child: ListView.builder( + itemCount: 4, + itemBuilder: (_, index) { + return WaveBubble( + index: index + 1, + isSender: index.isOdd, + width: MediaQuery.of(context).size.width / 2, + appDirectory: appDirectory, + ); + }, + ), + ), + if (isRecordingCompleted) + WaveBubble( + path: path, + isSender: true, + appDirectory: appDirectory, + ), + if (musicFile != null) + WaveBubble( + path: musicFile, + isSender: true, + appDirectory: appDirectory, + ), + SafeArea( + child: Row( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isRecording + ? AudioWaveforms( + enableGesture: true, + size: Size( + MediaQuery.of(context).size.width / 2, + 50), + recorderController: recorderController, + waveStyle: const WaveStyle( + waveColor: Colors.white, + extendWaveform: true, + showMiddleLine: false, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xFF1E1B26), + ), + padding: const EdgeInsets.only(left: 18), + margin: const EdgeInsets.symmetric( + horizontal: 15), + ) + : Container( + width: + MediaQuery.of(context).size.width / 1.7, + height: 50, + decoration: BoxDecoration( + color: const Color(0xFF1E1B26), + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.only(left: 18), + margin: const EdgeInsets.symmetric( + horizontal: 15), + child: TextField( + readOnly: true, + decoration: InputDecoration( + hintText: "Type Something...", + hintStyle: const TextStyle( + color: Colors.white54), + contentPadding: + const EdgeInsets.only(top: 16), + border: InputBorder.none, + suffixIcon: IconButton( + onPressed: _pickFile, + icon: Icon(Icons.adaptive.share), + color: Colors.white54, + ), + ), + ), + ), + ), + IconButton( + onPressed: _refreshWave, + icon: Icon( + isRecording ? Icons.refresh : Icons.send, + color: Colors.white, + ), + ), + const SizedBox(width: 16), + IconButton( + onPressed: _startOrStopRecording, + icon: Icon(isRecording ? Icons.stop : Icons.mic), + color: Colors.white, + iconSize: 28, + ), + ], + ), + ), + ], + ), + ), + ); + } + + void _startOrStopRecording() async { + try { + if (isRecording) { + recorderController.reset(); + + path = await recorderController.stop(false); + + if (path != null) { + isRecordingCompleted = true; + debugPrint(path); + debugPrint("Recorded file size: ${File(path!).lengthSync()}"); + } + } else { + await recorderController.record(path: path); // Path is optional + } + } catch (e) { + debugPrint(e.toString()); + } finally { + if (recorderController.hasPermission) { + setState(() { + isRecording = !isRecording; + }); + } + } + } + + void _refreshWave() { + if (isRecording) recorderController.refresh(); + } +} \ No newline at end of file diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index 4c863b7..ee193e7 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -83,9 +83,10 @@ class _MaxFlameListTitleState extends State { @override Widget build(BuildContext context) { if (_directChat == null || + _directChat!.maxFlameCounter == 0 || _flameCounter >= (_directChat!.maxFlameCounter + 1) || _directChat!.lastFlameCounterChange! - .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { + .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { return Container(); } return BetterListTile( diff --git a/pubspec.lock b/pubspec.lock index b1d0c98..ff7558f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + audio_waveforms: + dependency: "direct main" + description: + name: audio_waveforms + sha256: "658fef41bbab299184b65ba2fd749e8ec658c1f7d54a21f7cf97fa96b173b4ce" + url: "https://pub.dev" + source: hosted + version: "1.3.0" avatar_maker: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 352eb50..0489c4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: ^3.6.0 dependencies: + audio_waveforms: ^1.3.0 avatar_maker: ^0.4.0 background_downloader: ^9.2.2 cached_network_image: ^3.4.1 From da4917ad46aed89d1a84308bd345b95f95bbc415 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 7 Nov 2025 17:44:51 +0100 Subject: [PATCH 71/76] fix multiple issues --- android/app/build.gradle | 3 + android/app/src/profile/AndroidManifest.xml | 2 + lib/main.dart | 9 +- lib/src/database/daos/contacts.dao.dart | 1 + lib/src/database/daos/groups.dao.dart | 17 +- lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + lib/src/services/api.service.dart | 4 +- .../mediafiles/media_background.service.dart | 6 +- .../background.notifications.dart | 14 +- .../notifications/pushkeys.notifications.dart | 2 +- lib/src/utils/log.dart | 2 +- lib/src/utils/misc.dart | 2 +- .../zoom_selector.dart | 3 +- .../layers/filters/location_filter.dart | 2 +- .../chat_reaction_row.dart | 10 +- .../entries/chat_media_entry.dart | 7 +- .../entries/chat_text_entry.dart | 4 +- .../message_send_state_icon.dart | 4 +- .../views/chats/chat_messages_components/test | 242 ------------------ lib/src/views/chats/media_viewer.view.dart | 18 +- .../components/max_flame_list_title.dart | 2 +- lib/src/views/settings/account.view.dart | 4 +- .../backup/twonly_safe_backup.view.dart | 2 +- .../developer/automated_testing.view.dart | 2 +- .../settings/developer/developer.view.dart | 4 +- .../views/settings/profile/profile.view.dart | 26 ++ .../settings/subscription/checkout.view.dart | 1 + .../subscription/select_payment.view.dart | 3 + .../subscription/subscription.view.dart | 1 + 33 files changed, 125 insertions(+), 290 deletions(-) delete mode 100644 lib/src/views/chats/chat_messages_components/test diff --git a/android/app/build.gradle b/android/app/build.gradle index a500868..bb0c654 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -55,6 +55,9 @@ android { debug { applicationIdSuffix ".testing" } + // profile { + // applicationIdSuffix ".STOP" + // } release { signingConfig signingConfigs.release } diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index e807f77..8606e90 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -6,4 +6,6 @@ + + diff --git a/lib/main.dart b/lib/main.dart index 61644cd..6b80c64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -63,13 +64,7 @@ void main() async { unawaited(createPushAvatars()); await twonlyDB.messagesDao.purgeMessageTable(); - // await twonlyDB.messagesDao.resetPendingDownloadState(); - // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); - // await twonlyDB.signalDao.purgeOutDatedPreKeys(); - - // unawaited(purgeSendMediaFiles()); - - // unawaited(performTwonlySafeBackup()); + unawaited(performTwonlySafeBackup()); runApp( MultiProvider( diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 7435688..0d4b6a8 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -109,6 +109,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { ..where( contacts.requested.equals(true) & contacts.accepted.equals(false) & + contacts.deletedByUser.equals(false) & contacts.blocked.equals(false), ) ..addColumns([count]); diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 0dd04e8..85ce7ec 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -281,7 +281,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ..where((t) => t.groupId.equals(groupId))) .getSingle(); - final totalMediaCounter = group.totalMediaCounter + 1; + final totalMediaCounter = group.totalMediaCounter + (received ? 0 : 1); var flameCounter = group.flameCounter; var maxFlameCounter = group.maxFlameCounter; var maxFlameCounterFrom = group.maxFlameCounterFrom; @@ -321,7 +321,11 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { if (updateFlame) { flameCounter += 1; lastFlameCounterChange = Value(timestamp); - if ((flameCounter + 1) >= maxFlameCounter) { + // Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days + if ((flameCounter + 1) >= maxFlameCounter || + maxFlameCounterFrom == null || + maxFlameCounterFrom + .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { maxFlameCounter = flameCounter + 1; maxFlameCounterFrom = DateTime.now(); } @@ -351,6 +355,15 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ); } + Stream watchSumTotalMediaCounter() { + final query = selectOnly(groups) + ..addColumns([groups.totalMediaCounter.sum()]); + return query.watch().map((rows) { + final expr = rows.first.read(groups.totalMediaCounter.sum()); + return expr ?? 0; + }); + } + Future increaseLastMessageExchange( String groupId, DateTime newLastMessage, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index f14986a..85299bc 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -816,5 +816,6 @@ "deleteChatAfterADay": "einem Tag.", "deleteChatAfterAWeek": "einer Woche.", "deleteChatAfterAMonth": "einem Monat.", - "deleteChatAfterAYear": "einem Jahr." + "deleteChatAfterAYear": "einem Jahr.", + "yourTwonlyScore": "Dein twonly-Score" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 08f553c..4078005 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -594,5 +594,6 @@ "deleteChatAfterADay": "one day.", "deleteChatAfterAWeek": "one week.", "deleteChatAfterAMonth": "one month.", - "deleteChatAfterAYear": "one year." + "deleteChatAfterAYear": "one year.", + "yourTwonlyScore": "Your twonly-Score" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 140bc39..db8ac2b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2671,6 +2671,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'one year.'** String get deleteChatAfterAYear; + + /// No description provided for @yourTwonlyScore. + /// + /// In en, this message translates to: + /// **'Your twonly-Score'** + String get yourTwonlyScore; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2d8c516..ebe795b 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1474,4 +1474,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get deleteChatAfterAYear => 'einem Jahr.'; + + @override + String get yourTwonlyScore => 'Dein twonly-Score'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 9834524..607bc3f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1464,4 +1464,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteChatAfterAYear => 'one year.'; + + @override + String get yourTwonlyScore => 'Your twonly-Score'; } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index a918d29..4cc3e23 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -51,8 +51,8 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kDebugMode ? '10.99.0.140:3030' : 'api.twonly.eu'; - final String apiSecure = kDebugMode ? '' : 's'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; + final String apiSecure = kReleaseMode ? 's' : ''; bool appIsOutdated = false; bool isAuthenticated = false; diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 8d1283e..6e513d2 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -35,15 +35,15 @@ Future initFileDownloader() async { try { var androidConfig = []; - if (kDebugMode) { - androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; + if (!kReleaseMode) { + androidConfig = [(Config.bypassTLSCertificateValidation, true)]; } await FileDownloader().configure(androidConfig: androidConfig); } catch (e) { Log.error(e); } - if (kDebugMode) { + if (!kReleaseMode) { FileDownloader().configureNotification( running: const TaskNotification( 'Uploading/Downloading', diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index ac5a889..905c3ed 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -23,9 +23,10 @@ Future customLocalPushNotification(String title, String msg) async { '1', 'System', channelDescription: 'System messages.', - importance: Importance.max, - priority: Priority.max, + importance: Importance.high, + priority: Priority.high, styleInformation: BigTextStyleInformation(msg), + icon: 'ic_launcher_foreground', ); const darwinNotificationDetails = DarwinNotificationDetails(); @@ -34,8 +35,10 @@ Future customLocalPushNotification(String title, String msg) async { iOS: darwinNotificationDetails, ); + final id = Random.secure().nextInt(9999); + await flutterLocalNotificationsPlugin.show( - Random.secure().nextInt(9999), + id, title, msg, notificationDetails, @@ -95,11 +98,11 @@ Future handlePushData(String pushDataB64) async { } } } catch (e) { + Log.error(e); await customLocalPushNotification( 'Du hast eine neue Nachricht.', 'Öffne twonly um mehr zu erfahren.', ); - Log.error(e); } } @@ -161,6 +164,7 @@ Future showLocalPushNotification( priority: Priority.max, ticker: 'You got a new message.', largeIcon: styleInformation, + icon: 'ic_launcher_foreground', ); const darwinNotificationDetails = DarwinNotificationDetails(); @@ -174,7 +178,7 @@ Future showLocalPushNotification( title, body, notificationDetails, - payload: pushNotification.kind.name, + // payload: pushNotification.kind.name, ); } diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 64509a8..b02ab0f 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -51,7 +51,7 @@ Future setupNotificationWithUsers({ final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); - if (pushUser != null) { + if (pushUser != null && pushUser.pushKeys.isNotEmpty) { // make it harder to predict the change of the key final timeBefore = DateTime.now().subtract(Duration(days: 5 + random.nextInt(5))); diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 8917f23..0344dfa 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -9,7 +9,7 @@ void initLogger() { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) async { await _writeLogToFile(record); - if (kDebugMode) { + if (!kReleaseMode) { print( '${record.level.name} [twonly] ${record.loggerName} > ${record.message}', ); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index c41c306..27b151e 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -263,7 +263,7 @@ bool isUUIDNewer(String uuid1, String uuid2) { return timestamp1 > timestamp2; } catch (e) { Log.error(e); - return true; + return false; } } diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index 3d4f944..856224e 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; class CameraZoomButtons extends StatefulWidget { @@ -50,6 +50,7 @@ class _CameraZoomButtonsState extends State { Future initAsync() async { showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; + Log.info('Found ${gCameras.length} cameras for zoom.'); if (!showWideAngleZoom && Platform.isIOS && gCameras.length == 3) { showWideAngleZoomIOS = true; } diff --git a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart index 5b3ae64..d8ef27f 100644 --- a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart +++ b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart @@ -131,7 +131,7 @@ Future> getStickerIndex() async { final indexFile = File('${directory.path}/stickers.json'); var res = []; - if (indexFile.existsSync() && !kDebugMode) { + if (indexFile.existsSync() && kReleaseMode) { final lastModified = indexFile.lastModifiedSync(); final difference = DateTime.now().difference(lastModified); final content = await indexFile.readAsString(); diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index c6d4a27..1a46d10 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -45,10 +47,10 @@ class ReactionRow extends StatelessWidget { child: Center( child: Text( reaction.emoji, - style: const TextStyle(fontSize: 18), - strutStyle: const StrutStyle( + style: TextStyle(fontSize: Platform.isIOS ? 18 : 15), + strutStyle: StrutStyle( forceStrutHeight: true, - height: 1.6, + height: Platform.isIOS ? 1.6 : 1.3, ), ), ), @@ -114,7 +116,7 @@ class ReactionRow extends StatelessWidget { entry.$2.toString(), textAlign: TextAlign.center, style: TextStyle( - fontSize: 15, + fontSize: 13, color: isDarkMode(context) ? Colors.white : Colors.black, decoration: TextDecoration.none, 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 96104b4..1f89085 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 @@ -67,9 +67,10 @@ class _ChatMediaEntryState extends State { if (widget.message.openedAt == null || widget.message.mediaStored) { return; } - if (widget.mediaService.tempPath.existsSync()) { - await sendCipherTextToGroup( - widget.message.groupId, + if (widget.mediaService.tempPath.existsSync() && + widget.message.senderId != null) { + await sendCipherText( + widget.message.senderId!, EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.REOPENED, diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart index 89c336e..0d9f365 100644 --- a/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -77,10 +77,10 @@ class ChatTextEntry extends StatelessWidget { children: [ if (info.expanded) Expanded( - child: BetterText(text: text, textColor: info.textColor), + child: BetterText(text: info.text, textColor: info.textColor), ) else ...[ - BetterText(text: text, textColor: info.textColor), + BetterText(text: info.text, textColor: info.textColor), SizedBox( width: info.spacerWidth, ), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index e147dbc..9f2aefc 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -242,10 +242,10 @@ class _MessageSendStateIconState extends State { child: Center( child: Text( widget.lastReaction!.emoji, - style: const TextStyle(fontSize: 18), + style: const TextStyle(fontSize: 15), strutStyle: const StrutStyle( forceStrutHeight: true, - height: 1.6, + height: 1.4, ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/test b/lib/src/views/chats/chat_messages_components/test deleted file mode 100644 index db5158b..0000000 --- a/lib/src/views/chats/chat_messages_components/test +++ /dev/null @@ -1,242 +0,0 @@ -import 'dart:io'; - -import 'package:audio_waveforms/audio_waveforms.dart'; -import 'package:audio_waveforms_example/chat_bubble.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Audio Waveforms', - debugShowCheckedModeBanner: false, - home: Home(), - ); - } -} - -class Home extends StatefulWidget { - const Home({super.key}); - - @override - State createState() => _HomeState(); -} - -class _HomeState extends State { - late final RecorderController recorderController; - - String? path; - String? musicFile; - bool isRecording = false; - bool isRecordingCompleted = false; - bool isLoading = true; - late Directory appDirectory; - - @override - void initState() { - super.initState(); - _getDir(); - _initialiseControllers(); - } - - void _getDir() async { - appDirectory = await getApplicationDocumentsDirectory(); - path = "${appDirectory.path}/recording.m4a"; - isLoading = false; - setState(() {}); - } - - void _initialiseControllers() { - recorderController = RecorderController() - ..androidEncoder = AndroidEncoder.aac - ..androidOutputFormat = AndroidOutputFormat.mpeg4 - ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC - ..sampleRate = 44100; - } - - void _pickFile() async { - FilePickerResult? result = await FilePicker.platform.pickFiles(); - if (result != null) { - musicFile = result.files.single.path; - setState(() {}); - } else { - debugPrint("File not picked"); - } - } - - @override - void dispose() { - recorderController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFF252331), - appBar: AppBar( - backgroundColor: const Color(0xFF252331), - elevation: 1, - centerTitle: true, - shadowColor: Colors.grey, - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/logo.png', - scale: 1.5, - ), - const SizedBox(width: 10), - const Text( - 'Simform', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - body: isLoading - ? const Center( - child: CircularProgressIndicator(), - ) - : SafeArea( - child: Column( - children: [ - const SizedBox(height: 20), - Expanded( - child: ListView.builder( - itemCount: 4, - itemBuilder: (_, index) { - return WaveBubble( - index: index + 1, - isSender: index.isOdd, - width: MediaQuery.of(context).size.width / 2, - appDirectory: appDirectory, - ); - }, - ), - ), - if (isRecordingCompleted) - WaveBubble( - path: path, - isSender: true, - appDirectory: appDirectory, - ), - if (musicFile != null) - WaveBubble( - path: musicFile, - isSender: true, - appDirectory: appDirectory, - ), - SafeArea( - child: Row( - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: isRecording - ? AudioWaveforms( - enableGesture: true, - size: Size( - MediaQuery.of(context).size.width / 2, - 50), - recorderController: recorderController, - waveStyle: const WaveStyle( - waveColor: Colors.white, - extendWaveform: true, - showMiddleLine: false, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: const Color(0xFF1E1B26), - ), - padding: const EdgeInsets.only(left: 18), - margin: const EdgeInsets.symmetric( - horizontal: 15), - ) - : Container( - width: - MediaQuery.of(context).size.width / 1.7, - height: 50, - decoration: BoxDecoration( - color: const Color(0xFF1E1B26), - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.only(left: 18), - margin: const EdgeInsets.symmetric( - horizontal: 15), - child: TextField( - readOnly: true, - decoration: InputDecoration( - hintText: "Type Something...", - hintStyle: const TextStyle( - color: Colors.white54), - contentPadding: - const EdgeInsets.only(top: 16), - border: InputBorder.none, - suffixIcon: IconButton( - onPressed: _pickFile, - icon: Icon(Icons.adaptive.share), - color: Colors.white54, - ), - ), - ), - ), - ), - IconButton( - onPressed: _refreshWave, - icon: Icon( - isRecording ? Icons.refresh : Icons.send, - color: Colors.white, - ), - ), - const SizedBox(width: 16), - IconButton( - onPressed: _startOrStopRecording, - icon: Icon(isRecording ? Icons.stop : Icons.mic), - color: Colors.white, - iconSize: 28, - ), - ], - ), - ), - ], - ), - ), - ); - } - - void _startOrStopRecording() async { - try { - if (isRecording) { - recorderController.reset(); - - path = await recorderController.stop(false); - - if (path != null) { - isRecordingCompleted = true; - debugPrint(path); - debugPrint("Recorded file size: ${File(path!).lengthSync()}"); - } - } else { - await recorderController.record(path: path); // Path is optional - } - } catch (e) { - debugPrint(e.toString()); - } finally { - if (recorderController.hasPermission) { - setState(() { - isRecording = !isRecording; - }); - } - } - } - - void _refreshWave() { - if (isRecording) recorderController.refresh(); - } -} \ No newline at end of file diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index e81347c..9fe19f9 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -82,6 +82,7 @@ class _MediaViewerViewState extends State { _subscription.cancel(); downloadStateListener?.cancel(); videoController?.dispose(); + videoController = null; super.dispose(); } @@ -141,7 +142,9 @@ class _MediaViewerViewState extends State { Future loadCurrentMediaFile({bool showTwonly = false}) async { if (!mounted || !context.mounted) return; - if (allMediaFiles.isEmpty) return nextMediaOrExit(); + if (allMediaFiles.isEmpty || allMediaFiles.first.mediaId == null) { + return nextMediaOrExit(); + } await _noScreenshot.screenshotOff(); setState(() { @@ -175,7 +178,8 @@ class _MediaViewerViewState extends State { downloadTriggered = true; final mediaFile = await twonlyDB.mediaFilesDao .getMediaFileById(allMediaFiles.first.mediaId!); - await startDownloadMedia(mediaFile!, true); + if (mediaFile == null) return; + await startDownloadMedia(mediaFile, true); unawaited(tryDownloadAllMediaFiles(force: true)); } return; @@ -269,6 +273,10 @@ class _MediaViewerViewState extends State { } }); progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { + if (currentMedia!.mediaFile.displayLimitInMilliseconds == null || + canBeSeenUntil == null) { + return; + } final difference = canBeSeenUntil!.difference(DateTime.now()); // Calculate the progress as a value between 0.0 and 1.0 progress = difference.inMilliseconds / @@ -312,10 +320,12 @@ class _MediaViewerViewState extends State { void displayShortReactions() { final renderBox = - mediaWidgetKey.currentContext!.findRenderObject()! as RenderBox; + mediaWidgetKey.currentContext!.findRenderObject() as RenderBox?; setState(() { showShortReactions = true; - mediaViewerDistanceFromBottom = renderBox.size.height; + if (renderBox != null) { + mediaViewerDistanceFromBottom = renderBox.size.height; + } }); } diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index ee193e7..efe80ba 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -85,7 +85,7 @@ class _MaxFlameListTitleState extends State { if (_directChat == null || _directChat!.maxFlameCounter == 0 || _flameCounter >= (_directChat!.maxFlameCounter + 1) || - _directChat!.lastFlameCounterChange! + _directChat!.maxFlameCounterFrom! .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { return Container(); } diff --git a/lib/src/views/settings/account.view.dart b/lib/src/views/settings/account.view.dart index 37af702..d56e734 100644 --- a/lib/src/views/settings/account.view.dart +++ b/lib/src/views/settings/account.view.dart @@ -37,7 +37,7 @@ class _AccountViewState extends State { .where( (x) => x.transactionType != Response_TransactionTypes.ThanksForTesting || - kDebugMode, + !kReleaseMode, ) .map((a) => a.depositCents.toInt()) .sum; @@ -101,7 +101,7 @@ class _AccountViewState extends State { ), ) : Text(context.lang.settingsAccountDeleteAccountNoBallance), - onLongPress: kDebugMode + onLongPress: !kReleaseMode ? () async { await deleteLocalUserData(); await Restart.restartApp( diff --git a/lib/src/views/settings/backup/twonly_safe_backup.view.dart b/lib/src/views/settings/backup/twonly_safe_backup.view.dart index 64f7ba1..c5255d1 100644 --- a/lib/src/views/settings/backup/twonly_safe_backup.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_backup.view.dart @@ -204,7 +204,7 @@ class _TwonlyIdentityBackupViewState extends State { onPressed: (!isLoading && (passwordCtrl.text == repeatedPasswordCtrl.text && passwordCtrl.text.length >= 8 || - kDebugMode)) + !kReleaseMode)) ? onPressedEnableTwonlySafe : null, icon: isLoading diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart index 89f3774..02a7b9d 100644 --- a/lib/src/views/settings/developer/automated_testing.view.dart +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -32,7 +32,7 @@ class _AutomatedTestingViewState extends State { ), body: ListView( children: [ - if (kDebugMode) + if (!kReleaseMode) ListTile( title: const Text('Sending a lot of messages.'), subtitle: Text(lotsOfMessagesStatus), diff --git a/lib/src/views/settings/developer/developer.view.dart b/lib/src/views/settings/developer/developer.view.dart index efa8812..42e4aa3 100644 --- a/lib/src/views/settings/developer/developer.view.dart +++ b/lib/src/views/settings/developer/developer.view.dart @@ -66,7 +66,7 @@ class _DeveloperSettingsViewState extends State { ); }, ), - // if (kDebugMode) + // if (!kReleaseMode) // ListTile( // title: const Text('FlameSync Test'), // onTap: () async { @@ -74,7 +74,7 @@ class _DeveloperSettingsViewState extends State { // await syncFlameCounters(); // }, // ), - if (kDebugMode) + if (!kReleaseMode) ListTile( title: const Text('Automated Testing'), onTap: () async { diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 02aa643..5d24f45 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -25,11 +25,26 @@ class _ProfileViewState extends State { final AvatarMakerController _avatarMakerController = PersistentAvatarMakerController(customizedPropertyCategories: []); + int twonlyScore = 0; + late StreamSubscription twonlyScoreSub; + @override void initState() { + twonlyScoreSub = + twonlyDB.groupsDao.watchSumTotalMediaCounter().listen((update) { + setState(() { + twonlyScore = update; + }); + }); super.initState(); } + @override + void dispose() { + twonlyScoreSub.cancel(); + super.dispose(); + } + Future updateUserDisplayName(String displayName) async { await updateUserdata((user) { user @@ -156,6 +171,17 @@ class _ProfileViewState extends State { } }, ), + BetterListTile( + text: context.lang.yourTwonlyScore, + icon: FontAwesomeIcons.trophy, + trailing: Text( + twonlyScore.toString(), + style: TextStyle( + color: context.color.primary, + fontSize: 18, + ), + ), + ), ], ), ); diff --git a/lib/src/views/settings/subscription/checkout.view.dart b/lib/src/views/settings/subscription/checkout.view.dart index 83804a5..3d6797c 100644 --- a/lib/src/views/settings/subscription/checkout.view.dart +++ b/lib/src/views/settings/subscription/checkout.view.dart @@ -102,6 +102,7 @@ class _CheckoutViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index 4ae1ae2..f9d3b8f 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -111,6 +111,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.all(16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -193,6 +194,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -215,6 +217,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index 6d1ccbf..d87434a 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -444,6 +444,7 @@ class PlanCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Column( From aebf6de4a5c8385da72ccd726c8658426f96575e Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 7 Nov 2025 17:50:33 +0100 Subject: [PATCH 72/76] remove print --- lib/src/utils/log.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 0344dfa..af28bc6 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -10,6 +10,7 @@ void initLogger() { Logger.root.onRecord.listen((record) async { await _writeLogToFile(record); if (!kReleaseMode) { + // ignore: avoid_print print( '${record.level.name} [twonly] ${record.loggerName} > ${record.message}', ); From 80d6f85350ac568b26201d883d7ef586af809c91 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 12:26:17 +0100 Subject: [PATCH 73/76] draft images, multiple bug fixes --- lib/app.dart | 9 ++- lib/src/database/daos/mediafiles.dao.dart | 21 ++++++- lib/src/database/tables/mediafiles.table.dart | 2 + lib/src/database/twonly.db.g.dart | 60 +++++++++++++++++++ lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 ++ .../generated/app_localizations_de.dart | 4 ++ .../generated/app_localizations_en.dart | 4 ++ lib/src/services/api.service.dart | 15 +++-- .../api/mediafiles/upload.service.dart | 31 +++++++++- .../mediafiles/compression.service.dart | 8 ++- .../mediafiles/mediafile.service.dart | 9 +++ .../background.notifications.dart | 2 +- .../camera_preview_controller_view.dart | 2 + .../views/camera/share_image_editor_view.dart | 13 ++++ lib/src/views/chats/media_viewer.view.dart | 4 +- lib/src/views/components/flame.dart | 6 ++ .../components/max_flame_list_title.dart | 9 +-- lib/src/views/home.view.dart | 18 ++++++ lib/src/views/onboarding/register.view.dart | 60 +++++++++++++++++-- 21 files changed, 261 insertions(+), 28 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 52371c3..74fd9b2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -156,7 +156,7 @@ class _AppMainWidgetState extends State { bool _showOnboarding = true; bool _isLoaded = false; - Future? _proofOfWork; + (Future?, bool) _proofOfWork = (null, false); @override void initState() { @@ -176,11 +176,14 @@ class _AppMainWidgetState extends State { if (!_isUserCreated && !_showDatabaseMigration) { // This means the user is in the onboarding screen, so start with the Proof of Work. - final proof = await apiService.getProofOfWork(); + final (proof, disabled) = await apiService.getProofOfWork(); if (proof != null) { Log.info('Starting with proof of work calculation.'); // Starting with the proof of work. - _proofOfWork = calculatePoW(proof.prefix, proof.difficulty.toInt()); + _proofOfWork = + (calculatePoW(proof.prefix, proof.difficulty.toInt()), false); + } else { + _proofOfWork = (null, disabled); } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 03fae40..b1e09d9 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -50,11 +50,27 @@ class MediaFilesDao extends DatabaseAccessor .write(updates); } + Future updateAllMediaFiles( + MediaFilesCompanion updates, + ) async { + await update(mediaFiles).write(updates); + } + Future getMediaFileById(String mediaId) async { return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) .getSingleOrNull(); } + Future getDraftMediaFile() async { + final medias = await (select(mediaFiles) + ..where((t) => t.isDraftMedia.equals(true))) + .get(); + if (medias.isEmpty) { + return null; + } + return medias.first; + } + Stream watchMedia(String mediaId) { return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) .watchSingleOrNull(); @@ -87,10 +103,9 @@ class MediaFilesDao extends DatabaseAccessor Future> getAllMediaFilesPendingUpload() async { return (select(mediaFiles) ..where( - (t) => - t.uploadState.equals(UploadState.initialized.name) | + (t) => (t.uploadState.equals(UploadState.initialized.name) | t.uploadState.equals(UploadState.uploadLimitReached.name) | - t.uploadState.equals(UploadState.preprocessing.name), + t.uploadState.equals(UploadState.preprocessing.name)), )) .get(); } diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 6651485..af129d8 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -44,10 +44,12 @@ class MediaFiles extends Table { BoolColumn get requiresAuthentication => boolean().withDefault(const Constant(false))(); + BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); BoolColumn get stored => boolean().withDefault(const Constant(false))(); + BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); TextColumn get reuploadRequestedBy => text().map(IntListTypeConverter()).nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index f3e9a93..0e7f60b 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1905,6 +1905,16 @@ class $MediaFilesTable extends MediaFiles defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _isDraftMediaMeta = + const VerificationMeta('isDraftMedia'); + @override + late final GeneratedColumn isDraftMedia = GeneratedColumn( + 'is_draft_media', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_draft_media" IN (0, 1))'), + defaultValue: const Constant(false)); @override late final GeneratedColumnWithTypeConverter?, String> reuploadRequestedBy = GeneratedColumn( @@ -1968,6 +1978,7 @@ class $MediaFilesTable extends MediaFiles requiresAuthentication, reopenByContact, stored, + isDraftMedia, reuploadRequestedBy, displayLimitInMilliseconds, removeAudio, @@ -2009,6 +2020,12 @@ class $MediaFilesTable extends MediaFiles context.handle(_storedMeta, stored.isAcceptableOrUnknown(data['stored']!, _storedMeta)); } + if (data.containsKey('is_draft_media')) { + context.handle( + _isDraftMediaMeta, + isDraftMedia.isAcceptableOrUnknown( + data['is_draft_media']!, _isDraftMediaMeta)); + } if (data.containsKey('display_limit_in_milliseconds')) { context.handle( _displayLimitInMillisecondsMeta, @@ -2076,6 +2093,8 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, stored: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, + isDraftMedia: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!, reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}reupload_requested_by'])), @@ -2129,6 +2148,7 @@ class MediaFile extends DataClass implements Insertable { final bool requiresAuthentication; final bool reopenByContact; final bool stored; + final bool isDraftMedia; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; final bool? removeAudio; @@ -2145,6 +2165,7 @@ class MediaFile extends DataClass implements Insertable { required this.requiresAuthentication, required this.reopenByContact, required this.stored, + required this.isDraftMedia, this.reuploadRequestedBy, this.displayLimitInMilliseconds, this.removeAudio, @@ -2172,6 +2193,7 @@ class MediaFile extends DataClass implements Insertable { map['requires_authentication'] = Variable(requiresAuthentication); map['reopen_by_contact'] = Variable(reopenByContact); map['stored'] = Variable(stored); + map['is_draft_media'] = Variable(isDraftMedia); if (!nullToAbsent || reuploadRequestedBy != null) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -2213,6 +2235,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: Value(requiresAuthentication), reopenByContact: Value(reopenByContact), stored: Value(stored), + isDraftMedia: Value(isDraftMedia), reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent ? const Value.absent() : Value(reuploadRequestedBy), @@ -2254,6 +2277,7 @@ class MediaFile extends DataClass implements Insertable { serializer.fromJson(json['requiresAuthentication']), reopenByContact: serializer.fromJson(json['reopenByContact']), stored: serializer.fromJson(json['stored']), + isDraftMedia: serializer.fromJson(json['isDraftMedia']), reuploadRequestedBy: serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: @@ -2280,6 +2304,7 @@ class MediaFile extends DataClass implements Insertable { 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'reopenByContact': serializer.toJson(reopenByContact), 'stored': serializer.toJson(stored), + 'isDraftMedia': serializer.toJson(isDraftMedia), 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), @@ -2300,6 +2325,7 @@ class MediaFile extends DataClass implements Insertable { bool? requiresAuthentication, bool? reopenByContact, bool? stored, + bool? isDraftMedia, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -2318,6 +2344,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, reuploadRequestedBy: reuploadRequestedBy.present ? reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -2352,6 +2379,9 @@ class MediaFile extends DataClass implements Insertable { ? data.reopenByContact.value : this.reopenByContact, stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, reuploadRequestedBy: data.reuploadRequestedBy.present ? data.reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -2386,6 +2416,7 @@ class MediaFile extends DataClass implements Insertable { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('removeAudio: $removeAudio, ') @@ -2407,6 +2438,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication, reopenByContact, stored, + isDraftMedia, reuploadRequestedBy, displayLimitInMilliseconds, removeAudio, @@ -2426,6 +2458,7 @@ class MediaFile extends DataClass implements Insertable { other.requiresAuthentication == this.requiresAuthentication && other.reopenByContact == this.reopenByContact && other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && other.removeAudio == this.removeAudio && @@ -2445,6 +2478,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value requiresAuthentication; final Value reopenByContact; final Value stored; + final Value isDraftMedia; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; final Value removeAudio; @@ -2462,6 +2496,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.removeAudio = const Value.absent(), @@ -2480,6 +2515,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.removeAudio = const Value.absent(), @@ -2499,6 +2535,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? requiresAuthentication, Expression? reopenByContact, Expression? stored, + Expression? isDraftMedia, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, Expression? removeAudio, @@ -2518,6 +2555,7 @@ class MediaFilesCompanion extends UpdateCompanion { 'requires_authentication': requiresAuthentication, if (reopenByContact != null) 'reopen_by_contact': reopenByContact, if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, if (reuploadRequestedBy != null) 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) @@ -2540,6 +2578,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? requiresAuthentication, Value? reopenByContact, Value? stored, + Value? isDraftMedia, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, Value? removeAudio, @@ -2558,6 +2597,7 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, @@ -2599,6 +2639,9 @@ class MediaFilesCompanion extends UpdateCompanion { if (stored.present) { map['stored'] = Variable(stored.value); } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable(isDraftMedia.value); + } if (reuploadRequestedBy.present) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -2642,6 +2685,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('removeAudio: $removeAudio, ') @@ -9161,6 +9205,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value requiresAuthentication, Value reopenByContact, Value stored, + Value isDraftMedia, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value removeAudio, @@ -9179,6 +9224,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value requiresAuthentication, Value reopenByContact, Value stored, + Value isDraftMedia, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value removeAudio, @@ -9248,6 +9294,9 @@ class $$MediaFilesTableFilterComposer ColumnFilters get stored => $composableBuilder( column: $table.stored, builder: (column) => ColumnFilters(column)); + ColumnFilters get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters?, List, String> get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, @@ -9331,6 +9380,10 @@ class $$MediaFilesTableOrderingComposer ColumnOrderings get stored => $composableBuilder( column: $table.stored, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, builder: (column) => ColumnOrderings(column)); @@ -9394,6 +9447,9 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get stored => $composableBuilder(column: $table.stored, builder: (column) => column); + GeneratedColumn get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, builder: (column) => column); @@ -9471,6 +9527,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -9489,6 +9546,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, stored: stored, + isDraftMedia: isDraftMedia, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, removeAudio: removeAudio, @@ -9507,6 +9565,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -9525,6 +9584,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, stored: stored, + isDraftMedia: isDraftMedia, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, removeAudio: removeAudio, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 85299bc..5e7bba5 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -817,5 +817,6 @@ "deleteChatAfterAWeek": "einer Woche.", "deleteChatAfterAMonth": "einem Monat.", "deleteChatAfterAYear": "einem Jahr.", - "yourTwonlyScore": "Dein twonly-Score" + "yourTwonlyScore": "Dein twonly-Score", + "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 4078005..cb78dfd 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -595,5 +595,6 @@ "deleteChatAfterAWeek": "one week.", "deleteChatAfterAMonth": "one month.", "deleteChatAfterAYear": "one year.", - "yourTwonlyScore": "Your twonly-Score" + "yourTwonlyScore": "Your twonly-Score", + "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index db8ac2b..feedd96 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2677,6 +2677,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Your twonly-Score'** String get yourTwonlyScore; + + /// No description provided for @registrationClosed. + /// + /// In en, this message translates to: + /// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'** + String get registrationClosed; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index ebe795b..fe61244 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1477,4 +1477,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get yourTwonlyScore => 'Dein twonly-Score'; + + @override + String get registrationClosed => + 'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 607bc3f..790bd19 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1467,4 +1467,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get yourTwonlyScore => 'Your twonly-Score'; + + @override + String get registrationClosed => + 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 4cc3e23..86651df 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -51,8 +51,9 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; - final String apiSecure = kReleaseMode ? 's' : ''; + // final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; + final String apiSecure = kReleaseMode ? 's' : 's'; bool appIsOutdated = false; bool isAuthenticated = false; @@ -508,15 +509,19 @@ class ApiService { return null; } - Future getProofOfWork() async { + Future<(Response_ProofOfWork?, bool)> getProofOfWork() async { final handshake = Handshake()..requestPOW = Handshake_RequestPOW(); final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); if (result.isError) { Log.error('could not request proof of work params', result); - return null; + if (result.error == ErrorCode.RegistrationDisabled) { + return (null, true); + } + Log.error('could not request proof of work params', result); + return (null, false); } - return result.value.proofOfWork as Response_ProofOfWork; + return (result.value.proofOfWork as Response_ProofOfWork, false); } Future downloadDone(List token) async { diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index d4a6783..7fa39d9 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -24,8 +24,24 @@ Future finishStartedPreprocessing() async { await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); for (final mediaFile in mediaFiles) { + if (mediaFile.isDraftMedia) { + continue; + } try { final service = await MediaFileService.fromMedia(mediaFile); + if (!service.originalPath.existsSync() && + !service.uploadRequestPath.existsSync()) { + if (service.storedPath.existsSync()) { + // media files was just stored.. + continue; + } + Log.info( + 'Deleted media files, as originalPath and uploadRequestPath both do not exists', + ); + // the file does not exists anymore. + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + continue; + } await startBackgroundMediaUpload(service); } catch (e) { Log.error(e); @@ -35,18 +51,24 @@ Future finishStartedPreprocessing() async { Future initializeMediaUpload( MediaType type, - int? displayLimitInMilliseconds, -) async { + int? displayLimitInMilliseconds, { + bool isDraftMedia = false, +}) async { final chacha20 = FlutterChacha20.poly1305Aead(); final encryptionKey = await (await chacha20.newSecretKey()).extract(); final encryptionNonce = chacha20.newNonce(); + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion(isDraftMedia: Value(false)), + ); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( MediaFilesCompanion( uploadState: const Value(UploadState.initialized), displayLimitInMilliseconds: Value(displayLimitInMilliseconds), encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)), encryptionNonce: Value(Uint8List.fromList(encryptionNonce)), + isDraftMedia: Value(isDraftMedia), type: Value(type), ), ); @@ -58,6 +80,11 @@ Future insertMediaFileInMessagesTable( MediaFileService mediaService, List groupIds, ) async { + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); for (final groupId in groupIds) { final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index b09f33e..e1da17c 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -65,20 +65,24 @@ Future compressAndOverlayVideo(MediaFileService media) async { if (media.tempPath.existsSync()) { media.tempPath.deleteSync(); } + if (media.ffmpegOutputPath.existsSync()) { + media.ffmpegOutputPath.deleteSync(); + } final stopwatch = Stopwatch()..start(); var command = - '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.tempPath.path}"'; + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"'; if (media.removeAudio) { command = - '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.tempPath.path}"'; + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.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', diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 27e2ea1..00bed5a 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -45,11 +45,16 @@ class MediaFileService { var delete = true; final service = await MediaFileService.fromMediaId(mediaId); + if (service == null) { Log.error( 'Purging media file, as it is not in the database $mediaId.', ); } else { + if (service.mediaFile.isDraftMedia) { + delete = false; + } + final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); @@ -302,6 +307,10 @@ class MediaFileService { 'tmp', namePrefix: '.original', ); + File get ffmpegOutputPath => _buildFilePath( + 'tmp', + namePrefix: '.ffmpeg', + ); File get overlayImagePath => _buildFilePath( 'tmp', namePrefix: '.overlay', diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 905c3ed..2d6613f 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -253,7 +253,7 @@ String getPushNotificationText(PushNotification pushNotification) { PushKind.twonly.name: lang.notificationTwonly(inGroup), PushKind.video.name: lang.notificationVideo(inGroup), PushKind.image.name: lang.notificationImage(inGroup), - PushKind.video.name: lang.notificationAudio(inGroup), + PushKind.audio.name: lang.notificationAudio(inGroup), PushKind.contactRequest.name: lang.notificationContactRequest, PushKind.acceptRequest.name: lang.notificationAcceptRequest, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 5a4a93c..bab3b3b 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; @@ -352,6 +353,7 @@ class _CameraPreviewViewState extends State { final mediaFileService = await initializeMediaUpload( type, gUser.defaultShowTime, + isDraftMedia: true, ); if (!mounted) return true; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index bfa5c3b..3db605c 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:collection'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -82,6 +84,8 @@ class _ShareImageEditorView extends State { } else { if (widget.mediaFileService.tempPath.existsSync()) { loadImage(widget.mediaFileService.tempPath.readAsBytes()); + } else if (widget.mediaFileService.originalPath.existsSync()) { + loadImage(widget.mediaFileService.originalPath.readAsBytes()); } } } @@ -106,6 +110,11 @@ class _ShareImageEditorView extends State { isDisposed = true; layers.clear(); videoController?.dispose(); + twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); super.dispose(); } @@ -388,6 +397,10 @@ class _ShareImageEditorView extends State { Future loadImage(Future imageBytesFuture) async { imageBytes = await imageBytesFuture; + + // store this image so it can be used as a draft in case the app is restarted + mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + await currentImage.load(imageBytes); if (isDisposed) return; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 9fe19f9..471cc42 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -476,8 +476,8 @@ class _MediaViewerViewState extends State { if (videoController != null) Positioned.fill( child: VideoPlayer(videoController!), - ), - if (currentMedia != null && + ) + else if (currentMedia != null && currentMedia!.mediaFile.type == MediaType.image || currentMedia!.mediaFile.type == MediaType.gif) Positioned.fill( diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 122d014..dc83b12 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -41,6 +41,12 @@ class _FlameCounterWidgetState extends State { if (widget.groupId == null && widget.contactId != null) { final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!); groupId = group?.groupId; + } else if (groupId != null) { + // do not display the flame counter for groups + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (!(group?.isDirectChat ?? false)) { + return; + } } if (groupId != null) { isBestFriend = gUser.myBestFriendGroupId == groupId; diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index efe80ba..ba490e8 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -36,7 +36,8 @@ class _MaxFlameListTitleState extends State { _flameCounterSub = stream.listen((counter) { if (mounted) { setState(() { - _flameCounter = counter; + _flameCounter = counter - + 1; // in the watchFlameCounter a one is added, so remove this here }); } }); @@ -73,7 +74,7 @@ class _MaxFlameListTitleState extends State { await twonlyDB.groupsDao.updateGroup( _groupId, GroupsCompanion( - flameCounter: Value(_directChat!.maxFlameCounter - 1), + flameCounter: Value(_directChat!.maxFlameCounter), lastFlameCounterChange: Value(DateTime.now()), ), ); @@ -84,7 +85,7 @@ class _MaxFlameListTitleState extends State { Widget build(BuildContext context) { if (_directChat == null || _directChat!.maxFlameCounter == 0 || - _flameCounter >= (_directChat!.maxFlameCounter + 1) || + _flameCounter >= _directChat!.maxFlameCounter || _directChat!.maxFlameCounterFrom! .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { return Container(); @@ -97,7 +98,7 @@ class _MaxFlameListTitleState extends State { emoji: '🔥', ), ), - text: 'Restore your ${_directChat!.maxFlameCounter} lost flames', + text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames', ); } } diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index ee2907d..53ae88b 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; @@ -145,6 +148,21 @@ class HomeViewState extends State { globalUpdateOfHomeViewPageIndex(0); } } + + final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); + if (draftMedia != null) { + final service = await MediaFileService.fromMedia(draftMedia); + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageEditorView( + mediaFileService: service, + sharedFromGallery: true, + ), + ), + ); + } } @override diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index d06df15..67b6f1c 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -27,7 +27,7 @@ class RegisterView extends StatefulWidget { }); final Function callbackOnSuccess; - final Future? proofOfWork; + final (Future?, bool) proofOfWork; @override State createState() => _RegisterViewState(); } @@ -36,10 +36,20 @@ class _RegisterViewState extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController inviteCodeController = TextEditingController(); + bool _registrationDisabled = false; bool _isTryingToRegister = false; bool _isValidUserName = false; bool _showUserNameError = false; + late Future? proofOfWork; + + @override + void initState() { + proofOfWork = widget.proofOfWork.$1; + _registrationDisabled = widget.proofOfWork.$2; + super.initState(); + } + Future createNewUser() async { if (!_isValidUserName) { setState(() { @@ -57,11 +67,12 @@ class _RegisterViewState extends State { late int proof; - if (widget.proofOfWork != null) { - proof = await widget.proofOfWork!; + if (proofOfWork != null) { + proof = await proofOfWork!; } else { - final pow = await apiService.getProofOfWork(); + final (pow, registrationDisabled) = await apiService.getProofOfWork(); if (pow == null) { + _registrationDisabled = registrationDisabled; if (mounted) { showNetworkIssue(context); } @@ -82,6 +93,10 @@ class _RegisterViewState extends State { Log.info('Got user_id ${res.value} from server'); userId = res.value.userid.toInt() as int; } else { + if (res.error == ErrorCode.RegistrationDisabled) { + _registrationDisabled = true; + return; + } if (res.error == ErrorCode.UserIdAlreadyTaken) { Log.error('User ID already token. Tying again.'); await deleteLocalUserData(); @@ -127,6 +142,43 @@ class _RegisterViewState extends State { @override Widget build(BuildContext context) { + if (_registrationDisabled) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView( + children: [ + const SizedBox(height: 50), + Text( + context.lang.registerTitle, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 30), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Text( + context.lang.registerSlogan, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(height: 130), + Text( + context.lang.registrationClosed, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.red, + ), + ), + ], + ), + ), + ), + ); + } + InputDecoration getInputDecoration(String hintText) { return InputDecoration(hintText: hintText, fillColor: Colors.grey[400]); } From d59b8602f9eeec1f4b9f353e497fc14d87119e46 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 13:36:54 +0100 Subject: [PATCH 74/76] fixed some issues with the migration --- .../camera_preview_controller_view.dart | 1 - .../chat_list_components/group_list_item.dart | 11 +- .../updates/62_database_migration.view.dart | 220 ++++++++---------- 3 files changed, 103 insertions(+), 129 deletions(-) diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index bab3b3b..3c7019a 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; -import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 6f1939a..e70ebf8 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -218,7 +218,16 @@ class _UserListItem extends State { subtitle: (_currentMessage == null) ? (widget.group.totalMediaCounter == 0) ? Text(context.lang.chatsTapToSend) - : LastMessageTime(dateTime: widget.group.lastMessageExchange) + : Row( + children: [ + LastMessageTime( + dateTime: widget.group.lastMessageExchange), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ) : Row( children: [ MessageSendStateIcon( diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index b04a407..b2d0bff 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -1,5 +1,7 @@ +import 'dart:collection' show HashSet; import 'dart:convert'; import 'dart:io'; +import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; @@ -8,13 +10,11 @@ import 'package:restart_app/restart_app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; 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/database/twonly_database_old.dart' show TwonlyDatabaseOld; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; class DatabaseMigrationView extends StatefulWidget { @@ -37,141 +37,107 @@ class _DatabaseMigrationViewState extends State { final oldDatabase = TwonlyDatabaseOld(); final oldContacts = await oldDatabase.contacts.select().get(); - final oldMessages = await oldDatabase.messages.select().get(); for (final oldContact in oldContacts) { - if (oldContact.deleted) continue; - Uint8List? avatarSvg; - if (oldContact.avatarSvg != null) { - avatarSvg = - Uint8List.fromList(gzip.encode(utf8.encode(oldContact.avatarSvg!))); - } - await twonlyDB.contactsDao.insertContact( - ContactsCompanion( - userId: Value(oldContact.userId), - username: Value(oldContact.username), - displayName: Value(oldContact.displayName), - nickName: Value(oldContact.nickName), - avatarSvgCompressed: Value(avatarSvg), - senderProfileCounter: const Value(0), - accepted: Value(oldContact.accepted), - requested: Value(oldContact.requested), - blocked: Value(oldContact.blocked), - verified: Value(oldContact.verified), - createdAt: Value(oldContact.createdAt), - ), - ); - setState(() { - _contactsMigrated += 1; - }); - final group = await twonlyDB.groupsDao.createNewDirectChat( - oldContact.userId, - GroupsCompanion( - pinned: Value(oldContact.pinned), - archived: Value(oldContact.archived), - groupName: Value(getContactDisplayNameOld(oldContact)), - totalMediaCounter: Value(oldContact.totalMediaCounter), - alsoBestFriend: Value(oldContact.alsoBestFriend), - createdAt: Value(oldContact.createdAt), - lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), - lastFlameSync: Value(oldContact.lastFlameSync), - lastMessageExchange: Value(oldContact.lastMessageExchange), - lastMessageReceived: Value(oldContact.lastMessageReceived), - lastMessageSend: Value(oldContact.lastMessageSend), - flameCounter: Value(oldContact.flameCounter), - ), - ); - if (group == null) continue; - for (final oldMessage in oldMessages) { - if (oldMessage.mediaUploadId == null && - oldMessage.mediaDownloadId == null) { - /// only interested in media files... - continue; + try { + if (oldContact.deleted) continue; + Uint8List? avatarSvg; + if (oldContact.avatarSvg != null) { + avatarSvg = Uint8List.fromList( + gzip.encode(utf8.encode(oldContact.avatarSvg!))); } - if (oldMessage.contactId != oldContact.userId) continue; - if (!oldMessage.mediaStored) continue; - - var storedMediaPath = - join((await getApplicationSupportDirectory()).path, 'media'); - if (oldMessage.mediaDownloadId != null) { - storedMediaPath = - '${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}'; - } else { - storedMediaPath = - '${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}'; - } - - var type = MediaType.image; - if (File('$storedMediaPath.mp4').existsSync()) { - type = MediaType.video; - storedMediaPath = '$storedMediaPath.mp4'; - } else if (File('$storedMediaPath.png').existsSync()) { - type = MediaType.image; - storedMediaPath = '$storedMediaPath.png'; - } else if (File('$storedMediaPath.webp').existsSync()) { - type = MediaType.image; - storedMediaPath = '$storedMediaPath.webp'; - } else { - continue; - } - - final uniqueId = Value( - getUUIDforDirectChat( - oldMessage.messageOtherId ?? oldMessage.messageId, - oldMessage.contactId ^ gUser.userId, + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + userId: Value(oldContact.userId), + username: Value(oldContact.username), + displayName: Value(oldContact.displayName), + nickName: Value(oldContact.nickName), + avatarSvgCompressed: Value(avatarSvg), + senderProfileCounter: const Value(0), + accepted: Value(oldContact.accepted), + requested: Value(oldContact.requested), + blocked: Value(oldContact.blocked), + verified: Value(oldContact.verified), + createdAt: Value(oldContact.createdAt), ), ); - - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - mediaId: uniqueId, - stored: const Value(true), - type: Value(type), - createdAt: Value(oldMessage.sendAt), - ), - ); - if (mediaFile == null) continue; - - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - messageId: uniqueId, - groupId: Value(group.groupId), - mediaId: uniqueId, - type: const Value(MessageType.media), - ), - ); - if (message == null) continue; - - final mediaService = await MediaFileService.fromMedia(mediaFile); - File(storedMediaPath).copySync(mediaService.storedPath.path); setState(() { - _storedMediaFiles += 1; + _contactsMigrated += 1; }); + await twonlyDB.groupsDao.createNewDirectChat( + oldContact.userId, + GroupsCompanion( + pinned: Value(oldContact.pinned), + archived: Value(oldContact.archived), + groupName: Value(getContactDisplayNameOld(oldContact)), + totalMediaCounter: Value(oldContact.totalMediaCounter), + alsoBestFriend: Value(oldContact.alsoBestFriend), + createdAt: Value(oldContact.createdAt), + lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), + lastFlameSync: Value(oldContact.lastFlameSync), + lastMessageExchange: Value(oldContact.lastMessageExchange), + lastMessageReceived: Value(oldContact.lastMessageReceived), + lastMessageSend: Value(oldContact.lastMessageSend), + flameCounter: Value(oldContact.flameCounter), + maxFlameCounter: Value(oldContact.flameCounter), + maxFlameCounterFrom: Value(DateTime.now()), + ), + ); + } catch (e) { + Log.error(e); } } - final memoriesPath = Directory( - join((await getApplicationSupportDirectory()).path, 'media', 'memories'), - ); - if (memoriesPath.existsSync()) { - final files = memoriesPath.listSync(); - for (final file in files) { - if (file.path.contains('thumbnail')) continue; - final type = - file.path.contains('mp4') ? MediaType.video : MediaType.image; - final stat = FileStat.statSync(file.path); - final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( - MediaFilesCompanion( - type: Value(type), - createdAt: Value(stat.modified), - stored: const Value(true), - ), - ); - final mediaService = await MediaFileService.fromMedia(mediaFile!); - File(file.path).copySync(mediaService.storedPath.path); - setState(() { - _storedMediaFiles += 1; - }); + final folders = ['memories', 'send', 'received']; + + final alreadyCopied = HashSet(); + + for (final folder in folders) { + final memoriesPath = Directory( + join( + (await getApplicationSupportDirectory()).path, + 'media', + folder, + ), + ); + if (memoriesPath.existsSync()) { + final files = memoriesPath.listSync(); + for (final file in files) { + try { + if (file.path.contains('thumbnail')) continue; + late MediaType type; + if (file.path.contains('mp4')) { + type = MediaType.video; + } else if (file.path.contains('png')) { + type = MediaType.image; + } else { + continue; + } + + final bytes = File(file.path).readAsBytesSync(); + final digest = (await Sha256().hash(bytes)).bytes; + if (alreadyCopied.contains(digest)) { + continue; + } + alreadyCopied.add(digest); + + final stat = FileStat.statSync(file.path); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + type: Value(type), + createdAt: Value(stat.modified), + stored: const Value(true), + ), + ); + final mediaService = await MediaFileService.fromMedia(mediaFile!); + File(file.path).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); + } catch (e) { + Log.error(e); + } + } } } From 0a1732523b26a90bce0b10e09e0186807f111394 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 14:30:20 +0100 Subject: [PATCH 75/76] do not load data from server until appVersion is valid --- lib/main.dart | 2 +- lib/src/services/api.service.dart | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 6b80c64..607db29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,7 +41,7 @@ void main() async { await settingsController.loadSettings(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // unawaited(setupPushNotification()); + unawaited(setupPushNotification()); gCameras = await availableCameras(); diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 86651df..57b3ed9 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -367,6 +367,12 @@ class ApiService { final user = await getUser(); if (apiAuthToken != null && user != null) { + if (user.appVersion < 62) { + Log.error( + 'DID NOT authenticate the user, as he still has the old version!', + ); + return false; + } final authenticate = Handshake_Authenticate() ..userId = Int64(userId) ..appVersion = (await PackageInfo.fromPlatform()).version From cc2578781e46cf8670cc9ac94fdc000565fba155 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 15:06:47 +0100 Subject: [PATCH 76/76] bump version --- CHANGELOG.md | 14 +++++++------- pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9e6a8..6827338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,21 +2,21 @@ ## 0.0.62 -- Support for groups -- Edit & Delete messages -- Switched to FFmpeg for improved video compression +- Support for groups with multiple administrators +- Edit and delete messages - Create images using volume buttons -- Video max. length increased to 60 seconds - New and improved emoji picker - Removing audio after recording is possible - Edited image is now embedded into the video -- New context menu and other UI enhancements +- Video max length increased to 60 seconds +- Switched to FFmpeg for improved video compression +- New context menu and other UI enhancements - Client-to-client protocol migrated to Protocol Buffers (Protobuf) -- Database identifiers converted to UUIDs -- Completely redesigned database schema +- Database identifiers converted to UUIDs and the database schema completely redesigned - Improved reliability of client-to-client messaging - Multiple bug fixes + ## 0.0.61 - Improving image editor when changing colors diff --git a/pubspec.yaml b/pubspec.yaml index 0489c4b..245c4de 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.61+61 +version: 0.0.62+62 environment: sdk: ^3.6.0