feature: share contacts

This commit is contained in:
otsmr 2026-02-13 02:16:04 +01:00
parent 15ae2b5669
commit 835ee9ee2d
49 changed files with 1432 additions and 214 deletions

View file

@ -5,4 +5,12 @@ part of 'contacts.dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> { mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts; $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);
} }

View file

@ -8,4 +8,19 @@ mixin _$GroupsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts; $ContactsTable get contacts => attachedDatabase.contacts;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$GroupHistoriesTable get groupHistories => attachedDatabase.groupHistories; $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);
} }

View file

@ -5,4 +5,12 @@ part of 'mediafiles.dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> { mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> {
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $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);
} }

View file

@ -75,10 +75,12 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
(t.isDeletedFromSender.equals(true) | (t.isDeletedFromSender.equals(true) |
((t.type.equals(MessageType.text.name) & (t.type.equals(MessageType.text.name).not() |
t.content.isNotNull()) | t.type.equals(MessageType.media.name).not()) |
(t.type.equals(MessageType.media.name) & (t.type.equals(MessageType.text.name) &
t.mediaId.isNotNull()))), t.content.isNotNull()) |
(t.type.equals(MessageType.media.name) &
t.mediaId.isNotNull())),
)) ))
..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
.watch(); .watch();

View file

@ -13,4 +13,28 @@ mixin _$MessagesDaoMixin on DatabaseAccessor<TwonlyDB> {
attachedDatabase.messageHistories; attachedDatabase.messageHistories;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$MessageActionsTable get messageActions => attachedDatabase.messageActions; $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);
} }

View file

@ -9,4 +9,20 @@ mixin _$ReactionsDaoMixin on DatabaseAccessor<TwonlyDB> {
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
$MessagesTable get messages => attachedDatabase.messages; $MessagesTable get messages => attachedDatabase.messages;
$ReactionsTable get reactions => attachedDatabase.reactions; $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);
} }

View file

@ -12,4 +12,26 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor<TwonlyDB> {
$MessageActionsTable get messageActions => attachedDatabase.messageActions; $MessageActionsTable get messageActions => attachedDatabase.messageActions;
$ReceivedReceiptsTable get receivedReceipts => $ReceivedReceiptsTable get receivedReceipts =>
attachedDatabase.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);
} }

View file

@ -9,4 +9,19 @@ mixin _$SignalDaoMixin on DatabaseAccessor<TwonlyDB> {
attachedDatabase.signalContactPreKeys; attachedDatabase.signalContactPreKeys;
$SignalContactSignedPreKeysTable get signalContactSignedPreKeys => $SignalContactSignedPreKeysTable get signalContactSignedPreKeys =>
attachedDatabase.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);
} }

View file

@ -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/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
enum MessageType { media, text } enum MessageType { media, text, contacts }
@DataClassName('Message') @DataClassName('Message')
class Messages extends Table { class Messages extends Table {
@ -15,7 +15,7 @@ class Messages extends Table {
IntColumn get senderId => IntColumn get senderId =>
integer().nullable().references(Contacts, #userId)(); integer().nullable().references(Contacts, #userId)();
TextColumn get type => textEnum<MessageType>()(); TextColumn get type => text()();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();
TextColumn get mediaId => text() TextColumn get mediaId => text()

View file

@ -2776,11 +2776,11 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)'));
static const VerificationMeta _typeMeta = const VerificationMeta('type');
@override @override
late final GeneratedColumnWithTypeConverter<MessageType, String> type = late final GeneratedColumn<String> type = GeneratedColumn<String>(
GeneratedColumn<String>('type', aliasedName, false, 'type', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string, requiredDuringInsert: true);
.withConverter<MessageType>($MessagesTable.$convertertype);
static const VerificationMeta _contentMeta = static const VerificationMeta _contentMeta =
const VerificationMeta('content'); const VerificationMeta('content');
@override @override
@ -2929,6 +2929,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
context.handle(_senderIdMeta, context.handle(_senderIdMeta,
senderId.isAcceptableOrUnknown(data['sender_id']!, _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')) { if (data.containsKey('content')) {
context.handle(_contentMeta, context.handle(_contentMeta,
content.isAcceptableOrUnknown(data['content']!, _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'])!, .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!,
senderId: attachedDatabase.typeMapping senderId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}sender_id']), .read(DriftSqlType.int, data['${effectivePrefix}sender_id']),
type: $MessagesTable.$convertertype.fromSql(attachedDatabase.typeMapping type: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}type'])!), .read(DriftSqlType.string, data['${effectivePrefix}type'])!,
content: attachedDatabase.typeMapping content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content']), .read(DriftSqlType.string, data['${effectivePrefix}content']),
mediaId: attachedDatabase.typeMapping mediaId: attachedDatabase.typeMapping
@ -3057,16 +3063,13 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
$MessagesTable createAlias(String alias) { $MessagesTable createAlias(String alias) {
return $MessagesTable(attachedDatabase, alias); return $MessagesTable(attachedDatabase, alias);
} }
static JsonTypeConverter2<MessageType, String, String> $convertertype =
const EnumNameConverter<MessageType>(MessageType.values);
} }
class Message extends DataClass implements Insertable<Message> { class Message extends DataClass implements Insertable<Message> {
final String groupId; final String groupId;
final String messageId; final String messageId;
final int? senderId; final int? senderId;
final MessageType type; final String type;
final String? content; final String? content;
final String? mediaId; final String? mediaId;
final Uint8List? additionalMessageData; final Uint8List? additionalMessageData;
@ -3108,9 +3111,7 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || senderId != null) { if (!nullToAbsent || senderId != null) {
map['sender_id'] = Variable<int>(senderId); map['sender_id'] = Variable<int>(senderId);
} }
{ map['type'] = Variable<String>(type);
map['type'] = Variable<String>($MessagesTable.$convertertype.toSql(type));
}
if (!nullToAbsent || content != null) { if (!nullToAbsent || content != null) {
map['content'] = Variable<String>(content); map['content'] = Variable<String>(content);
} }
@ -3201,8 +3202,7 @@ class Message extends DataClass implements Insertable<Message> {
groupId: serializer.fromJson<String>(json['groupId']), groupId: serializer.fromJson<String>(json['groupId']),
messageId: serializer.fromJson<String>(json['messageId']), messageId: serializer.fromJson<String>(json['messageId']),
senderId: serializer.fromJson<int?>(json['senderId']), senderId: serializer.fromJson<int?>(json['senderId']),
type: $MessagesTable.$convertertype type: serializer.fromJson<String>(json['type']),
.fromJson(serializer.fromJson<String>(json['type'])),
content: serializer.fromJson<String?>(json['content']), content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']), mediaId: serializer.fromJson<String?>(json['mediaId']),
additionalMessageData: additionalMessageData:
@ -3228,8 +3228,7 @@ class Message extends DataClass implements Insertable<Message> {
'groupId': serializer.toJson<String>(groupId), 'groupId': serializer.toJson<String>(groupId),
'messageId': serializer.toJson<String>(messageId), 'messageId': serializer.toJson<String>(messageId),
'senderId': serializer.toJson<int?>(senderId), 'senderId': serializer.toJson<int?>(senderId),
'type': 'type': serializer.toJson<String>(type),
serializer.toJson<String>($MessagesTable.$convertertype.toJson(type)),
'content': serializer.toJson<String?>(content), 'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId), 'mediaId': serializer.toJson<String?>(mediaId),
'additionalMessageData': 'additionalMessageData':
@ -3252,7 +3251,7 @@ class Message extends DataClass implements Insertable<Message> {
{String? groupId, {String? groupId,
String? messageId, String? messageId,
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
MessageType? type, String? type,
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> additionalMessageData = const Value.absent(), Value<Uint8List?> additionalMessageData = const Value.absent(),
@ -3403,7 +3402,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<String> groupId; final Value<String> groupId;
final Value<String> messageId; final Value<String> messageId;
final Value<int?> senderId; final Value<int?> senderId;
final Value<MessageType> type; final Value<String> type;
final Value<String?> content; final Value<String?> content;
final Value<String?> mediaId; final Value<String?> mediaId;
final Value<Uint8List?> additionalMessageData; final Value<Uint8List?> additionalMessageData;
@ -3444,7 +3443,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
required String groupId, required String groupId,
required String messageId, required String messageId,
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
required MessageType type, required String type,
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.additionalMessageData = const Value.absent(), this.additionalMessageData = const Value.absent(),
@ -3513,7 +3512,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
{Value<String>? groupId, {Value<String>? groupId,
Value<String>? messageId, Value<String>? messageId,
Value<int?>? senderId, Value<int?>? senderId,
Value<MessageType>? type, Value<String>? type,
Value<String?>? content, Value<String?>? content,
Value<String?>? mediaId, Value<String?>? mediaId,
Value<Uint8List?>? additionalMessageData, Value<Uint8List?>? additionalMessageData,
@ -3566,8 +3565,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
map['sender_id'] = Variable<int>(senderId.value); map['sender_id'] = Variable<int>(senderId.value);
} }
if (type.present) { if (type.present) {
map['type'] = map['type'] = Variable<String>(type.value);
Variable<String>($MessagesTable.$convertertype.toSql(type.value));
} }
if (content.present) { if (content.present) {
map['content'] = Variable<String>(content.value); map['content'] = Variable<String>(content.value);
@ -10148,7 +10146,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
required String groupId, required String groupId,
required String messageId, required String messageId,
Value<int?> senderId, Value<int?> senderId,
required MessageType type, required String type,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<Uint8List?> additionalMessageData, Value<Uint8List?> additionalMessageData,
@ -10169,7 +10167,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<String> groupId, Value<String> groupId,
Value<String> messageId, Value<String> messageId,
Value<int?> senderId, Value<int?> senderId,
Value<MessageType> type, Value<String> type,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<Uint8List?> additionalMessageData, Value<Uint8List?> additionalMessageData,
@ -10314,10 +10312,8 @@ class $$MessagesTableFilterComposer
ColumnFilters<String> get messageId => $composableBuilder( ColumnFilters<String> get messageId => $composableBuilder(
column: $table.messageId, builder: (column) => ColumnFilters(column)); column: $table.messageId, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<MessageType, MessageType, String> get type => ColumnFilters<String> get type => $composableBuilder(
$composableBuilder( column: $table.type, builder: (column) => ColumnFilters(column));
column: $table.type,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnFilters<String> get content => $composableBuilder( ColumnFilters<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnFilters(column)); column: $table.content, builder: (column) => ColumnFilters(column));
@ -10638,7 +10634,7 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<String> get messageId => GeneratedColumn<String> get messageId =>
$composableBuilder(column: $table.messageId, builder: (column) => column); $composableBuilder(column: $table.messageId, builder: (column) => column);
GeneratedColumnWithTypeConverter<MessageType, String> get type => GeneratedColumn<String> get type =>
$composableBuilder(column: $table.type, builder: (column) => column); $composableBuilder(column: $table.type, builder: (column) => column);
GeneratedColumn<String> get content => GeneratedColumn<String> get content =>
@ -10858,7 +10854,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<String> groupId = const Value.absent(), Value<String> groupId = const Value.absent(),
Value<String> messageId = const Value.absent(), Value<String> messageId = const Value.absent(),
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<MessageType> type = const Value.absent(), Value<String> type = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> additionalMessageData = const Value.absent(), Value<Uint8List?> additionalMessageData = const Value.absent(),
@ -10900,7 +10896,7 @@ class $$MessagesTableTableManager extends RootTableManager<
required String groupId, required String groupId,
required String messageId, required String messageId,
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
required MessageType type, required String type,
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> additionalMessageData = const Value.absent(), Value<Uint8List?> additionalMessageData = const Value.absent(),

View file

@ -2967,6 +2967,30 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'You must authenticate to reopen the image.'** /// **'You must authenticate to reopen the image.'**
String get authRequestReopenImage; 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 class _AppLocalizationsDelegate

View file

@ -1656,4 +1656,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get authRequestReopenImage => String get authRequestReopenImage =>
'Um das Bild erneut zu öffnen, musst du dich authentifizieren.'; '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.';
} }

View file

@ -1644,4 +1644,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get authRequestReopenImage => String get authRequestReopenImage =>
'You must authenticate to reopen the image.'; '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.';
} }

View file

@ -1644,4 +1644,17 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get authRequestReopenImage => String get authRequestReopenImage =>
'You must authenticate to reopen the image.'; '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.';
} }

@ -1 +1 @@
Subproject commit 4caaa3d91aaf1ac2f13160ba770a2880c26bd229 Subproject commit 69d295db737253e0c1b68aedc39bf757e8d642e6

View file

@ -1,11 +1,18 @@
syntax = "proto3"; syntax = "proto3";
message SharedContact {
int64 user_id = 1;
bytes public_identity_key = 2;
string display_name = 3;
}
message AdditionalMessageData { message AdditionalMessageData {
enum Type { enum Type {
LINK = 0; LINK = 0;
CONTACTS = 1;
} }
Type type = 1; Type type = 1;
optional string link = 2; optional string link = 2;
repeated SharedContact contacts = 3;
} }

View file

@ -12,6 +12,7 @@
import 'dart:core' as $core; import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
import 'data.pbenum.dart'; import 'data.pbenum.dart';
@ -20,14 +21,96 @@ export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'data.pbenum.dart'; 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<SharedContact> createRepeated() =>
$pb.PbList<SharedContact>();
@$core.pragma('dart2js:noInline')
static SharedContact getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SharedContact>(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 { class AdditionalMessageData extends $pb.GeneratedMessage {
factory AdditionalMessageData({ factory AdditionalMessageData({
AdditionalMessageData_Type? type, AdditionalMessageData_Type? type,
$core.String? link, $core.String? link,
$core.Iterable<SharedContact>? contacts,
}) { }) {
final result = create(); final result = create();
if (type != null) result.type = type; if (type != null) result.type = type;
if (link != null) result.link = link; if (link != null) result.link = link;
if (contacts != null) result.contacts.addAll(contacts);
return result; return result;
} }
@ -49,6 +132,9 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
valueOf: AdditionalMessageData_Type.valueOf, valueOf: AdditionalMessageData_Type.valueOf,
enumValues: AdditionalMessageData_Type.values) enumValues: AdditionalMessageData_Type.values)
..aOS(2, _omitFieldNames ? '' : 'link') ..aOS(2, _omitFieldNames ? '' : 'link')
..pc<SharedContact>(
3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM,
subBuilder: SharedContact.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$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); $core.bool hasLink() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearLink() => $_clearField(2); void clearLink() => $_clearField(2);
@$pb.TagNumber(3)
$pb.PbList<SharedContact> get contacts => $_getList(2);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -17,14 +17,17 @@ import 'package:protobuf/protobuf.dart' as $pb;
class AdditionalMessageData_Type extends $pb.ProtobufEnum { class AdditionalMessageData_Type extends $pb.ProtobufEnum {
static const AdditionalMessageData_Type LINK = static const AdditionalMessageData_Type LINK =
AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK'); AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK');
static const AdditionalMessageData_Type CONTACTS =
AdditionalMessageData_Type._(1, _omitEnumNames ? '' : 'CONTACTS');
static const $core.List<AdditionalMessageData_Type> values = static const $core.List<AdditionalMessageData_Type> values =
<AdditionalMessageData_Type>[ <AdditionalMessageData_Type>[
LINK, LINK,
CONTACTS,
]; ];
static final $core.List<AdditionalMessageData_Type?> _byValue = static final $core.List<AdditionalMessageData_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 0); $pb.ProtobufEnum.$_initByValueList(values, 1);
static AdditionalMessageData_Type? valueOf($core.int value) => static AdditionalMessageData_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -14,6 +14,28 @@ import 'dart:convert' as $convert;
import 'dart:core' as $core; import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data; 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') @$core.Deprecated('Use additionalMessageDataDescriptor instead')
const AdditionalMessageData$json = { const AdditionalMessageData$json = {
'1': 'AdditionalMessageData', '1': 'AdditionalMessageData',
@ -27,6 +49,14 @@ const AdditionalMessageData$json = {
'10': 'type' '10': 'type'
}, },
{'1': 'link', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'link', '17': true}, {'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], '4': [AdditionalMessageData_Type$json],
'8': [ '8': [
@ -39,11 +69,13 @@ const AdditionalMessageData_Type$json = {
'1': 'Type', '1': 'Type',
'2': [ '2': [
{'1': 'LINK', '2': 0}, {'1': 'LINK', '2': 0},
{'1': 'CONTACTS', '2': 1},
], ],
}; };
/// Descriptor for `AdditionalMessageData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AdditionalMessageData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode(
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBASIQCgRUeXBlEggKBExJ' 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
'TksQAEIHCgVfbGluaw=='); 'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzIh4KBFR5cGUSCAoETElOSxAAEgwKCENPTl'
'RBQ1RTEAFCBwoFX2xpbms=');

View file

@ -767,6 +767,106 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage {
void clearQuoteMessageId() => $_clearField(4); 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<EncryptedContent_AdditionalDataMessage> createRepeated() =>
$pb.PbList<EncryptedContent_AdditionalDataMessage>();
@$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 { class EncryptedContent_Reaction extends $pb.GeneratedMessage {
factory EncryptedContent_Reaction({ factory EncryptedContent_Reaction({
$core.String? targetMessageId, $core.String? targetMessageId,
@ -1611,6 +1711,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_GroupUpdate? groupUpdate, EncryptedContent_GroupUpdate? groupUpdate,
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
EncryptedContent_ErrorMessages? errorMessages, EncryptedContent_ErrorMessages? errorMessages,
EncryptedContent_AdditionalDataMessage? additionalDataMessage,
}) { }) {
final result = create(); final result = create();
if (groupId != null) result.groupId = groupId; if (groupId != null) result.groupId = groupId;
@ -1632,6 +1733,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (resendGroupPublicKey != null) if (resendGroupPublicKey != null)
result.resendGroupPublicKey = resendGroupPublicKey; result.resendGroupPublicKey = resendGroupPublicKey;
if (errorMessages != null) result.errorMessages = errorMessages; if (errorMessages != null) result.errorMessages = errorMessages;
if (additionalDataMessage != null)
result.additionalDataMessage = additionalDataMessage;
return result; return result;
} }
@ -1695,6 +1798,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_ErrorMessages>( ..aOM<EncryptedContent_ErrorMessages>(
18, _omitFieldNames ? '' : 'errorMessages', 18, _omitFieldNames ? '' : 'errorMessages',
subBuilder: EncryptedContent_ErrorMessages.create) subBuilder: EncryptedContent_ErrorMessages.create)
..aOM<EncryptedContent_AdditionalDataMessage>(
19, _omitFieldNames ? '' : 'additionalDataMessage',
subBuilder: EncryptedContent_AdditionalDataMessage.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1905,6 +2011,20 @@ class EncryptedContent extends $pb.GeneratedMessage {
void clearErrorMessages() => $_clearField(18); void clearErrorMessages() => $_clearField(18);
@$pb.TagNumber(18) @$pb.TagNumber(18)
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16); 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 = const $core.bool _omitFieldNames =

View file

@ -316,6 +316,16 @@ const EncryptedContent$json = {
'10': 'errorMessages', '10': 'errorMessages',
'17': true '17': true
}, },
{
'1': 'additional_data_message',
'3': 19,
'4': 1,
'5': 11,
'6': '.EncryptedContent.AdditionalDataMessage',
'9': 17,
'10': 'additionalDataMessage',
'17': true
},
], ],
'3': [ '3': [
EncryptedContent_ErrorMessages$json, EncryptedContent_ErrorMessages$json,
@ -324,6 +334,7 @@ const EncryptedContent$json = {
EncryptedContent_ResendGroupPublicKey$json, EncryptedContent_ResendGroupPublicKey$json,
EncryptedContent_GroupUpdate$json, EncryptedContent_GroupUpdate$json,
EncryptedContent_TextMessage$json, EncryptedContent_TextMessage$json,
EncryptedContent_AdditionalDataMessage$json,
EncryptedContent_Reaction$json, EncryptedContent_Reaction$json,
EncryptedContent_MessageUpdate$json, EncryptedContent_MessageUpdate$json,
EncryptedContent_Media$json, EncryptedContent_Media$json,
@ -351,6 +362,7 @@ const EncryptedContent$json = {
{'1': '_groupUpdate'}, {'1': '_groupUpdate'},
{'1': '_resendGroupPublicKey'}, {'1': '_resendGroupPublicKey'},
{'1': '_error_messages'}, {'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') @$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_Reaction$json = { const EncryptedContent_Reaction$json = {
'1': 'Reaction', '1': 'Reaction',
@ -827,63 +861,69 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw' '5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw'
'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY'
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
'4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVk' 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
'X3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk' 'bERhdGFNZXNzYWdliAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX'
'9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVO' 'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf'
'S05PV05fTUVTU0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCH' 'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0'
'N0YXRlS2V5EiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91' '1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT'
'cEpvaW4SJgoOZ3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZE' 'U0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5Ei'
'dyb3VwUHVibGljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlS' 'YKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoO'
'D2dyb3VwQWN0aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZE' 'Z3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibG'
'NvbnRhY3RJZIgBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMK' 'ljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0'
'Im5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTW' 'aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZI'
'Vzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25l' 'gBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0'
'd0dyb3VwTmFtZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVG' 'ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZn'
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE' 'Rlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFt'
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU' 'ZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2'
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa' 'USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEo'
'YgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFA' 'CVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZB'
'oFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNzYWdl' 'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0'
'VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVH' 'aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2'
'lwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEB' 'FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUS'
'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW' 'OwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYX'
'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz' 'RhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRN'
'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF' 'ZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFg'
'9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB' 'oGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIk'
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk' 'LkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3'
'1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa' 'NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNz'
'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh' 'YWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUg'
'gEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz' 'R0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRF'
'dGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg1kb3' 'EAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdG'
'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI' 'V4dBrwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQS'
'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn' 'MAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaX'
'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl' 'NwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vj'
'iAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYW' 'b25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbn'
'dlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxACEgcK' 'RpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlk'
'A0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW' 'GAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG'
'90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5j' '93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmI'
'cnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZG' 'AQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cH'
'F0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVk' 'Rpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNz'
'aWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3' 'YWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUk'
'NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9F' 'VVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtf'
'UlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW' 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG'
'50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVK' '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0'
'RUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3' 'aW9uTm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2Cg'
'J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXBy' 'R0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigK'
'ZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCU' 'D3RhcmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUE'
'gBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIf' 'VORUQQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVl'
'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZE' 'c3QSOQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZV'
'ILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgO' 'IEdHlwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoN'
'Mh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSA' 'Q29udGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VX'
'BSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJS' 'BkYXRlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT'
'CWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SW' 'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3'
'RCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEg' 'BsYXlOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoK'
'ASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdE' 'BlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcG'
'ZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAKC2Zv' 'xheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1'
'cmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaG' 'c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgAS'
'F0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFC' 'gMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUS'
'DgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCg' 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQX'
'pfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4K' 'QaqQEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZs'
'DF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3' 'YXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCm'
'JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2Vz'); 'Jlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNl'
'VXBkYXRlQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3'
'VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2Nv'
'bnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZX'
'lzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VCDgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91'
'cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbmRHcm91cFB1YmxpY0tleUIRCg9fZXJyb3'
'JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdl');

View file

@ -52,7 +52,7 @@ message EncryptedContent {
optional GroupUpdate groupUpdate = 16; optional GroupUpdate groupUpdate = 16;
optional ResendGroupPublicKey resendGroupPublicKey = 17; optional ResendGroupPublicKey resendGroupPublicKey = 17;
optional ErrorMessages error_messages = 18; optional ErrorMessages error_messages = 18;
optional AdditionalDataMessage additional_data_message = 19;
message ErrorMessages { message ErrorMessages {
enum Type { enum Type {
@ -93,6 +93,13 @@ message EncryptedContent {
optional string quoteMessageId = 4; 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 { message Reaction {
string targetMessageId = 1; string targetMessageId = 1;
string emoji = 2; string emoji = 2;

View file

@ -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<void> 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}');
}
}

View file

@ -116,7 +116,7 @@ Future<void> handleMedia(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaFile.mediaId), mediaId: Value(mediaFile.mediaId),
type: const Value(MessageType.media), type: Value(MessageType.media.name),
additionalMessageData: Value.absentIfNull( additionalMessageData: Value.absentIfNull(
media.hasAdditionalMessageData() media.hasAdditionalMessageData()
? Uint8List.fromList(media.additionalMessageData) ? Uint8List.fromList(media.additionalMessageData)

View file

@ -22,7 +22,7 @@ Future<void> handleTextMessage(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
content: Value(textMessage.text), content: Value(textMessage.text),
type: const Value(MessageType.text), type: Value(MessageType.text.name),
quotesMessageId: Value( quotesMessageId: Value(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
), ),

View file

@ -5,7 +5,6 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';

View file

@ -102,7 +102,7 @@ Future<void> insertMediaFileInMessagesTable(
MessagesCompanion( MessagesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaService.mediaFile.mediaId), mediaId: Value(mediaService.mediaFile.mediaId),
type: const Value(MessageType.media), type: Value(MessageType.media.name),
additionalMessageData: additionalMessageData:
Value.absentIfNull(additionalData?.writeToBuffer()), Value.absentIfNull(additionalData?.writeToBuffer()),
), ),

View file

@ -7,14 +7,17 @@ import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.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' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb; as pb;
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.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/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -204,7 +207,7 @@ Future<void> insertAndSendTextMessage(
MessagesCompanion( MessagesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
content: Value(textMessage), content: Value(textMessage),
type: const Value(MessageType.text), type: Value(MessageType.text.name),
quotesMessageId: Value(quotesMessageId), quotesMessageId: Value(quotesMessageId),
), ),
); );
@ -232,6 +235,61 @@ Future<void> insertAndSendTextMessage(
); );
} }
Future<void> insertAndSendContactShareMessage(
String groupId,
List<int> contactsToShare,
) async {
final contacts = <SharedContact>[];
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<void> sendCipherTextToGroup( Future<void> sendCipherTextToGroup(
String groupId, String groupId,
pb.EncryptedContent encryptedContent, { pb.EncryptedContent encryptedContent, {

View file

@ -13,6 +13,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server; as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; 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/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/contact.c2c.dart';
import 'package:twonly/src/services/api/client2client/errors.c2c.dart'; import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; import 'package:twonly/src/services/api/client2client/groups.c2c.dart';
@ -374,6 +375,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null); return (null, null);
} }
if (content.hasAdditionalDataMessage()) {
await handleAdditionalDataMessage(
fromUserId,
content.groupId,
content.additionalDataMessage,
);
return (null, null);
}
if (content.hasTextMessage()) { if (content.hasTextMessage()) {
await handleTextMessage( await handleTextMessage(
fromUserId, fromUserId,

View file

@ -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' import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
hide Message; hide Message;
import 'package:twonly/src/services/api/messages.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';
class Result<T, E> { class Result<T, E> {
Result.error(this.error) : value = null; Result.error(this.error) : value = null;
@ -78,3 +80,23 @@ Future<void> handleMediaError(MediaFile media) async {
), ),
); );
} }
Future<void> 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,
),
),
);
}
}

View file

@ -242,6 +242,11 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
additionalContent = group.groupName; additionalContent = group.groupName;
} }
} }
if (content.hasAdditionalDataMessage()) {
kind = PushKind.text;
}
if (content.hasMedia()) { if (content.hasMedia()) {
switch (content.media.type) { switch (content.media.type) {
case EncryptedContent_Media_Type.REUPLOAD: case EncryptedContent_Media_Type.REUPLOAD:

View file

@ -24,6 +24,16 @@ extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!; AppLocalizations get lang => AppLocalizations.of(this)!;
TwonlyDB get db => Provider.of<TwonlyDB>(this); TwonlyDB get db => Provider.of<TwonlyDB>(this);
ColorScheme get color => Theme.of(this).colorScheme; ColorScheme get color => Theme.of(this).colorScheme;
Future<dynamic> navPush(Widget route) async {
return Navigator.push(
this,
MaterialPageRoute(
builder: (context) {
return route;
},
),
);
}
} }
Future<String?> saveImageToGallery(Uint8List imageBytes) async { Future<String?> saveImageToGallery(Uint8List imageBytes) async {
@ -292,7 +302,7 @@ Color getMessageColorFromType(
) { ) {
Color color; Color color;
if (message.type == MessageType.text) { if (message.type == MessageType.text.name) {
color = Colors.blueAccent; color = Colors.blueAccent;
} else if (mediaFile != null) { } else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) { if (mediaFile.requiresAuthentication) {

View file

@ -4,12 +4,9 @@ import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.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/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/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.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'; import 'package:twonly/src/services/signal/utils.signal.dart';
Future<Uint8List> getProfileQrCodeData() async { Future<Uint8List> getProfileQrCodeData() async {
@ -80,21 +77,5 @@ Future<void> addNewContactFromPublicProfile(PublicProfile profile) async {
), ),
); );
if (added > 0) { if (added > 0) await importSignalContactAndCreateRequest(userdata);
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,
),
),
);
}
}
} }

View file

@ -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/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/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/session.signal.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
@ -110,23 +109,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
), ),
); );
if (added > 0) { if (added > 0) await importSignalContactAndCreateRequest(userdata);
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,
),
),
);
}
}
} }
InputDecoration getInputDecoration(String hintText) { InputDecoration getInputDecoration(String hintText) {
@ -212,7 +195,7 @@ class ContactsListView extends StatelessWidget {
child: IconButton( child: IconButton(
icon: const FaIcon(Icons.archive_outlined, size: 15), icon: const FaIcon(Icons.archive_outlined, size: 15),
onPressed: () async { onPressed: () async {
const update = ContactsCompanion(requested: Value(false)); const update = ContactsCompanion(deletedByUser: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
}, },
), ),

View file

@ -129,10 +129,11 @@ class _UserListItem extends State<GroupListItem> {
_previewMessages = [newLastMessage]; _previewMessages = [newLastMessage];
} }
final msgs = final msgs = _previewMessages
_previewMessages.where((x) => x.type == MessageType.media).toList(); .where((x) => x.type == MessageType.media.name)
.toList();
if (msgs.isNotEmpty && if (msgs.isNotEmpty &&
msgs.first.type == MessageType.media && msgs.first.type == MessageType.media.name &&
!msgs.first.isDeletedFromSender && !msgs.first.isDeletedFromSender &&
msgs.first.senderId != null && msgs.first.senderId != null &&
msgs.first.openedAt == null) { msgs.first.openedAt == null) {
@ -167,8 +168,9 @@ class _UserListItem extends State<GroupListItem> {
} }
if (_hasNonOpenedMediaFile) { if (_hasNonOpenedMediaFile) {
final msgs = final msgs = _previewMessages
_previewMessages.where((x) => x.type == MessageType.media).toList(); .where((x) => x.type == MessageType.media.name)
.toList();
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
if (mediaFile?.type != MediaType.audio) { if (mediaFile?.type != MediaType.audio) {

View file

@ -200,7 +200,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
} }
index += 1; index += 1;
if (msg.type == MessageType.text && if (msg.type != MessageType.media.name &&
msg.senderId != null && msg.senderId != null &&
msg.openedAt == null) { msg.openedAt == null) {
if (openedMessages[msg.senderId!] == null) { if (openedMessages[msg.senderId!] == null) {
@ -209,7 +209,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
openedMessages[msg.senderId!]!.add(msg.messageId); openedMessages[msg.senderId!]!.add(msg.messageId);
} }
if (msg.type == MessageType.media && msg.mediaStored) { if (msg.type == MessageType.media.name && msg.mediaStored) {
storedMediaFiles.add(msg); storedMediaFiles.add(msg);
} }

View file

@ -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<ShareAdditionalView> createState() => _ShareAdditionalViewState();
}
class _ShareAdditionalViewState extends State<ShareAdditionalView> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
Future<void> openShareContactView() async {
final selectedContacts = await context.navPush(
SelectContactsView(
text: SelectedContactViewText(
title: context.lang.shareContactsTitle,
submitButton: (_, __) => context.lang.shareContactsSubmit,
submitIcon: FontAwesomeIcons.shareNodes,
),
),
) as List<int>?;
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,
),
],
),
),
],
),
),
],
),
),
);
}
}

View file

@ -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/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/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_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_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_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/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_actions.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
@ -90,6 +92,49 @@ class _ChatListEntryState extends State<ChatListEntry> {
setState(() {}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final right = widget.message.senderId == null; final right = widget.message.senderId == null;
@ -129,34 +174,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
mediaService: mediaService, mediaService: mediaService,
borderRadius: borderRadius, borderRadius: borderRadius,
scrollToMessage: widget.scrollToMessage, scrollToMessage: widget.scrollToMessage,
child: (widget.message.type == MessageType.text) child: _getChatEntry(borderRadius, reactionsForWidth),
? 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,
),
), ),
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
], ],

View file

@ -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<ChatContactsEntry> createState() => _ChatContactsEntryState();
}
class _ChatContactsEntryState extends State<ChatContactsEntry> {
@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<void> _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<Contact?>(
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<Color>(Colors.white),
),
)
else
const FaIcon(
FontAwesomeIcons.userPlus,
color: Colors.white,
size: 16,
),
],
],
),
),
);
},
);
}
}

View file

@ -117,7 +117,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
return GestureDetector( return GestureDetector(
key: reopenMediaFile, key: reopenMediaFile,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onTap: (widget.message.type == MessageType.media) ? onTap : null, onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
child: SizedBox( child: SizedBox(
width: (widget.minWidth > 150) ? widget.minWidth : 150, width: (widget.minWidth > 150) ? widget.minWidth : 150,
height: (widget.message.mediaStored && height: (widget.message.mediaStored &&

View file

@ -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,
),
);
}
}

View file

@ -85,8 +85,8 @@ double measureTextWidth(
bool combineTextMessageWithNext(Message message, Message? nextMessage) { bool combineTextMessageWithNext(Message message, Message? nextMessage) {
if (nextMessage != null && nextMessage.content != null) { if (nextMessage != null && nextMessage.content != null) {
if (nextMessage.senderId == message.senderId) { if (nextMessage.senderId == message.senderId) {
if (nextMessage.type == MessageType.text && if (nextMessage.type == MessageType.text.name &&
message.type == MessageType.text) { message.type == MessageType.text.name) {
if (!EmojiAnimation.supported(nextMessage.content!)) { if (!EmojiAnimation.supported(nextMessage.content!)) {
final diff = final diff =
nextMessage.createdAt.difference(message.createdAt).inMinutes; nextMessage.createdAt.difference(message.createdAt).inMinutes;

View file

@ -127,7 +127,7 @@ class MessageContextMenu extends StatelessWidget {
), ),
if (!message.isDeletedFromSender && if (!message.isDeletedFromSender &&
message.senderId == null && message.senderId == null &&
message.type == MessageType.text) message.type == MessageType.text.name)
ContextMenuItem( ContextMenuItem(
title: context.lang.edit, title: context.lang.edit,
onTap: () async { onTap: () async {

View file

@ -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/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to.view.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'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
class MessageInput extends StatefulWidget { class MessageInput extends StatefulWidget {
@ -167,6 +168,19 @@ class _MessageInputState extends State<MessageInput> {
} }
} }
Future<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -312,6 +326,20 @@ class _MessageInputState extends State<MessageInput> {
], ],
), ),
), ),
if (_textFieldController.text == '')
IconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
);
},
),
if (_textFieldController.text == '') if (_textFieldController.text == '')
GestureDetector( GestureDetector(
onLongPressMoveUpdate: (details) { onLongPressMoveUpdate: (details) {
@ -452,18 +480,9 @@ class _MessageInputState extends State<MessageInput> {
) )
else else
IconButton( IconButton(
icon: const FaIcon(FontAwesomeIcons.camera), icon: const FaIcon(FontAwesomeIcons.plus),
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
onPressed: () { onPressed: () => _showAdditionalShareModal(context),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
);
},
), ),
], ],
), ),

View file

@ -87,7 +87,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
var text = ''; var text = '';
Widget? textWidget; Widget? textWidget;
textWidget = null; textWidget = null;
final kindsAlreadyShown = HashSet<MessageType>(); final kindsAlreadyShown = HashSet<String>();
var hasLoader = false; var hasLoader = false;
GestureTapCallback? onTap; GestureTapCallback? onTap;
@ -133,7 +133,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
case MessageSendState.received: case MessageSendState.received:
icon = Icon(Icons.square_rounded, size: 14, color: color); icon = Icon(Icons.square_rounded, size: 14, color: color);
text = context.lang.messageSendState_Received; 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) { if (mediaFile.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad; text = context.lang.messageSendState_TapToLoad;
} }
@ -210,7 +210,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
break; break;
} }
if (message.type == MessageType.media) { if (message.type == MessageType.media.name) {
icons.insert(0, icon); icons.insert(0, icon);
} else { } else {
icons.add(icon); icons.add(icon);

View file

@ -169,12 +169,12 @@ class _ResponsePreviewState extends State<ResponsePreview> {
var color = const Color.fromARGB(233, 68, 137, 255); var color = const Color.fromARGB(233, 68, 137, 255);
if (_message != null) { if (_message != null) {
if (_message!.type == MessageType.text) { if (_message!.type == MessageType.text.name) {
if (_message!.content != null) { if (_message!.content != null) {
subtitle = truncateString(_message!.content!); subtitle = truncateString(_message!.content!);
} }
} }
if (_message!.type == MessageType.media && _mediaService != null) { if (_message!.type == MessageType.media.name && _mediaService != null) {
switch (_mediaService!.mediaFile.type) { switch (_mediaService!.mediaFile.type) {
case MediaType.image: case MediaType.image:
subtitle = context.lang.image; subtitle = context.lang.image;

View file

@ -45,6 +45,7 @@ class AdditionalMessageContent extends StatelessWidget {
], ],
), ),
); );
// ignore: no_default_cases
default: default:
} }
// ignore: empty_catches // ignore: empty_catches

View file

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.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/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/groups/group.view.dart';
import 'package:twonly/src/views/public_profile.view.dart';
class ContactView extends StatefulWidget { class ContactView extends StatefulWidget {
const ContactView(this.userId, {super.key}); const ContactView(this.userId, {super.key});
@ -187,14 +188,7 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.shieldHeart, icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle, text: context.lang.contactVerifyNumberTitle,
onTap: () async { onTap: () async {
await Navigator.push( await context.push(Routes.settingsPublicProfile);
context,
MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
setState(() {}); setState(() {});
}, },
), ),

View file

@ -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<int>? alreadySelected;
final int? limit;
@override
State<SelectContactsView> createState() => _SelectAdditionalUsers();
}
class _SelectAdditionalUsers extends State<SelectContactsView> {
List<Contact> contacts = [];
List<Contact> allContacts = [];
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Contact>> contactSub;
final HashSet<int> selectedUsers = HashSet();
late HashSet<int> _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<void> 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,
),
],
),
),
);
}
}