From 835ee9ee2d3a0e76f9a2b697ddd39fe3988ce0e8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 13 Feb 2026 02:16:04 +0100 Subject: [PATCH] feature: share contacts --- lib/src/database/daos/contacts.dao.g.dart | 8 + lib/src/database/daos/groups.dao.g.dart | 15 + lib/src/database/daos/mediafiles.dao.g.dart | 8 + lib/src/database/daos/messages.dao.dart | 10 +- lib/src/database/daos/messages.dao.g.dart | 24 ++ lib/src/database/daos/reactions.dao.g.dart | 16 ++ lib/src/database/daos/receipts.dao.g.dart | 22 ++ lib/src/database/daos/signal.dao.g.dart | 15 + lib/src/database/tables/messages.table.dart | 4 +- lib/src/database/twonly.db.g.dart | 60 ++-- .../generated/app_localizations.dart | 24 ++ .../generated/app_localizations_de.dart | 13 + .../generated/app_localizations_en.dart | 13 + .../generated/app_localizations_sv.dart | 13 + lib/src/localization/translations | 2 +- lib/src/model/protobuf/client/data.proto | 7 + .../protobuf/client/generated/data.pb.dart | 89 ++++++ .../client/generated/data.pbenum.dart | 5 +- .../client/generated/data.pbjson.dart | 36 ++- .../client/generated/messages.pb.dart | 120 ++++++++ .../client/generated/messages.pbjson.dart | 160 +++++++---- lib/src/model/protobuf/client/messages.proto | 9 +- .../client2client/additional_data.c2c.dart | 36 +++ .../services/api/client2client/media.c2c.dart | 2 +- .../api/client2client/text_message.c2c.dart | 2 +- .../api/mediafiles/download.service.dart | 1 - .../api/mediafiles/upload.service.dart | 2 +- lib/src/services/api/messages.dart | 60 +++- lib/src/services/api/server_messages.dart | 10 + lib/src/services/api/utils.dart | 22 ++ .../notifications/pushkeys.notifications.dart | 5 + lib/src/utils/misc.dart | 12 +- lib/src/utils/qr.dart | 23 +- lib/src/views/chats/add_new_user.view.dart | 23 +- .../chat_list_components/group_list_item.dart | 12 +- lib/src/views/chats/chat_messages.view.dart | 4 +- .../share_additional.bottom_sheet.dart | 112 ++++++++ .../chat_list_entry.dart | 74 +++-- .../entries/chat_contacts.entry.dart | 201 +++++++++++++ .../entries/chat_media_entry.dart | 2 +- .../entries/chat_unkown.entry.dart | 29 ++ .../entries/common.dart | 4 +- .../message_context_menu.dart | 2 +- .../message_input.dart | 41 ++- .../message_send_state_icon.dart | 6 +- .../response_container.dart | 4 +- .../additional_message_content.dart | 1 + lib/src/views/contact/contact.view.dart | 12 +- .../views/shared/select_contacts.view.dart | 271 ++++++++++++++++++ 49 files changed, 1432 insertions(+), 214 deletions(-) create mode 100644 lib/src/services/api/client2client/additional_data.c2c.dart create mode 100644 lib/src/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart create mode 100644 lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart create mode 100644 lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart create mode 100644 lib/src/views/shared/select_contacts.view.dart diff --git a/lib/src/database/daos/contacts.dao.g.dart b/lib/src/database/daos/contacts.dao.g.dart index 626cccb..f76b607 100644 --- a/lib/src/database/daos/contacts.dao.g.dart +++ b/lib/src/database/daos/contacts.dao.g.dart @@ -5,4 +5,12 @@ part of 'contacts.dao.dart'; // ignore_for_file: type=lint mixin _$ContactsDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; + ContactsDaoManager get managers => ContactsDaoManager(this); +} + +class ContactsDaoManager { + final _$ContactsDaoMixin _db; + ContactsDaoManager(this._db); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); } diff --git a/lib/src/database/daos/groups.dao.g.dart b/lib/src/database/daos/groups.dao.g.dart index 4f873ec..2b5ae1b 100644 --- a/lib/src/database/daos/groups.dao.g.dart +++ b/lib/src/database/daos/groups.dao.g.dart @@ -8,4 +8,19 @@ mixin _$GroupsDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; $GroupHistoriesTable get groupHistories => attachedDatabase.groupHistories; + GroupsDaoManager get managers => GroupsDaoManager(this); +} + +class GroupsDaoManager { + final _$GroupsDaoMixin _db; + GroupsDaoManager(this._db); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$GroupMembersTableTableManager get groupMembers => + $$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers); + $$GroupHistoriesTableTableManager get groupHistories => + $$GroupHistoriesTableTableManager( + _db.attachedDatabase, _db.groupHistories); } diff --git a/lib/src/database/daos/mediafiles.dao.g.dart b/lib/src/database/daos/mediafiles.dao.g.dart index 0157e2d..3bc0ce8 100644 --- a/lib/src/database/daos/mediafiles.dao.g.dart +++ b/lib/src/database/daos/mediafiles.dao.g.dart @@ -5,4 +5,12 @@ part of 'mediafiles.dao.dart'; // ignore_for_file: type=lint mixin _$MediaFilesDaoMixin on DatabaseAccessor { $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + MediaFilesDaoManager get managers => MediaFilesDaoManager(this); +} + +class MediaFilesDaoManager { + final _$MediaFilesDaoMixin _db; + MediaFilesDaoManager(this._db); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 72a8d59..1e41efc 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -75,10 +75,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { (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()))), + (t.type.equals(MessageType.text.name).not() | + t.type.equals(MessageType.media.name).not()) | + (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(); diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart index feb9283..3fb2892 100644 --- a/lib/src/database/daos/messages.dao.g.dart +++ b/lib/src/database/daos/messages.dao.g.dart @@ -13,4 +13,28 @@ mixin _$MessagesDaoMixin on DatabaseAccessor { attachedDatabase.messageHistories; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; $MessageActionsTable get messageActions => attachedDatabase.messageActions; + MessagesDaoManager get managers => MessagesDaoManager(this); +} + +class MessagesDaoManager { + final _$MessagesDaoMixin _db; + MessagesDaoManager(this._db); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db.attachedDatabase, _db.messages); + $$ReactionsTableTableManager get reactions => + $$ReactionsTableTableManager(_db.attachedDatabase, _db.reactions); + $$MessageHistoriesTableTableManager get messageHistories => + $$MessageHistoriesTableTableManager( + _db.attachedDatabase, _db.messageHistories); + $$GroupMembersTableTableManager get groupMembers => + $$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers); + $$MessageActionsTableTableManager get messageActions => + $$MessageActionsTableTableManager( + _db.attachedDatabase, _db.messageActions); } diff --git a/lib/src/database/daos/reactions.dao.g.dart b/lib/src/database/daos/reactions.dao.g.dart index 26ac0da..a87f9a8 100644 --- a/lib/src/database/daos/reactions.dao.g.dart +++ b/lib/src/database/daos/reactions.dao.g.dart @@ -9,4 +9,20 @@ mixin _$ReactionsDaoMixin on DatabaseAccessor { $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MessagesTable get messages => attachedDatabase.messages; $ReactionsTable get reactions => attachedDatabase.reactions; + ReactionsDaoManager get managers => ReactionsDaoManager(this); +} + +class ReactionsDaoManager { + final _$ReactionsDaoMixin _db; + ReactionsDaoManager(this._db); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db.attachedDatabase, _db.messages); + $$ReactionsTableTableManager get reactions => + $$ReactionsTableTableManager(_db.attachedDatabase, _db.reactions); } diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart index 4230aa8..9cada53 100644 --- a/lib/src/database/daos/receipts.dao.g.dart +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -12,4 +12,26 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor { $MessageActionsTable get messageActions => attachedDatabase.messageActions; $ReceivedReceiptsTable get receivedReceipts => attachedDatabase.receivedReceipts; + ReceiptsDaoManager get managers => ReceiptsDaoManager(this); +} + +class ReceiptsDaoManager { + final _$ReceiptsDaoMixin _db; + ReceiptsDaoManager(this._db); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db.attachedDatabase, _db.messages); + $$ReceiptsTableTableManager get receipts => + $$ReceiptsTableTableManager(_db.attachedDatabase, _db.receipts); + $$MessageActionsTableTableManager get messageActions => + $$MessageActionsTableTableManager( + _db.attachedDatabase, _db.messageActions); + $$ReceivedReceiptsTableTableManager get receivedReceipts => + $$ReceivedReceiptsTableTableManager( + _db.attachedDatabase, _db.receivedReceipts); } diff --git a/lib/src/database/daos/signal.dao.g.dart b/lib/src/database/daos/signal.dao.g.dart index a9eea13..68c90c1 100644 --- a/lib/src/database/daos/signal.dao.g.dart +++ b/lib/src/database/daos/signal.dao.g.dart @@ -9,4 +9,19 @@ mixin _$SignalDaoMixin on DatabaseAccessor { attachedDatabase.signalContactPreKeys; $SignalContactSignedPreKeysTable get signalContactSignedPreKeys => attachedDatabase.signalContactSignedPreKeys; + SignalDaoManager get managers => SignalDaoManager(this); +} + +class SignalDaoManager { + final _$SignalDaoMixin _db; + SignalDaoManager(this._db); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$SignalContactPreKeysTableTableManager get signalContactPreKeys => + $$SignalContactPreKeysTableTableManager( + _db.attachedDatabase, _db.signalContactPreKeys); + $$SignalContactSignedPreKeysTableTableManager + get signalContactSignedPreKeys => + $$SignalContactSignedPreKeysTableTableManager( + _db.attachedDatabase, _db.signalContactSignedPreKeys); } diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 89e2975..b5bd471 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -3,7 +3,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'; -enum MessageType { media, text } +enum MessageType { media, text, contacts } @DataClassName('Message') class Messages extends Table { @@ -15,7 +15,7 @@ class Messages extends Table { IntColumn get senderId => integer().nullable().references(Contacts, #userId)(); - TextColumn get type => textEnum()(); + TextColumn get type => text()(); TextColumn get content => text().nullable()(); TextColumn get mediaId => text() diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index bad66a1..6502008 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -2776,11 +2776,11 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override - late final GeneratedColumnWithTypeConverter type = - GeneratedColumn('type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter($MessagesTable.$convertertype); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _contentMeta = const VerificationMeta('content'); @override @@ -2929,6 +2929,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_senderIdMeta, senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta)); } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } if (data.containsKey('content')) { context.handle(_contentMeta, content.isAcceptableOrUnknown(data['content']!, _contentMeta)); @@ -3020,8 +3026,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'])!), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, content: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}content']), mediaId: attachedDatabase.typeMapping @@ -3057,16 +3063,13 @@ 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 type; final String? content; final String? mediaId; final Uint8List? additionalMessageData; @@ -3108,9 +3111,7 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || senderId != null) { map['sender_id'] = Variable(senderId); } - { - map['type'] = Variable($MessagesTable.$convertertype.toSql(type)); - } + map['type'] = Variable(type); if (!nullToAbsent || content != null) { map['content'] = Variable(content); } @@ -3201,8 +3202,7 @@ 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'])), + type: serializer.fromJson(json['type']), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), additionalMessageData: @@ -3228,8 +3228,7 @@ 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)), + 'type': serializer.toJson(type), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), 'additionalMessageData': @@ -3252,7 +3251,7 @@ class Message extends DataClass implements Insertable { {String? groupId, String? messageId, Value senderId = const Value.absent(), - MessageType? type, + String? type, Value content = const Value.absent(), Value mediaId = const Value.absent(), Value additionalMessageData = const Value.absent(), @@ -3403,7 +3402,7 @@ class MessagesCompanion extends UpdateCompanion { final Value groupId; final Value messageId; final Value senderId; - final Value type; + final Value type; final Value content; final Value mediaId; final Value additionalMessageData; @@ -3444,7 +3443,7 @@ class MessagesCompanion extends UpdateCompanion { required String groupId, required String messageId, this.senderId = const Value.absent(), - required MessageType type, + required String type, this.content = const Value.absent(), this.mediaId = const Value.absent(), this.additionalMessageData = const Value.absent(), @@ -3513,7 +3512,7 @@ class MessagesCompanion extends UpdateCompanion { {Value? groupId, Value? messageId, Value? senderId, - Value? type, + Value? type, Value? content, Value? mediaId, Value? additionalMessageData, @@ -3566,8 +3565,7 @@ class MessagesCompanion extends UpdateCompanion { map['sender_id'] = Variable(senderId.value); } if (type.present) { - map['type'] = - Variable($MessagesTable.$convertertype.toSql(type.value)); + map['type'] = Variable(type.value); } if (content.present) { map['content'] = Variable(content.value); @@ -10148,7 +10146,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String groupId, required String messageId, Value senderId, - required MessageType type, + required String type, Value content, Value mediaId, Value additionalMessageData, @@ -10169,7 +10167,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value groupId, Value messageId, Value senderId, - Value type, + Value type, Value content, Value mediaId, Value additionalMessageData, @@ -10314,10 +10312,8 @@ 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 type => $composableBuilder( + column: $table.type, builder: (column) => ColumnFilters(column)); ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); @@ -10638,7 +10634,7 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get messageId => $composableBuilder(column: $table.messageId, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumn get content => @@ -10858,7 +10854,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 type = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), Value additionalMessageData = const Value.absent(), @@ -10900,7 +10896,7 @@ class $$MessagesTableTableManager extends RootTableManager< required String groupId, required String messageId, Value senderId = const Value.absent(), - required MessageType type, + required String type, Value content = const Value.absent(), Value mediaId = const Value.absent(), Value additionalMessageData = const Value.absent(), diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 7b6c04b..1821ee6 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2967,6 +2967,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'You must authenticate to reopen the image.'** String get authRequestReopenImage; + + /// No description provided for @shareContactsMenu. + /// + /// In en, this message translates to: + /// **'Contact'** + String get shareContactsMenu; + + /// No description provided for @shareContactsTitle. + /// + /// In en, this message translates to: + /// **'Select contacts'** + String get shareContactsTitle; + + /// No description provided for @shareContactsSubmit. + /// + /// In en, this message translates to: + /// **'Share now'** + String get shareContactsSubmit; + + /// No description provided for @updateTwonlyMessage. + /// + /// In en, this message translates to: + /// **'To see this message, you need to update twonly.'** + String get updateTwonlyMessage; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 3a3c99a..4385fee 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1656,4 +1656,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get authRequestReopenImage => 'Um das Bild erneut zu öffnen, musst du dich authentifizieren.'; + + @override + String get shareContactsMenu => 'Kontakt'; + + @override + String get shareContactsTitle => 'Kontakte auswählen'; + + @override + String get shareContactsSubmit => 'Jetzt teilen'; + + @override + String get updateTwonlyMessage => + 'Um diese Nachricht zu sehen, musst du twonly aktualisieren.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index e4d51a4..62fee3d 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1644,4 +1644,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get authRequestReopenImage => 'You must authenticate to reopen the image.'; + + @override + String get shareContactsMenu => 'Contact'; + + @override + String get shareContactsTitle => 'Select contacts'; + + @override + String get shareContactsSubmit => 'Share now'; + + @override + String get updateTwonlyMessage => + 'To see this message, you need to update twonly.'; } diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index 05a47b1..286fb3b 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -1644,4 +1644,17 @@ class AppLocalizationsSv extends AppLocalizations { @override String get authRequestReopenImage => 'You must authenticate to reopen the image.'; + + @override + String get shareContactsMenu => 'Contact'; + + @override + String get shareContactsTitle => 'Select contacts'; + + @override + String get shareContactsSubmit => 'Share now'; + + @override + String get updateTwonlyMessage => + 'To see this message, you need to update twonly.'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 4caaa3d..69d295d 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 4caaa3d91aaf1ac2f13160ba770a2880c26bd229 +Subproject commit 69d295db737253e0c1b68aedc39bf757e8d642e6 diff --git a/lib/src/model/protobuf/client/data.proto b/lib/src/model/protobuf/client/data.proto index 20a3b48..f708fe0 100644 --- a/lib/src/model/protobuf/client/data.proto +++ b/lib/src/model/protobuf/client/data.proto @@ -1,11 +1,18 @@ syntax = "proto3"; +message SharedContact { + int64 user_id = 1; + bytes public_identity_key = 2; + string display_name = 3; +} message AdditionalMessageData { enum Type { LINK = 0; + CONTACTS = 1; } Type type = 1; optional string link = 2; + repeated SharedContact contacts = 3; } \ No newline at end of file diff --git a/lib/src/model/protobuf/client/generated/data.pb.dart b/lib/src/model/protobuf/client/generated/data.pb.dart index 9b547b3..109b83e 100644 --- a/lib/src/model/protobuf/client/generated/data.pb.dart +++ b/lib/src/model/protobuf/client/generated/data.pb.dart @@ -12,6 +12,7 @@ import 'dart:core' as $core; +import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'data.pbenum.dart'; @@ -20,14 +21,96 @@ export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'data.pbenum.dart'; +class SharedContact extends $pb.GeneratedMessage { + factory SharedContact({ + $fixnum.Int64? userId, + $core.List<$core.int>? publicIdentityKey, + $core.String? displayName, + }) { + final result = create(); + if (userId != null) result.userId = userId; + if (publicIdentityKey != null) result.publicIdentityKey = publicIdentityKey; + if (displayName != null) result.displayName = displayName; + return result; + } + + SharedContact._(); + + factory SharedContact.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SharedContact.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SharedContact', + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'userId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'publicIdentityKey', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'displayName') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SharedContact clone() => SharedContact()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SharedContact copyWith(void Function(SharedContact) updates) => + super.copyWith((message) => updates(message as SharedContact)) + as SharedContact; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SharedContact create() => SharedContact._(); + @$core.override + SharedContact createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SharedContact getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SharedContact? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get userId => $_getI64(0); + @$pb.TagNumber(1) + set userId($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasUserId() => $_has(0); + @$pb.TagNumber(1) + void clearUserId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get publicIdentityKey => $_getN(1); + @$pb.TagNumber(2) + set publicIdentityKey($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasPublicIdentityKey() => $_has(1); + @$pb.TagNumber(2) + void clearPublicIdentityKey() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get displayName => $_getSZ(2); + @$pb.TagNumber(3) + set displayName($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasDisplayName() => $_has(2); + @$pb.TagNumber(3) + void clearDisplayName() => $_clearField(3); +} + class AdditionalMessageData extends $pb.GeneratedMessage { factory AdditionalMessageData({ AdditionalMessageData_Type? type, $core.String? link, + $core.Iterable? contacts, }) { final result = create(); if (type != null) result.type = type; if (link != null) result.link = link; + if (contacts != null) result.contacts.addAll(contacts); return result; } @@ -49,6 +132,9 @@ class AdditionalMessageData extends $pb.GeneratedMessage { valueOf: AdditionalMessageData_Type.valueOf, enumValues: AdditionalMessageData_Type.values) ..aOS(2, _omitFieldNames ? '' : 'link') + ..pc( + 3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM, + subBuilder: SharedContact.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -91,6 +177,9 @@ class AdditionalMessageData extends $pb.GeneratedMessage { $core.bool hasLink() => $_has(1); @$pb.TagNumber(2) void clearLink() => $_clearField(2); + + @$pb.TagNumber(3) + $pb.PbList get contacts => $_getList(2); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/data.pbenum.dart b/lib/src/model/protobuf/client/generated/data.pbenum.dart index 4f40622..22a856c 100644 --- a/lib/src/model/protobuf/client/generated/data.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/data.pbenum.dart @@ -17,14 +17,17 @@ import 'package:protobuf/protobuf.dart' as $pb; class AdditionalMessageData_Type extends $pb.ProtobufEnum { static const AdditionalMessageData_Type LINK = AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK'); + static const AdditionalMessageData_Type CONTACTS = + AdditionalMessageData_Type._(1, _omitEnumNames ? '' : 'CONTACTS'); static const $core.List values = [ LINK, + CONTACTS, ]; static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 0); + $pb.ProtobufEnum.$_initByValueList(values, 1); static AdditionalMessageData_Type? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; diff --git a/lib/src/model/protobuf/client/generated/data.pbjson.dart b/lib/src/model/protobuf/client/generated/data.pbjson.dart index fb0a248..328a6f7 100644 --- a/lib/src/model/protobuf/client/generated/data.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/data.pbjson.dart @@ -14,6 +14,28 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use sharedContactDescriptor instead') +const SharedContact$json = { + '1': 'SharedContact', + '2': [ + {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, + { + '1': 'public_identity_key', + '3': 2, + '4': 1, + '5': 12, + '10': 'publicIdentityKey' + }, + {'1': 'display_name', '3': 3, '4': 1, '5': 9, '10': 'displayName'}, + ], +}; + +/// Descriptor for `SharedContact`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sharedContactDescriptor = $convert.base64Decode( + 'Cg1TaGFyZWRDb250YWN0EhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIuChNwdWJsaWNfaWRlbn' + 'RpdHlfa2V5GAIgASgMUhFwdWJsaWNJZGVudGl0eUtleRIhCgxkaXNwbGF5X25hbWUYAyABKAlS' + 'C2Rpc3BsYXlOYW1l'); + @$core.Deprecated('Use additionalMessageDataDescriptor instead') const AdditionalMessageData$json = { '1': 'AdditionalMessageData', @@ -27,6 +49,14 @@ const AdditionalMessageData$json = { '10': 'type' }, {'1': 'link', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'link', '17': true}, + { + '1': 'contacts', + '3': 3, + '4': 3, + '5': 11, + '6': '.SharedContact', + '10': 'contacts' + }, ], '4': [AdditionalMessageData_Type$json], '8': [ @@ -39,11 +69,13 @@ const AdditionalMessageData_Type$json = { '1': 'Type', '2': [ {'1': 'LINK', '2': 0}, + {'1': 'CONTACTS', '2': 1}, ], }; /// Descriptor for `AdditionalMessageData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode( 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' - 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBASIQCgRUeXBlEggKBExJ' - 'TksQAEIHCgVfbGluaw=='); + 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD' + 'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzIh4KBFR5cGUSCAoETElOSxAAEgwKCENPTl' + 'RBQ1RTEAFCBwoFX2xpbms='); diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index b137dde..cf8c9e0 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -767,6 +767,106 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage { void clearQuoteMessageId() => $_clearField(4); } +class EncryptedContent_AdditionalDataMessage extends $pb.GeneratedMessage { + factory EncryptedContent_AdditionalDataMessage({ + $core.String? senderMessageId, + $fixnum.Int64? timestamp, + $core.String? type, + $core.List<$core.int>? additionalMessageData, + }) { + final result = create(); + if (senderMessageId != null) result.senderMessageId = senderMessageId; + if (timestamp != null) result.timestamp = timestamp; + if (type != null) result.type = type; + if (additionalMessageData != null) + result.additionalMessageData = additionalMessageData; + return result; + } + + EncryptedContent_AdditionalDataMessage._(); + + factory EncryptedContent_AdditionalDataMessage.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory EncryptedContent_AdditionalDataMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'EncryptedContent.AdditionalDataMessage', + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'senderMessageId') + ..aInt64(2, _omitFieldNames ? '' : 'timestamp') + ..aOS(3, _omitFieldNames ? '' : 'type') + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'additionalMessageData', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EncryptedContent_AdditionalDataMessage clone() => + EncryptedContent_AdditionalDataMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EncryptedContent_AdditionalDataMessage copyWith( + void Function(EncryptedContent_AdditionalDataMessage) updates) => + super.copyWith((message) => + updates(message as EncryptedContent_AdditionalDataMessage)) + as EncryptedContent_AdditionalDataMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_AdditionalDataMessage create() => + EncryptedContent_AdditionalDataMessage._(); + @$core.override + EncryptedContent_AdditionalDataMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_AdditionalDataMessage getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + EncryptedContent_AdditionalDataMessage>(create); + static EncryptedContent_AdditionalDataMessage? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get senderMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set senderMessageId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSenderMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearSenderMessageId() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get timestamp => $_getI64(1); + @$pb.TagNumber(2) + set timestamp($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasTimestamp() => $_has(1); + @$pb.TagNumber(2) + void clearTimestamp() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get type => $_getSZ(2); + @$pb.TagNumber(3) + set type($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasType() => $_has(2); + @$pb.TagNumber(3) + void clearType() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get additionalMessageData => $_getN(3); + @$pb.TagNumber(4) + set additionalMessageData($core.List<$core.int> value) => + $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasAdditionalMessageData() => $_has(3); + @$pb.TagNumber(4) + void clearAdditionalMessageData() => $_clearField(4); +} + class EncryptedContent_Reaction extends $pb.GeneratedMessage { factory EncryptedContent_Reaction({ $core.String? targetMessageId, @@ -1611,6 +1711,7 @@ class EncryptedContent extends $pb.GeneratedMessage { EncryptedContent_GroupUpdate? groupUpdate, EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, EncryptedContent_ErrorMessages? errorMessages, + EncryptedContent_AdditionalDataMessage? additionalDataMessage, }) { final result = create(); if (groupId != null) result.groupId = groupId; @@ -1632,6 +1733,8 @@ class EncryptedContent extends $pb.GeneratedMessage { if (resendGroupPublicKey != null) result.resendGroupPublicKey = resendGroupPublicKey; if (errorMessages != null) result.errorMessages = errorMessages; + if (additionalDataMessage != null) + result.additionalDataMessage = additionalDataMessage; return result; } @@ -1695,6 +1798,9 @@ class EncryptedContent extends $pb.GeneratedMessage { ..aOM( 18, _omitFieldNames ? '' : 'errorMessages', subBuilder: EncryptedContent_ErrorMessages.create) + ..aOM( + 19, _omitFieldNames ? '' : 'additionalDataMessage', + subBuilder: EncryptedContent_AdditionalDataMessage.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1905,6 +2011,20 @@ class EncryptedContent extends $pb.GeneratedMessage { void clearErrorMessages() => $_clearField(18); @$pb.TagNumber(18) EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16); + + @$pb.TagNumber(19) + EncryptedContent_AdditionalDataMessage get additionalDataMessage => + $_getN(17); + @$pb.TagNumber(19) + set additionalDataMessage(EncryptedContent_AdditionalDataMessage value) => + $_setField(19, value); + @$pb.TagNumber(19) + $core.bool hasAdditionalDataMessage() => $_has(17); + @$pb.TagNumber(19) + void clearAdditionalDataMessage() => $_clearField(19); + @$pb.TagNumber(19) + EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() => + $_ensure(17); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index fd49512..ac60f8f 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -316,6 +316,16 @@ const EncryptedContent$json = { '10': 'errorMessages', '17': true }, + { + '1': 'additional_data_message', + '3': 19, + '4': 1, + '5': 11, + '6': '.EncryptedContent.AdditionalDataMessage', + '9': 17, + '10': 'additionalDataMessage', + '17': true + }, ], '3': [ EncryptedContent_ErrorMessages$json, @@ -324,6 +334,7 @@ const EncryptedContent$json = { EncryptedContent_ResendGroupPublicKey$json, EncryptedContent_GroupUpdate$json, EncryptedContent_TextMessage$json, + EncryptedContent_AdditionalDataMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, @@ -351,6 +362,7 @@ const EncryptedContent$json = { {'1': '_groupUpdate'}, {'1': '_resendGroupPublicKey'}, {'1': '_error_messages'}, + {'1': '_additional_data_message'}, ], }; @@ -470,6 +482,28 @@ const EncryptedContent_TextMessage$json = { ], }; +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_AdditionalDataMessage$json = { + '1': 'AdditionalDataMessage', + '2': [ + {'1': 'sender_message_id', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'timestamp', '3': 2, '4': 1, '5': 3, '10': 'timestamp'}, + {'1': 'type', '3': 3, '4': 1, '5': 9, '10': 'type'}, + { + '1': 'additional_message_data', + '3': 4, + '4': 1, + '5': 12, + '9': 0, + '10': 'additionalMessageData', + '17': true + }, + ], + '8': [ + {'1': '_additional_message_data'}, + ], +}; + @$core.Deprecated('Use encryptedContentDescriptor instead') const EncryptedContent_Reaction$json = { '1': 'Reaction', @@ -827,63 +861,69 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( '5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw' 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' - 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA' - '4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVk' - 'X3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk' - '9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVO' - 'S05PV05fTUVTU0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCH' - 'N0YXRlS2V5EiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91' - 'cEpvaW4SJgoOZ3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZE' - 'dyb3VwUHVibGljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlS' - 'D2dyb3VwQWN0aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZE' - 'NvbnRhY3RJZIgBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMK' - 'Im5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTW' - 'Vzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25l' - 'd0dyb3VwTmFtZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVG' - 'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' - 'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU' - '1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa' - 'YgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFA' - 'oFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNzYWdl' - 'VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVH' - 'lwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEB' - 'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW' - 'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz' - 'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF' - '9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB' - 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk' - '1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa' - 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh' - 'gEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz' - 'dGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg1kb3' - 'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI' - 'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn' - 'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl' - 'iAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYW' - 'dlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxACEgcK' - 'A0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW' - '90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5j' - 'cnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZG' - 'F0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVk' - 'aWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3' - 'NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9F' - 'UlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW' - '50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVK' - 'RUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3' - 'J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXBy' - 'ZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCU' - 'gBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIf' - 'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZE' - 'ILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgO' - 'Mh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSA' - 'BSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJS' - 'CWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SW' - 'RCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEg' - 'ASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdE' - 'ZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAKC2Zv' - 'cmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaG' - 'F0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFC' - 'DgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCg' - 'pfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4K' - 'DF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3' - 'JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2Vz'); + 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS' + 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h' + 'bERhdGFNZXNzYWdliAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' + 'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf' + 'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' + '1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT' + 'U0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5Ei' + 'YKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoO' + 'Z3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibG' + 'ljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0' + 'aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZI' + 'gBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0' + 'ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZn' + 'Rlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFt' + 'ZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2' + 'USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEo' + 'CVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZB' + 'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0' + 'aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2' + 'FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUS' + 'OwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYX' + 'RhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRN' + 'ZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFg' + 'oGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIk' + 'LkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3' + 'NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNz' + 'YWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUg' + 'R0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRF' + 'EAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdG' + 'V4dBrwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQS' + 'MAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaX' + 'NwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vj' + 'b25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbn' + 'RpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlk' + 'GAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG' + '93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmI' + 'AQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cH' + 'Rpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNz' + 'YWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUk' + 'VVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtf' + 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG' + '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0' + 'aW9uTm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2Cg' + 'R0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigK' + 'D3RhcmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUE' + 'VORUQQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVl' + 'c3QSOQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZV' + 'IEdHlwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoN' + 'Q29udGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VX' + 'BkYXRlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT' + 'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3' + 'BsYXlOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoK' + 'BlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcG' + 'xheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1' + 'c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgAS' + 'gMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUS' + 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQX' + 'QaqQEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZs' + 'YXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCm' + 'Jlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNl' + 'VXBkYXRlQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3' + 'VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2Nv' + 'bnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZX' + 'lzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VCDgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91' + 'cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbmRHcm91cFB1YmxpY0tleUIRCg9fZXJyb3' + 'JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdl'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 38e24ad..062fc6f 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -52,7 +52,7 @@ message EncryptedContent { optional GroupUpdate groupUpdate = 16; optional ResendGroupPublicKey resendGroupPublicKey = 17; optional ErrorMessages error_messages = 18; - + optional AdditionalDataMessage additional_data_message = 19; message ErrorMessages { enum Type { @@ -93,6 +93,13 @@ message EncryptedContent { optional string quoteMessageId = 4; } + message AdditionalDataMessage { + string sender_message_id = 1; + int64 timestamp = 2; + string type = 3; + optional bytes additional_message_data = 4; + } + message Reaction { string targetMessageId = 1; string emoji = 2; diff --git a/lib/src/services/api/client2client/additional_data.c2c.dart b/lib/src/services/api/client2client/additional_data.c2c.dart new file mode 100644 index 0000000..fdea032 --- /dev/null +++ b/lib/src/services/api/client2client/additional_data.c2c.dart @@ -0,0 +1,36 @@ +import 'package:clock/clock.dart' show clock; +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 handleAdditionalDataMessage( + int fromUserId, + String groupId, + EncryptedContent_AdditionalDataMessage message, +) async { + Log.info( + 'Got a additional data message: ${message.senderMessageId} from $groupId', + ); + final msg = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(message.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + type: Value(message.type), + additionalMessageData: + Value(Uint8List.fromList(message.additionalMessageData)), + createdAt: Value(fromTimestamp(message.timestamp)), + ackByServer: Value(clock.now()), + ), + ); + await twonlyDB.groupsDao.increaseLastMessageExchange( + groupId, + fromTimestamp(message.timestamp), + ); + if (msg != null) { + Log.info('Inserted a new text message with ID: ${msg.messageId}'); + } +} diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 4cef307..9ea1886 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -116,7 +116,7 @@ Future handleMedia( senderId: Value(fromUserId), groupId: Value(groupId), mediaId: Value(mediaFile.mediaId), - type: const Value(MessageType.media), + type: Value(MessageType.media.name), additionalMessageData: Value.absentIfNull( media.hasAdditionalMessageData() ? Uint8List.fromList(media.additionalMessageData) diff --git a/lib/src/services/api/client2client/text_message.c2c.dart b/lib/src/services/api/client2client/text_message.c2c.dart index 2a9b301..a189f39 100644 --- a/lib/src/services/api/client2client/text_message.c2c.dart +++ b/lib/src/services/api/client2client/text_message.c2c.dart @@ -22,7 +22,7 @@ Future handleTextMessage( senderId: Value(fromUserId), groupId: Value(groupId), content: Value(textMessage.text), - type: const Value(MessageType.text), + type: Value(MessageType.text.name), quotesMessageId: Value( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 75368ab..6782e3b 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -5,7 +5,6 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:path/path.dart'; diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 7dd4f20..6e06a2c 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -102,7 +102,7 @@ Future insertMediaFileInMessagesTable( MessagesCompanion( groupId: Value(groupId), mediaId: Value(mediaService.mediaFile.mediaId), - type: const Value(MessageType.media), + type: Value(MessageType.media.name), additionalMessageData: Value.absentIfNull(additionalData?.writeToBuffer()), ), diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index c92bdab..dd83859 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -7,14 +7,17 @@ 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/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/protobuf/api/websocket/error.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; 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/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -204,7 +207,7 @@ Future insertAndSendTextMessage( MessagesCompanion( groupId: Value(groupId), content: Value(textMessage), - type: const Value(MessageType.text), + type: Value(MessageType.text.name), quotesMessageId: Value(quotesMessageId), ), ); @@ -232,6 +235,61 @@ Future insertAndSendTextMessage( ); } +Future insertAndSendContactShareMessage( + String groupId, + List contactsToShare, +) async { + final contacts = []; + + for (final contactId in contactsToShare) { + final contact = await twonlyDB.contactsDao.getContactById(contactId); + if (contact != null) { + final publicIdentityKey = await getPublicKeyFromContact(contactId); + + contacts.add( + SharedContact( + userId: Int64(contact.userId), + publicIdentityKey: publicIdentityKey, + displayName: getContactDisplayName(contact), + ), + ); + } + } + + final additionalMessageData = AdditionalMessageData( + type: AdditionalMessageData_Type.CONTACTS, + contacts: contacts, + ); + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + type: Value(MessageType.contacts.name), + additionalMessageData: Value(additionalMessageData.writeToBuffer()), + ), + ); + + if (message == null) { + Log.error('Could not insert message into database'); + return; + } + + final encryptedContent = pb.EncryptedContent( + additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage( + senderMessageId: message.messageId, + additionalMessageData: additionalMessageData.writeToBuffer(), + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + type: MessageType.contacts.name, + ), + ); + + await sendCipherTextToGroup( + groupId, + encryptedContent, + messageId: message.messageId, + ); +} + Future sendCipherTextToGroup( String groupId, pb.EncryptedContent encryptedContent, { diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 2a0546f..7ca7f0d 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -13,6 +13,7 @@ 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/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/client2client/additional_data.c2c.dart'; import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; import 'package:twonly/src/services/api/client2client/errors.c2c.dart'; import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; @@ -374,6 +375,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( return (null, null); } + if (content.hasAdditionalDataMessage()) { + await handleAdditionalDataMessage( + fromUserId, + content.groupId, + content.additionalDataMessage, + ); + return (null, null); + } + if (content.hasTextMessage()) { await handleTextMessage( fromUserId, diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 7357d06..facc198 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -12,6 +12,8 @@ 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/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; class Result { Result.error(this.error) : value = null; @@ -78,3 +80,23 @@ Future handleMediaError(MediaFile media) async { ), ); } + +Future importSignalContactAndCreateRequest( + server.Response_UserData userdata, +) async { + if (await createNewSignalSession(userdata)) { + // 1. Setup notifications keys with the other user + await setupNotificationWithUsers( + forceContact: userdata.userId.toInt(), + ); + // 2. Then send user request + await sendCipherText( + userdata.userId.toInt(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), + ), + ); + } +} diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 062b87c..dd319b8 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -242,6 +242,11 @@ Future getPushNotificationFromEncryptedContent( additionalContent = group.groupName; } } + + if (content.hasAdditionalDataMessage()) { + kind = PushKind.text; + } + if (content.hasMedia()) { switch (content.media.type) { case EncryptedContent_Media_Type.REUPLOAD: diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 2db2272..e398fac 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -24,6 +24,16 @@ extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; TwonlyDB get db => Provider.of(this); ColorScheme get color => Theme.of(this).colorScheme; + Future navPush(Widget route) async { + return Navigator.push( + this, + MaterialPageRoute( + builder: (context) { + return route; + }, + ), + ); + } } Future saveImageToGallery(Uint8List imageBytes) async { @@ -292,7 +302,7 @@ Color getMessageColorFromType( ) { Color color; - if (message.type == MessageType.text) { + if (message.type == MessageType.text.name) { color = Colors.blueAccent; } else if (mediaFile != null) { if (mediaFile.requiresAuthentication) { diff --git a/lib/src/utils/qr.dart b/lib/src/utils/qr.dart index 5b2df48..331b00f 100644 --- a/lib/src/utils/qr.dart +++ b/lib/src/utils/qr.dart @@ -4,12 +4,9 @@ import 'package:flutter/foundation.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; -import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; -import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; Future getProfileQrCodeData() async { @@ -80,21 +77,5 @@ Future addNewContactFromPublicProfile(PublicProfile profile) async { ), ); - if (added > 0) { - if (await createNewSignalSession(userdata)) { - // 1. Setup notifications keys with the other user - await setupNotificationWithUsers( - forceContact: userdata.userId.toInt(), - ); - // 2. Then send user request - await sendCipherText( - userdata.userId.toInt(), - EncryptedContent( - contactRequest: EncryptedContent_ContactRequest( - type: EncryptedContent_ContactRequest_Type.REQUEST, - ), - ), - ); - } - } + if (added > 0) await importSignalContactAndCreateRequest(userdata); } diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index aef43d6..a80ec70 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -9,8 +9,7 @@ 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/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/services/signal/session.signal.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'; @@ -110,23 +109,7 @@ class _SearchUsernameView extends State { ), ); - if (added > 0) { - if (await createNewSignalSession(userdata)) { - // 1. Setup notifications keys with the other user - await setupNotificationWithUsers( - forceContact: userdata.userId.toInt(), - ); - // 2. Then send user request - await sendCipherText( - userdata.userId.toInt(), - EncryptedContent( - contactRequest: EncryptedContent_ContactRequest( - type: EncryptedContent_ContactRequest_Type.REQUEST, - ), - ), - ); - } - } + if (added > 0) await importSignalContactAndCreateRequest(userdata); } InputDecoration getInputDecoration(String hintText) { @@ -212,7 +195,7 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const FaIcon(Icons.archive_outlined, size: 15), onPressed: () async { - const update = ContactsCompanion(requested: Value(false)); + const update = ContactsCompanion(deletedByUser: Value(true)); await twonlyDB.contactsDao.updateContact(contact.userId, update); }, ), 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 b760097..fa7a22f 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 @@ -129,10 +129,11 @@ class _UserListItem extends State { _previewMessages = [newLastMessage]; } - final msgs = - _previewMessages.where((x) => x.type == MessageType.media).toList(); + final msgs = _previewMessages + .where((x) => x.type == MessageType.media.name) + .toList(); if (msgs.isNotEmpty && - msgs.first.type == MessageType.media && + msgs.first.type == MessageType.media.name && !msgs.first.isDeletedFromSender && msgs.first.senderId != null && msgs.first.openedAt == null) { @@ -167,8 +168,9 @@ class _UserListItem extends State { } if (_hasNonOpenedMediaFile) { - final msgs = - _previewMessages.where((x) => x.type == MessageType.media).toList(); + final msgs = _previewMessages + .where((x) => x.type == MessageType.media.name) + .toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); if (mediaFile?.type != MediaType.audio) { diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 771ac88..fab9358 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -200,7 +200,7 @@ class _ChatMessagesViewState extends State { } } index += 1; - if (msg.type == MessageType.text && + if (msg.type != MessageType.media.name && msg.senderId != null && msg.openedAt == null) { if (openedMessages[msg.senderId!] == null) { @@ -209,7 +209,7 @@ class _ChatMessagesViewState extends State { openedMessages[msg.senderId!]!.add(msg.messageId); } - if (msg.type == MessageType.media && msg.mediaStored) { + if (msg.type == MessageType.media.name && msg.mediaStored) { storedMediaFiles.add(msg); } diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart new file mode 100644 index 0000000..508ce95 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart @@ -0,0 +1,112 @@ +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/shared/select_contacts.view.dart'; + +class ShareAdditionalView extends StatefulWidget { + const ShareAdditionalView({required this.group, super.key}); + + final Group group; + + @override + State createState() => _ShareAdditionalViewState(); +} + +class _ShareAdditionalViewState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Future openShareContactView() async { + final selectedContacts = await context.navPush( + SelectContactsView( + text: SelectedContactViewText( + title: context.lang.shareContactsTitle, + submitButton: (_, __) => context.lang.shareContactsSubmit, + submitIcon: FontAwesomeIcons.shareNodes, + ), + ), + ) as List?; + if (selectedContacts != null && selectedContacts.isNotEmpty) { + await insertAndSendContactShareMessage( + widget.group.groupId, + selectedContacts, + ); + } + if (mounted) { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 220, + 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.only(top: 30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: openShareContactView, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + child: const FaIcon(FontAwesomeIcons.circleUser), + ), + const SizedBox(height: 8), + Text( + context.lang.shareContactsMenu, + 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 7b05a6d..e13aef5 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 @@ -12,8 +12,10 @@ 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_reaction_row.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_contacts.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/chat_unkown.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'; @@ -90,6 +92,49 @@ class _ChatListEntryState extends State { setState(() {}); } + Widget? _getChatEntry(BorderRadius borderRadius, int reactionsForWidth) { + if (widget.message.type == MessageType.text.name) { + return ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + userIdToContact: widget.userIdToContact, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ); + } + + if (widget.message.type == MessageType.media.name) { + if (mediaService == null) return null; + if (mediaService!.mediaFile.type == MediaType.audio) { + return ChatAudioEntry( + message: widget.message, + nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + mediaService: mediaService!, + userIdToContact: widget.userIdToContact, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ); + } + return ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + minWidth: reactionsForWidth * 43, + ); + } + + if (widget.message.type == MessageType.contacts.name) { + return ChatContactsEntry( + message: widget.message, + ); + } + + return const ChatUnknownEntry(); + } + @override Widget build(BuildContext context) { final right = widget.message.senderId == null; @@ -129,34 +174,7 @@ class _ChatListEntryState extends State { mediaService: mediaService, borderRadius: borderRadius, scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - prevMessage: widget.prevMessage, - userIdToContact: widget.userIdToContact, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - : (mediaService == null) - ? null - : (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, - ), + child: _getChatEntry(borderRadius, reactionsForWidth), ), if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), ], diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart new file mode 100644 index 0000000..f025012 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart @@ -0,0 +1,201 @@ +import 'dart:convert'; + +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/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatContactsEntry extends StatefulWidget { + const ChatContactsEntry({ + required this.message, + super.key, + }); + + final Message message; + + @override + State createState() => _ChatContactsEntryState(); +} + +class _ChatContactsEntryState extends State { + @override + Widget build(BuildContext context) { + AdditionalMessageData? data; + + if (widget.message.additionalMessageData != null) { + try { + data = AdditionalMessageData.fromBuffer( + widget.message.additionalMessageData!, + ); + } catch (e) { + data = null; + } + } + + if (data == null || data.contacts.isEmpty) { + return const SizedBox.shrink(); + } + + final info = getBubbleInfo( + context, + widget.message, + null, + null, + null, + 0, + ); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: info.color, + borderRadius: BorderRadius.circular(12), + ), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < data.contacts.length; i++) ...[ + if (i > 0) + Divider( + height: 1, + color: Colors.white.withValues(alpha: 0.2), + ), + _ContactRow( + contact: data.contacts[i], + message: widget.message, + ), + ], + ], + ), + ), + ); + } +} + +class _ContactRow extends StatefulWidget { + const _ContactRow({ + required this.contact, + required this.message, + }); + + final SharedContact contact; + final Message message; + + @override + State<_ContactRow> createState() => _ContactRowState(); +} + +class _ContactRowState extends State<_ContactRow> { + bool _isLoading = false; + + Future _onContactClick() async { + setState(() { + _isLoading = true; + }); + + try { + final userdata = + await apiService.getUserById(widget.contact.userId.toInt()); + if (userdata == null) return; + + var verified = false; + if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) { + final sender = + await twonlyDB.contactsDao.getContactById(widget.message.senderId!); + // in case the sender is verified and the public keys are the same, this trust can be transferred + verified = sender != null && sender.verified; + } + + final added = await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(userdata.username)), + userId: Value(userdata.userId.toInt()), + requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), + verified: Value( + verified, + ), + ), + ); + + if (added > 0) await importSignalContactAndCreateRequest(userdata); + } catch (e) { + Log.error(e); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()), + builder: (context, snapshot) { + final contactInDb = snapshot.data; + final isAdded = contactInDb != null || + widget.contact.userId.toInt() == gUser.userId; + + return GestureDetector( + onTap: (widget.message.senderId == null || isAdded || _isLoading) + ? null + : _onContactClick, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.user, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 8), + Flexible( + child: BetterText( + text: widget.contact.displayName, + textColor: Colors.white, + ), + ), + if (widget.message.senderId != null && !isAdded) ...[ + const Spacer(), + const SizedBox(width: 8), + if (_isLoading) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + else + const FaIcon( + FontAwesomeIcons.userPlus, + color: Colors.white, + size: 16, + ), + ], + ], + ), + ), + ); + }, + ); + } +} 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 1f89085..4a497b3 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 @@ -117,7 +117,7 @@ class _ChatMediaEntryState extends State { return GestureDetector( key: reopenMediaFile, onDoubleTap: onDoubleTap, - onTap: (widget.message.type == MessageType.media) ? onTap : null, + onTap: (widget.message.type == MessageType.media.name) ? onTap : null, child: SizedBox( width: (widget.minWidth > 150) ? widget.minWidth : 150, height: (widget.message.mediaStored && diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart new file mode 100644 index 0000000..6c221e6 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatUnknownEntry extends StatelessWidget { + const ChatUnknownEntry({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: isDarkMode(context) ? Colors.black : Colors.grey, + borderRadius: BorderRadius.circular(12), + ), + child: BetterText( + text: context.lang.updateTwonlyMessage, + textColor: isDarkMode(context) + ? const Color.fromARGB(255, 99, 99, 99) + : Colors.black, + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/common.dart b/lib/src/views/chats/chat_messages_components/entries/common.dart index da56997..f6be8f8 100644 --- a/lib/src/views/chats/chat_messages_components/entries/common.dart +++ b/lib/src/views/chats/chat_messages_components/entries/common.dart @@ -85,8 +85,8 @@ double measureTextWidth( 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 (nextMessage.type == MessageType.text.name && + message.type == MessageType.text.name) { if (!EmojiAnimation.supported(nextMessage.content!)) { final diff = nextMessage.createdAt.difference(message.createdAt).inMinutes; 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 078e657..e2b12b1 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 @@ -127,7 +127,7 @@ class MessageContextMenu extends StatelessWidget { ), if (!message.isDeletedFromSender && message.senderId == null && - message.type == MessageType.text) + message.type == MessageType.text.name) ContextMenuItem( title: context.lang.edit, onTap: () async { 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 6c195c7..af683a2 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -15,6 +15,7 @@ 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'; +import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; class MessageInput extends StatefulWidget { @@ -167,6 +168,19 @@ class _MessageInputState extends State { } } + Future _showAdditionalShareModal(BuildContext context) async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (context) { + return ShareAdditionalView( + group: widget.group, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Column( @@ -312,6 +326,20 @@ class _MessageInputState extends State { ], ), ), + if (_textFieldController.text == '') + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), if (_textFieldController.text == '') GestureDetector( onLongPressMoveUpdate: (details) { @@ -452,18 +480,9 @@ class _MessageInputState extends State { ) else IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), + icon: const FaIcon(FontAwesomeIcons.plus), padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, + onPressed: () => _showAdditionalShareModal(context), ), ], ), 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 c8400b2..2b40303 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 @@ -87,7 +87,7 @@ class _MessageSendStateIconState extends State { var text = ''; Widget? textWidget; textWidget = null; - final kindsAlreadyShown = HashSet(); + final kindsAlreadyShown = HashSet(); var hasLoader = false; GestureTapCallback? onTap; @@ -133,7 +133,7 @@ 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 && mediaFile != null) { + if (message.type == MessageType.media.name && mediaFile != null) { if (mediaFile.downloadState == DownloadState.pending) { text = context.lang.messageSendState_TapToLoad; } @@ -210,7 +210,7 @@ class _MessageSendStateIconState extends State { break; } - if (message.type == MessageType.media) { + if (message.type == MessageType.media.name) { icons.insert(0, icon); } else { icons.add(icon); 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 1c27f90..f312de1 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -169,12 +169,12 @@ class _ResponsePreviewState extends State { var color = const Color.fromARGB(233, 68, 137, 255); if (_message != null) { - if (_message!.type == MessageType.text) { + if (_message!.type == MessageType.text.name) { if (_message!.content != null) { subtitle = truncateString(_message!.content!); } } - if (_message!.type == MessageType.media && _mediaService != null) { + if (_message!.type == MessageType.media.name && _mediaService != null) { switch (_mediaService!.mediaFile.type) { case MediaType.image: subtitle = context.lang.image; diff --git a/lib/src/views/chats/media_viewer_components/additional_message_content.dart b/lib/src/views/chats/media_viewer_components/additional_message_content.dart index 7f810c2..5371b7f 100644 --- a/lib/src/views/chats/media_viewer_components/additional_message_content.dart +++ b/lib/src/views/chats/media_viewer_components/additional_message_content.dart @@ -45,6 +45,7 @@ class AdditionalMessageContent extends StatelessWidget { ], ), ); + // ignore: no_default_cases default: } // ignore: empty_catches diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 1deebbc..41bc54b 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/constants/routes.keys.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'; @@ -14,7 +16,6 @@ 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/groups/group.view.dart'; -import 'package:twonly/src/views/public_profile.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -187,14 +188,7 @@ class _ContactViewState extends State { icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PublicProfileView(); - }, - ), - ); + await context.push(Routes.settingsPublicProfile); setState(() {}); }, ), diff --git a/lib/src/views/shared/select_contacts.view.dart b/lib/src/views/shared/select_contacts.view.dart new file mode 100644 index 0000000..6b5a989 --- /dev/null +++ b/lib/src/views/shared/select_contacts.view.dart @@ -0,0 +1,271 @@ +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'; + +class SelectedContactViewText { + const SelectedContactViewText({ + required this.title, + required this.submitButton, + required this.submitIcon, + }); + final String title; + final String Function(int selected, int? limit) submitButton; + final IconData submitIcon; +} + +class SelectContactsView extends StatefulWidget { + const SelectContactsView({ + required this.text, + this.alreadySelected, + this.limit, + super.key, + }); + final SelectedContactViewText text; + final List? alreadySelected; + final int? limit; + @override + State createState() => _SelectAdditionalUsers(); +} + +class _SelectAdditionalUsers extends State { + List contacts = []; + List allContacts = []; + final TextEditingController searchUserName = TextEditingController(); + late StreamSubscription> contactSub; + + final HashSet selectedUsers = HashSet(); + late HashSet _alreadySelected; + + @override + void initState() { + super.initState(); + + _alreadySelected = HashSet.from(widget.alreadySelected ?? []); + + 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 (_alreadySelected.contains(userId)) return; + if (!selectedUsers.contains(userId)) { + if (widget.limit == null || selectedUsers.length < widget.limit!) { + 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(widget.text.title), + ), + floatingActionButton: FilledButton.icon( + onPressed: selectedUsers.isEmpty + ? null + : () => Navigator.pop(context, selectedUsers.toList()), + label: Text( + widget.text.submitButton( + selectedUsers.length + (widget.alreadySelected?.length ?? 0), + widget.limit, + ), + ), + icon: FaIcon(widget.text.submitIcon), + ), + 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: (context, 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) { + 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: ValueKey(user.userId), + contact: user, + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(user)), + FlameCounterWidget( + contactId: user.userId, + prefix: true, + ), + ], + ), + subtitle: (_alreadySelected.contains(user.userId)) + ? Text(context.lang.alreadyInGroup) + : null, + leading: AvatarIcon( + contactId: user.userId, + fontSize: 13, + ), + trailing: Checkbox( + value: selectedUsers.contains(user.userId) | + _alreadySelected.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: (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 GestureDetector( + onTap: () => onTap(contact.userId), + child: Chip( + avatar: AvatarIcon( + contactId: contact.userId, + fontSize: 10, + ), + label: 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, + ), + ], + ), + ), + ); + } +}