mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42:13 +00:00
notification of the verified user
This commit is contained in:
parent
e1f28e1b87
commit
7d09bd7283
34 changed files with 957 additions and 270 deletions
|
|
@ -12,6 +12,7 @@ import 'package:twonly/core/frb_generated.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/callbacks/callbacks.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
|
|
@ -121,15 +122,25 @@ Future<void> runMigrations() async {
|
|||
if (userService.currentUser.appVersion < 90) {
|
||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
|
||||
await updateUser((u) {
|
||||
u.appVersion = 90;
|
||||
});
|
||||
await updateUser((u) => u.appVersion = 90);
|
||||
}
|
||||
|
||||
if (userService.currentUser.appVersion < 91) {
|
||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||
await makeMigrationToVersion91();
|
||||
await updateUser((u) {
|
||||
u.appVersion = 91;
|
||||
});
|
||||
await updateUser((u) => u.appVersion = 91);
|
||||
}
|
||||
|
||||
if (userService.currentUser.appVersion < 109) {
|
||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
for (final contact in contacts) {
|
||||
if (contact.verified) {
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
contact.userId,
|
||||
VerificationType.migratedFromOldVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
await updateUser((u) => u.appVersion = 109);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
|
|||
|
||||
part 'contacts.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Contacts])
|
||||
@DriftAccessor(tables: [Contacts, KeyVerifications])
|
||||
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -99,6 +99,21 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<(Contact, bool)?> watchContactAndVerificationState(int userid) {
|
||||
final query = (select(contacts)..where((t) => t.userId.equals(userid))).join([
|
||||
leftOuterJoin(
|
||||
keyVerifications,
|
||||
keyVerifications.contactId.equalsExp(contacts.userId),
|
||||
),
|
||||
]);
|
||||
return query
|
||||
.map((row) => (
|
||||
row.readTable(contacts),
|
||||
row.readTableOrNull(keyVerifications) != null,
|
||||
))
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getAllContacts() {
|
||||
return select(contacts).get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ part of 'contacts.dao.dart';
|
|||
// ignore_for_file: type=lint
|
||||
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> {
|
||||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$KeyVerificationsTable get keyVerifications =>
|
||||
attachedDatabase.keyVerifications;
|
||||
ContactsDaoManager get managers => ContactsDaoManager(this);
|
||||
}
|
||||
|
||||
|
|
@ -13,4 +15,9 @@ class ContactsDaoManager {
|
|||
ContactsDaoManager(this._db);
|
||||
$$ContactsTableTableManager get contacts =>
|
||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$KeyVerificationsTableTableManager get keyVerifications =>
|
||||
$$KeyVerificationsTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.keyVerifications,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
81
lib/src/database/daos/key_verification.dao.dart
Normal file
81
lib/src/database/daos/key_verification.dao.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
part 'key_verification.dao.g.dart';
|
||||
|
||||
@DriftAccessor(
|
||||
tables: [Contacts, VerificationTokens, KeyVerifications, GroupMembers],
|
||||
)
|
||||
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||
with _$KeyVerificationDaoMixin {
|
||||
// ignore: matching_super_parameters
|
||||
KeyVerificationDao(super.db);
|
||||
|
||||
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
||||
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
|
||||
return (select(
|
||||
verificationTokens,
|
||||
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
||||
}
|
||||
|
||||
Future<int> insertVerificationToken(Uint8List token) {
|
||||
return into(verificationTokens).insert(
|
||||
VerificationTokensCompanion.insert(token: token),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a map of contactId → the verification type of the earliest
|
||||
/// [KeyVerification] row for that contact.
|
||||
Future<Map<int, VerificationType>>
|
||||
getFirstVerificationTypeByContacts() async {
|
||||
final rows = await (select(
|
||||
keyVerifications,
|
||||
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
|
||||
|
||||
final result = <int, VerificationType>{};
|
||||
for (final row in rows) {
|
||||
result.putIfAbsent(row.contactId, () => row.type);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> isContactVerified(int contactId) async {
|
||||
final row =
|
||||
await (select(keyVerifications)
|
||||
..where((kv) => kv.contactId.equals(contactId))
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row != null;
|
||||
}
|
||||
|
||||
Stream<List<KeyVerification>> watchContactVerification(int contactId) {
|
||||
return (select(
|
||||
keyVerifications,
|
||||
)..where((kv) => kv.contactId.equals(contactId))).watch();
|
||||
}
|
||||
|
||||
Stream<bool> watchAllGroupMembersVerified(String groupId) {
|
||||
final gm = groupMembers;
|
||||
final kv = keyVerifications;
|
||||
|
||||
final query = (select(gm)..where((m) => m.groupId.equals(groupId))).join([
|
||||
leftOuterJoin(kv, kv.contactId.equalsExp(gm.contactId)),
|
||||
]);
|
||||
|
||||
return query.watch().map(
|
||||
(rows) =>
|
||||
rows.isNotEmpty && rows.every((r) => r.readTableOrNull(kv) != null),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addKeyVerification(int contactId, VerificationType type) async {
|
||||
await into(keyVerifications).insertOnConflictUpdate(
|
||||
KeyVerificationsCompanion(
|
||||
contactId: Value(contactId),
|
||||
type: Value(type),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/src/database/daos/key_verification.dao.g.dart
Normal file
36
lib/src/database/daos/key_verification.dao.g.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'key_verification.dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$KeyVerificationDaoMixin on DatabaseAccessor<TwonlyDB> {
|
||||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$VerificationTokensTable get verificationTokens =>
|
||||
attachedDatabase.verificationTokens;
|
||||
$KeyVerificationsTable get keyVerifications =>
|
||||
attachedDatabase.keyVerifications;
|
||||
$GroupsTable get groups => attachedDatabase.groups;
|
||||
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
|
||||
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
|
||||
}
|
||||
|
||||
class KeyVerificationDaoManager {
|
||||
final _$KeyVerificationDaoMixin _db;
|
||||
KeyVerificationDaoManager(this._db);
|
||||
$$ContactsTableTableManager get contacts =>
|
||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$VerificationTokensTableTableManager get verificationTokens =>
|
||||
$$VerificationTokensTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.verificationTokens,
|
||||
);
|
||||
$$KeyVerificationsTableTableManager get keyVerifications =>
|
||||
$$KeyVerificationsTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.keyVerifications,
|
||||
);
|
||||
$$GroupsTableTableManager get groups =>
|
||||
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
|
||||
$$GroupMembersTableTableManager get groupMembers =>
|
||||
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
|
||||
}
|
||||
|
|
@ -38,8 +38,11 @@ class Contacts extends Table {
|
|||
}
|
||||
|
||||
enum VerificationType {
|
||||
qr,
|
||||
migratedFromOldVersion,
|
||||
qrScanned,
|
||||
link,
|
||||
secretQrToken,
|
||||
contactSharedByVerified,
|
||||
}
|
||||
|
||||
@DataClassName('KeyVerification')
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart'
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
|
||||
import 'package:twonly/src/database/daos/messages.dao.dart';
|
||||
import 'package:twonly/src/database/daos/reactions.dao.dart';
|
||||
|
|
@ -60,6 +61,7 @@ part 'twonly.db.g.dart';
|
|||
ReactionsDao,
|
||||
MediaFilesDao,
|
||||
UserDiscoveryDao,
|
||||
KeyVerificationDao,
|
||||
],
|
||||
)
|
||||
class TwonlyDB extends _$TwonlyDB {
|
||||
|
|
|
|||
|
|
@ -11374,6 +11374,9 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
late final UserDiscoveryDao userDiscoveryDao = UserDiscoveryDao(
|
||||
this as TwonlyDB,
|
||||
);
|
||||
late final KeyVerificationDao keyVerificationDao = KeyVerificationDao(
|
||||
this as TwonlyDB,
|
||||
);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
|
|
|
|||
|
|
@ -970,6 +970,42 @@ abstract class AppLocalizations {
|
|||
/// **'Clear verification'**
|
||||
String get contactVerifyNumberClearVerification;
|
||||
|
||||
/// No description provided for @userVerifiedTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'User verified'**
|
||||
String get userVerifiedTitle;
|
||||
|
||||
/// No description provided for @verificationTypeQrScanned.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You scanned their QR code.'**
|
||||
String get verificationTypeQrScanned;
|
||||
|
||||
/// No description provided for @verificationTypeSecretQrToken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The other person scanned your QR code.'**
|
||||
String get verificationTypeSecretQrToken;
|
||||
|
||||
/// No description provided for @verificationTypeLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verified via link.'**
|
||||
String get verificationTypeLink;
|
||||
|
||||
/// No description provided for @verificationTypeContactSharedByVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact received from a verified contact.'**
|
||||
String get verificationTypeContactSharedByVerified;
|
||||
|
||||
/// No description provided for @verificationTypeMigratedFromOldVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Migrated from old version.'**
|
||||
String get verificationTypeMigratedFromOldVersion;
|
||||
|
||||
/// No description provided for @contactVerifyNumberLongDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -482,6 +482,27 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get contactVerifyNumberClearVerification => 'Verifizierung aufheben';
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'Benutzer verifiziert';
|
||||
|
||||
@override
|
||||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||
|
||||
@override
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'Die andere Person hat deinen QR-Code gescannt.';
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||
|
||||
@override
|
||||
String get verificationTypeContactSharedByVerified =>
|
||||
'Von einem verifizierten Kontakt geteilt bekommen.';
|
||||
|
||||
@override
|
||||
String get verificationTypeMigratedFromOldVersion =>
|
||||
'Von alter Version migriert';
|
||||
|
||||
@override
|
||||
String contactVerifyNumberLongDesc(Object username) {
|
||||
return 'Um die Ende-zu-Ende-Verschlüsselung mit $username zu verifizieren, vergleiche die Zahlen mit deren Gerät. Die Person kann auch deinen Code mit deren Gerät scannen.';
|
||||
|
|
|
|||
|
|
@ -477,6 +477,27 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get contactVerifyNumberClearVerification => 'Clear verification';
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'User verified';
|
||||
|
||||
@override
|
||||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'The other person scanned your QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Verified via link.';
|
||||
|
||||
@override
|
||||
String get verificationTypeContactSharedByVerified =>
|
||||
'Contact received from a verified contact.';
|
||||
|
||||
@override
|
||||
String get verificationTypeMigratedFromOldVersion =>
|
||||
'Migrated from old version.';
|
||||
|
||||
@override
|
||||
String contactVerifyNumberLongDesc(Object username) {
|
||||
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';
|
||||
|
|
|
|||
|
|
@ -477,6 +477,27 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get contactVerifyNumberClearVerification => 'Clear verification';
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'User verified';
|
||||
|
||||
@override
|
||||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'The other person scanned your QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Verified via link.';
|
||||
|
||||
@override
|
||||
String get verificationTypeContactSharedByVerified =>
|
||||
'Contact received from a verified contact.';
|
||||
|
||||
@override
|
||||
String get verificationTypeMigratedFromOldVersion =>
|
||||
'Migrated from old version.';
|
||||
|
||||
@override
|
||||
String contactVerifyNumberLongDesc(Object username) {
|
||||
return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 57ec512977e514fca6413622bb4a7e03701f09a0
|
||||
Subproject commit a35f6a65cf87db6e1b48dea1c0260b59b52b21f1
|
||||
|
|
@ -1857,6 +1857,68 @@ class EncryptedContent_UserDiscoveryUpdate extends $pb.GeneratedMessage {
|
|||
$pb.PbList<$core.List<$core.int>> get messages => $_getList(0);
|
||||
}
|
||||
|
||||
class EncryptedContent_KeyVerificationProof extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_KeyVerificationProof({
|
||||
$core.List<$core.int>? calculatedMac,
|
||||
}) {
|
||||
final result = create();
|
||||
if (calculatedMac != null) result.calculatedMac = calculatedMac;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedContent_KeyVerificationProof._();
|
||||
|
||||
factory EncryptedContent_KeyVerificationProof.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedContent_KeyVerificationProof.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedContent.KeyVerificationProof',
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'calculatedMac', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_KeyVerificationProof clone() =>
|
||||
EncryptedContent_KeyVerificationProof()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_KeyVerificationProof copyWith(
|
||||
void Function(EncryptedContent_KeyVerificationProof) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as EncryptedContent_KeyVerificationProof))
|
||||
as EncryptedContent_KeyVerificationProof;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_KeyVerificationProof create() =>
|
||||
EncryptedContent_KeyVerificationProof._();
|
||||
@$core.override
|
||||
EncryptedContent_KeyVerificationProof createEmptyInstance() => create();
|
||||
static $pb.PbList<EncryptedContent_KeyVerificationProof> createRepeated() =>
|
||||
$pb.PbList<EncryptedContent_KeyVerificationProof>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_KeyVerificationProof getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
EncryptedContent_KeyVerificationProof>(create);
|
||||
static EncryptedContent_KeyVerificationProof? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get calculatedMac => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set calculatedMac($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasCalculatedMac() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearCalculatedMac() => $_clearField(1);
|
||||
}
|
||||
|
||||
class EncryptedContent extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent({
|
||||
$core.String? groupId,
|
||||
|
|
@ -1881,6 +1943,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? senderUserDiscoveryVersion,
|
||||
EncryptedContent_UserDiscoveryRequest? userDiscoveryRequest,
|
||||
EncryptedContent_UserDiscoveryUpdate? userDiscoveryUpdate,
|
||||
EncryptedContent_KeyVerificationProof? keyVerificationProof,
|
||||
}) {
|
||||
final result = create();
|
||||
if (groupId != null) result.groupId = groupId;
|
||||
|
|
@ -1911,6 +1974,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
result.userDiscoveryRequest = userDiscoveryRequest;
|
||||
if (userDiscoveryUpdate != null)
|
||||
result.userDiscoveryUpdate = userDiscoveryUpdate;
|
||||
if (keyVerificationProof != null)
|
||||
result.keyVerificationProof = keyVerificationProof;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1979,6 +2044,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
..aOM<EncryptedContent_UserDiscoveryUpdate>(
|
||||
23, _omitFieldNames ? '' : 'userDiscoveryUpdate',
|
||||
subBuilder: EncryptedContent_UserDiscoveryUpdate.create)
|
||||
..aOM<EncryptedContent_KeyVerificationProof>(
|
||||
24, _omitFieldNames ? '' : 'keyVerificationProof',
|
||||
subBuilder: EncryptedContent_KeyVerificationProof.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2251,6 +2319,19 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(23)
|
||||
EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() =>
|
||||
$_ensure(21);
|
||||
|
||||
@$pb.TagNumber(24)
|
||||
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(22);
|
||||
@$pb.TagNumber(24)
|
||||
set keyVerificationProof(EncryptedContent_KeyVerificationProof value) =>
|
||||
$_setField(24, value);
|
||||
@$pb.TagNumber(24)
|
||||
$core.bool hasKeyVerificationProof() => $_has(22);
|
||||
@$pb.TagNumber(24)
|
||||
void clearKeyVerificationProof() => $_clearField(24);
|
||||
@$pb.TagNumber(24)
|
||||
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
|
||||
$_ensure(22);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -365,6 +365,16 @@ const EncryptedContent$json = {
|
|||
'10': 'userDiscoveryUpdate',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'key_verification_proof',
|
||||
'3': 24,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.KeyVerificationProof',
|
||||
'9': 22,
|
||||
'10': 'keyVerificationProof',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
EncryptedContent_ErrorMessages$json,
|
||||
|
|
@ -384,7 +394,8 @@ const EncryptedContent$json = {
|
|||
EncryptedContent_FlameSync$json,
|
||||
EncryptedContent_TypingIndicator$json,
|
||||
EncryptedContent_UserDiscoveryRequest$json,
|
||||
EncryptedContent_UserDiscoveryUpdate$json
|
||||
EncryptedContent_UserDiscoveryUpdate$json,
|
||||
EncryptedContent_KeyVerificationProof$json
|
||||
],
|
||||
'8': [
|
||||
{'1': '_group_id'},
|
||||
|
|
@ -409,6 +420,7 @@ const EncryptedContent$json = {
|
|||
{'1': '_typing_indicator'},
|
||||
{'1': '_user_discovery_request'},
|
||||
{'1': '_user_discovery_update'},
|
||||
{'1': '_key_verification_proof'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -911,6 +923,14 @@ const EncryptedContent_UserDiscoveryUpdate$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_KeyVerificationProof$json = {
|
||||
'1': 'KeyVerificationProof',
|
||||
'2': [
|
||||
{'1': 'calculated_mac', '3': 1, '4': 1, '5': 12, '10': 'calculatedMac'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||
'ChBFbmNyeXB0ZWRDb250ZW50Eh4KCGdyb3VwX2lkGAIgASgJSABSB2dyb3VwSWSIAQESKQoOaX'
|
||||
|
|
@ -941,75 +961,79 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'gTUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEoCzIm'
|
||||
'LkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFFIUdXNlckRpc2NvdmVyeV'
|
||||
'JlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVkQ29u'
|
||||
'dGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBVSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQEa8AEKDU'
|
||||
'Vycm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
|
||||
'YWdlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2'
|
||||
'VpcHRJZCJ3CgRUeXBlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09V'
|
||||
'TlRfUkVRVUVTVF9JTlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU'
|
||||
'9OX09VVF9PRl9TWU5DEAMaVAoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0'
|
||||
'ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRo1CglHcm91cE'
|
||||
'pvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5k'
|
||||
'R3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgAS'
|
||||
'gJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZl'
|
||||
'Y3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbW'
|
||||
'WIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIi'
|
||||
'bmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udG'
|
||||
'FjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJf'
|
||||
'bWlsbGlzZWNvbmRzGq8BCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCV'
|
||||
'IPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgD'
|
||||
'Ugl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZI'
|
||||
'gBAUITChFfcXVvdGVfbWVzc2FnZV9pZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNl'
|
||||
'bmRlcl9tZXNzYWdlX2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgAS'
|
||||
'gDUgl0aW1lc3RhbXASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2Vf'
|
||||
'ZGF0YRgEIAEoDEgAUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbW'
|
||||
'Vzc2FnZV9kYXRhGmQKCFJlYWN0aW9uEioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJn'
|
||||
'ZXRNZXNzYWdlSWQSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3'
|
||||
'ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1l'
|
||||
'c3NhZ2VVcGRhdGUuVHlwZVIEdHlwZRIvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW'
|
||||
'5kZXJNZXNzYWdlSWSIAQESPQobbXVsdGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhht'
|
||||
'dWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbW'
|
||||
'VzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhU'
|
||||
'EAESCgoGT1BFTkVEEAJCFAoSX3NlbmRlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYR'
|
||||
'IqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiAB'
|
||||
'KA4yHC5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF'
|
||||
'9pbl9taWxsaXNlY29uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQES'
|
||||
'NwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb2'
|
||||
'4SHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEo'
|
||||
'CUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG'
|
||||
'9hZFRva2VuiAEBEioKDmVuY3J5cHRpb25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQES'
|
||||
'KgoOZW5jcnlwdGlvbl9tYWMYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW'
|
||||
'9uX25vbmNlGAogASgMSAVSD2VuY3J5cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3Nh'
|
||||
'Z2VfZGF0YRgLIAEoDEgGUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRV'
|
||||
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9k'
|
||||
'aXNwbGF5X2xpbWl0X2luX21pbGxpc2Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG'
|
||||
'93bmxvYWRfdG9rZW5CEQoPX2VuY3J5cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFf'
|
||||
'ZW5jcnlwdGlvbl9ub25jZUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVX'
|
||||
'BkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVS'
|
||||
'BHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeX'
|
||||
'BlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNv'
|
||||
'bnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZX'
|
||||
'F1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUND'
|
||||
'RVBUEAIapAIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbn'
|
||||
'QuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEo'
|
||||
'DEgAUhNhdmF0YXJTdmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW'
|
||||
'1liAEBEiYKDGRpc3BsYXlfbmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsK'
|
||||
'B1JFUVVFU1QQABIKCgZVUERBVEUQAUIYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2'
|
||||
'VybmFtZUIPCg1fZGlzcGxheV9uYW1lGtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5j'
|
||||
'cnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZX'
|
||||
'lJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3Jl'
|
||||
'YXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBg'
|
||||
'oEX2tleUINCgtfY3JlYXRlZF9hdBqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASAB'
|
||||
'KANSDGZsYW1lQ291bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYX'
|
||||
'N0RmxhbWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEK'
|
||||
'DGZvcmNlX3VwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCW'
|
||||
'lzX3R5cGluZxgBIAEoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0'
|
||||
'Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3Vycm'
|
||||
'VudFZlcnNpb24aMQoTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVz'
|
||||
'c2FnZXNCCwoJX2dyb3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbG'
|
||||
'VfY291bnRlckIgCh5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CEQoPX21lc3NhZ2Vf'
|
||||
'dXBkYXRlQggKBl9tZWRpYUIPCg1fbWVkaWFfdXBkYXRlQhEKD19jb250YWN0X3VwZGF0ZUISCh'
|
||||
'BfY29udGFjdF9yZXF1ZXN0Qg0KC19mbGFtZV9zeW5jQgwKCl9wdXNoX2tleXNCCwoJX3JlYWN0'
|
||||
'aW9uQg8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg'
|
||||
'1fZ3JvdXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVz'
|
||||
'c2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQh'
|
||||
'kKF191c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGU=');
|
||||
'dGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBVSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQoWa2'
|
||||
'V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVyaWZp'
|
||||
'Y2F0aW9uUHJvb2ZIFlIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQEa8AEKDUVycm9yTWVzc2FnZX'
|
||||
'MSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUgR0'
|
||||
'eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3CgRUeX'
|
||||
'BlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVUVTVF9J'
|
||||
'TlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9PRl9TWU'
|
||||
'5DEAMaVAoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91'
|
||||
'cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRo1CglHcm91cEpvaW4SKAoQZ3JvdX'
|
||||
'BfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNL'
|
||||
'ZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdG'
|
||||
'lvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJ'
|
||||
'ZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2'
|
||||
'RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVz'
|
||||
'c2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbm'
|
||||
'V3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRz'
|
||||
'Gq8BCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2'
|
||||
'FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXAS'
|
||||
'LQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdG'
|
||||
'VfbWVzc2FnZV9pZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdl'
|
||||
'X2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbX'
|
||||
'ASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgA'
|
||||
'UhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGm'
|
||||
'QKCFJlYWN0aW9uEioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQS'
|
||||
'FAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYW'
|
||||
'dlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUu'
|
||||
'VHlwZVIEdHlwZRIvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSW'
|
||||
'SIAQESPQobbXVsdGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdl'
|
||||
'dE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1'
|
||||
'IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVE'
|
||||
'EAJCFAoSX3NlbmRlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbW'
|
||||
'Vzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0'
|
||||
'ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY2'
|
||||
'9uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNf'
|
||||
'YXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW'
|
||||
'1wGAUgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1l'
|
||||
'c3NhZ2VJZIgBARIqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
|
||||
'oKDmVuY3J5cHRpb25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlv'
|
||||
'bl9tYWMYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogAS'
|
||||
'gMSAVSD2VuY3J5cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEo'
|
||||
'DEgGUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBU'
|
||||
'lNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0'
|
||||
'X2luX21pbGxpc2Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW'
|
||||
'5CEQoPX2VuY3J5cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9u'
|
||||
'b25jZUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cG'
|
||||
'UYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFy'
|
||||
'Z2V0X21lc3NhZ2VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTk'
|
||||
'VEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0'
|
||||
'EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBH'
|
||||
'R5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNv'
|
||||
'bnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZG'
|
||||
'F0ZS5UeXBlUgR0eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT'
|
||||
'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3'
|
||||
'BsYXlfbmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIK'
|
||||
'CgZVUERBVEUQAUIYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZG'
|
||||
'lzcGxheV9uYW1lGtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVu'
|
||||
'dC5QdXNoS2V5cy5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZX'
|
||||
'kYAyABKAxIAVIDa2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8K'
|
||||
'BFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3'
|
||||
'JlYXRlZF9hdBqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291'
|
||||
'bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudG'
|
||||
'VyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0'
|
||||
'ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIA'
|
||||
'EoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNj'
|
||||
'b3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQ'
|
||||
'oTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5'
|
||||
'VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYW'
|
||||
'NCCwoJX2dyb3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbGVfY291'
|
||||
'bnRlckIgCh5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CEQoPX21lc3NhZ2VfdXBkYX'
|
||||
'RlQggKBl9tZWRpYUIPCg1fbWVkaWFfdXBkYXRlQhEKD19jb250YWN0X3VwZGF0ZUISChBfY29u'
|
||||
'dGFjdF9yZXF1ZXN0Qg0KC19mbGFtZV9zeW5jQgwKCl9wdXNoX2tleXNCCwoJX3JlYWN0aW9uQg'
|
||||
'8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg1fZ3Jv'
|
||||
'dXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVzc2FnZX'
|
||||
'NCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQhkKF191'
|
||||
'c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGVCGQoXX2tleV'
|
||||
'92ZXJpZmljYXRpb25fcHJvb2Y=');
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$fixnum.Int64? registrationId,
|
||||
$core.List<$core.int>? signedPrekeySignature,
|
||||
$fixnum.Int64? signedPrekeyId,
|
||||
$core.List<$core.int>? secretVerificationToken,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
|
|
@ -109,6 +110,8 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
if (signedPrekeySignature != null)
|
||||
result.signedPrekeySignature = signedPrekeySignature;
|
||||
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
||||
if (secretVerificationToken != null)
|
||||
result.secretVerificationToken = secretVerificationToken;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +137,8 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
..a<$core.List<$core.int>>(
|
||||
6, _omitFieldNames ? '' : 'signedPrekeySignature', $pb.PbFieldType.OY)
|
||||
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
||||
..a<$core.List<$core.int>>(
|
||||
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -220,6 +225,16 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$core.bool hasSignedPrekeyId() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearSignedPrekeyId() => $_clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.List<$core.int> get secretVerificationToken => $_getN(7);
|
||||
@$pb.TagNumber(8)
|
||||
set secretVerificationToken($core.List<$core.int> value) =>
|
||||
$_setBytes(7, value);
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasSecretVerificationToken() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearSecretVerificationToken() => $_clearField(8);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -67,6 +67,18 @@ const PublicProfile$json = {
|
|||
'10': 'signedPrekeySignature'
|
||||
},
|
||||
{'1': 'signed_prekey_id', '3': 7, '4': 1, '5': 3, '10': 'signedPrekeyId'},
|
||||
{
|
||||
'1': 'secret_verification_token',
|
||||
'3': 8,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'secretVerificationToken',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_secret_verification_token'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -77,4 +89,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
|
|||
'dHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EicKD3JlZ2lzdHJhdG'
|
||||
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
||||
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
||||
'5zaWduZWRQcmVrZXlJZA==');
|
||||
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ message EncryptedContent {
|
|||
optional TypingIndicator typing_indicator = 20;
|
||||
optional UserDiscoveryRequest user_discovery_request = 22;
|
||||
optional UserDiscoveryUpdate user_discovery_update = 23;
|
||||
optional KeyVerificationProof key_verification_proof = 24;
|
||||
|
||||
message ErrorMessages {
|
||||
enum Type {
|
||||
|
|
@ -209,4 +210,8 @@ message EncryptedContent {
|
|||
repeated bytes messages = 1;
|
||||
}
|
||||
|
||||
message KeyVerificationProof {
|
||||
bytes calculated_mac = 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,4 +16,5 @@ message PublicProfile {
|
|||
int64 registration_id = 5;
|
||||
bytes signed_prekey_signature = 6;
|
||||
int64 signed_prekey_id = 7;
|
||||
optional bytes secret_verification_token = 8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
|
|||
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/services/key_verification.service.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
|
@ -330,6 +331,14 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasKeyVerificationProof()) {
|
||||
await KeyVerificationService.handleVerificationProof(
|
||||
fromUserId,
|
||||
content.keyVerificationProof.calculatedMac,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasMediaUpdate()) {
|
||||
await handleMediaUpdate(
|
||||
fromUserId,
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ import 'dart:io';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -70,7 +69,6 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
return true;
|
||||
}
|
||||
if (storedPublicKey.equals(receivedPublicKey)) {
|
||||
if (!contact.verified) {
|
||||
final markAsVerified = await showAlertDialog(
|
||||
context,
|
||||
context.lang.linkFromUsername(contact.username),
|
||||
|
|
@ -78,14 +76,12 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
customOk: context.lang.gotLinkFromFriend,
|
||||
);
|
||||
if (markAsVerified) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
contact.userId,
|
||||
const ContactsCompanion(
|
||||
verified: Value(true),
|
||||
),
|
||||
VerificationType.link,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
await context.push(Routes.profileContact(contact.userId));
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
120
lib/src/services/key_verification.service.dart
Normal file
120
lib/src/services/key_verification.service.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class KeyVerificationService {
|
||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||
final token = getRandomUint8List(16);
|
||||
await twonlyDB.keyVerificationDao.insertVerificationToken(token);
|
||||
return token.toList();
|
||||
}
|
||||
|
||||
static Future<void> handleScannedVerificationToken(
|
||||
int contactId,
|
||||
Uint8List contactPubKey,
|
||||
List<int> secretToken,
|
||||
) async {
|
||||
Log.info('Notifying verified user using the secret token');
|
||||
|
||||
final calculatedMac = await _createVerificationBytes(
|
||||
contactId,
|
||||
contactPubKey,
|
||||
secretToken,
|
||||
false,
|
||||
);
|
||||
|
||||
await sendCipherText(
|
||||
contactId,
|
||||
pb.EncryptedContent(
|
||||
keyVerificationProof: pb.EncryptedContent_KeyVerificationProof(
|
||||
calculatedMac: calculatedMac,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> handleVerificationProof(
|
||||
int fromUserId,
|
||||
List<int> receivedMac,
|
||||
) async {
|
||||
Log.info('Received a verification proof. Verifying the calculated mac...');
|
||||
|
||||
final contactPubKey = await getPublicKeyFromContact(fromUserId);
|
||||
if (contactPubKey == null) {
|
||||
Log.error('No public key stored..');
|
||||
return;
|
||||
}
|
||||
|
||||
final secretTokens = await twonlyDB.keyVerificationDao
|
||||
.getRecentVerificationTokens();
|
||||
for (final secretToken in secretTokens) {
|
||||
final recalculatedMac = await _createVerificationBytes(
|
||||
fromUserId,
|
||||
contactPubKey,
|
||||
secretToken.token,
|
||||
true,
|
||||
);
|
||||
if (recalculatedMac.equals(receivedMac)) {
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
fromUserId,
|
||||
VerificationType.secretQrToken,
|
||||
);
|
||||
Log.info('Contact was verified via secretQrToken');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.error('No valid secret token could be found...');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> _createVerificationBytes(
|
||||
int contactId,
|
||||
Uint8List contactPubKey,
|
||||
List<int> secretToken,
|
||||
bool verifying,
|
||||
) async {
|
||||
final bytes = <int>[];
|
||||
|
||||
final userPublicKey = await getUserPublicKey();
|
||||
|
||||
final ownBytes = [
|
||||
..._userIdToLeBytes(userService.currentUser.userId),
|
||||
...userPublicKey,
|
||||
];
|
||||
|
||||
final contactBytes = [..._userIdToLeBytes(contactId), ...contactPubKey];
|
||||
|
||||
if (verifying) {
|
||||
bytes
|
||||
..addAll(ownBytes)
|
||||
..addAll(contactBytes);
|
||||
} else {
|
||||
bytes
|
||||
..addAll(contactBytes)
|
||||
..addAll(ownBytes);
|
||||
}
|
||||
|
||||
final hmac = Hmac.sha256();
|
||||
final mac = await hmac.calculateMac(
|
||||
Uint8List.fromList(bytes),
|
||||
secretKey: SecretKey(secretToken),
|
||||
);
|
||||
|
||||
return mac.bytes;
|
||||
}
|
||||
|
||||
List<int> _userIdToLeBytes(int userId) {
|
||||
final byteData = ByteData(8)..setInt64(0, userId, Endian.little);
|
||||
return byteData.buffer.asUint8List();
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
|
@ -91,6 +92,12 @@ Future<SignalIdentity?> getSignalIdentity() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getUserPublicKey() async {
|
||||
final signalIdentity = (await getSignalIdentity())!;
|
||||
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
|
||||
return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize();
|
||||
}
|
||||
|
||||
Future<void> createIfNotExistsSignalIdentity() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -7,9 +6,9 @@ import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/qr.dart';
|
||||
|
||||
class UserDiscoveryService {
|
||||
static Future<void> checkForNewAnnouncedUsers() async {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -43,11 +44,28 @@ Future<void> handleUserStudyUpload() async {
|
|||
}
|
||||
|
||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
final verifications = await twonlyDB.keyVerificationDao
|
||||
.getFirstVerificationTypeByContacts();
|
||||
|
||||
final dataCollection = {
|
||||
'total_contacts': contacts.length,
|
||||
'accepted_contacts': contacts.where((c) => c.accepted).length,
|
||||
'verified_contacts': contacts.where((c) => c.verified).length,
|
||||
'verified_contacts': verifications.length,
|
||||
'verified_contacts_via_migrated_from_old_version': verifications.values
|
||||
.where((c) => c == VerificationType.migratedFromOldVersion)
|
||||
.length,
|
||||
'verified_contacts_via_qr_scanned': verifications.values
|
||||
.where((c) => c == VerificationType.qrScanned)
|
||||
.length,
|
||||
'verified_contacts_via_link': verifications.values
|
||||
.where((c) => c == VerificationType.link)
|
||||
.length,
|
||||
'verified_contacts_via_secret_qr_token': verifications.values
|
||||
.where((c) => c == VerificationType.secretQrToken)
|
||||
.length,
|
||||
'verified_contacts_via_contact_shared_by_verified': verifications.values
|
||||
.where((c) => c == VerificationType.contactSharedByVerified)
|
||||
.length,
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
|
|
|
|||
|
|
@ -1,21 +1,35 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart' show ListExtensions;
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/key_verification.service.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/utils/log.dart';
|
||||
|
||||
Future<Uint8List> getProfileQrCodeData() async {
|
||||
class QrCodeUtils {
|
||||
static String linkPrefix = 'https://me.twonly.eu/qr/#';
|
||||
|
||||
static Future<String> publicProfileLink() async {
|
||||
final signalIdentity = (await getSignalIdentity())!;
|
||||
|
||||
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
|
||||
|
||||
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
|
||||
|
||||
final secretVerificationToken =
|
||||
await KeyVerificationService.getNewSecretVerificationToken();
|
||||
|
||||
final publicProfile = PublicProfile(
|
||||
userId: Int64(userService.currentUser.userId),
|
||||
username: userService.currentUser.username,
|
||||
|
|
@ -26,6 +40,7 @@ Future<Uint8List> getProfileQrCodeData() async {
|
|||
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
|
||||
signedPrekeySignature: signedPreKey.signature,
|
||||
signedPrekeyId: Int64(signedPreKey.id),
|
||||
secretVerificationToken: secretVerificationToken,
|
||||
);
|
||||
|
||||
final data = publicProfile.writeToBuffer();
|
||||
|
|
@ -35,27 +50,67 @@ Future<Uint8List> getProfileQrCodeData() async {
|
|||
data: data,
|
||||
);
|
||||
|
||||
return qrEnvelope.writeToBuffer();
|
||||
final bytes = qrEnvelope.writeToBuffer();
|
||||
final urlSafeBase64 = base64Url.encode(bytes);
|
||||
|
||||
return '$linkPrefix$urlSafeBase64';
|
||||
}
|
||||
|
||||
Future<Uint8List> getUserPublicKey() async {
|
||||
final signalIdentity = (await getSignalIdentity())!;
|
||||
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
|
||||
return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize();
|
||||
}
|
||||
// returns: profile, NEW_USER=true/VERIFIED_USER=false, VERIFICATION_OK
|
||||
static Future<(PublicProfile, Contact?, bool)?> handleQrCodeLink(
|
||||
String link,
|
||||
) async {
|
||||
late PublicProfile profile;
|
||||
|
||||
PublicProfile? parseQrCodeData(Uint8List rawBytes) {
|
||||
try {
|
||||
final envelop = QREnvelope.fromBuffer(rawBytes);
|
||||
if (envelop.type == QREnvelope_Type.PUBLIC_PROFILE) {
|
||||
return PublicProfile.fromBuffer(envelop.data);
|
||||
}
|
||||
final bytes = base64Url.decode(link.replaceFirst(linkPrefix, ''));
|
||||
final envelope = QREnvelope.fromBuffer(bytes);
|
||||
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) return null;
|
||||
profile = PublicProfile.fromBuffer(envelope.data);
|
||||
} catch (e) {
|
||||
// Log.warn(e);
|
||||
}
|
||||
Log.error(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
profile.userId.toInt(),
|
||||
);
|
||||
|
||||
if (contact == null || !contact.accepted) {
|
||||
if (profile.username == userService.currentUser.username) {
|
||||
return null;
|
||||
}
|
||||
// NEW_USER
|
||||
return (profile, null, false);
|
||||
}
|
||||
|
||||
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
|
||||
if (storedPublicKey == null) return null;
|
||||
|
||||
final verificationOk = profile.publicIdentityKey.equals(
|
||||
storedPublicKey.toList(),
|
||||
);
|
||||
|
||||
if (verificationOk) {
|
||||
if (profile.hasSecretVerificationToken()) {
|
||||
unawaited(
|
||||
KeyVerificationService.handleScannedVerificationToken(
|
||||
contact.userId,
|
||||
storedPublicKey,
|
||||
profile.secretVerificationToken,
|
||||
),
|
||||
);
|
||||
}
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
contact.userId,
|
||||
VerificationType.qrScanned,
|
||||
);
|
||||
}
|
||||
|
||||
return (profile, contact, verificationOk);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
|
||||
final userdata = Response_UserData(
|
||||
userId: profile.userId,
|
||||
|
|
@ -72,14 +127,28 @@ Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
verified: const Value(
|
||||
true,
|
||||
), // This contact was added from a QR-Code scan, so the public key was not loaded from the server
|
||||
),
|
||||
);
|
||||
|
||||
// The user was added via the profile scanned from the QR code so the scanned public key was used.
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
profile.userId.toInt(),
|
||||
VerificationType.qrScanned,
|
||||
);
|
||||
|
||||
if (added > 0) {
|
||||
return importSignalContactAndCreateRequest(userdata);
|
||||
if (await importSignalContactAndCreateRequest(userdata)) {
|
||||
if (profile.hasSecretVerificationToken()) {
|
||||
await KeyVerificationService.handleScannedVerificationToken(
|
||||
profile.userId.toInt(),
|
||||
Uint8List.fromList(profile.publicIdentityKey),
|
||||
profile.secretVerificationToken,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,27 +28,29 @@ class VerificationBadgeComp extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||
bool isVerified = false;
|
||||
Contact? contact;
|
||||
bool _isVerified = false;
|
||||
|
||||
StreamSubscription<List<Contact>>? stream;
|
||||
StreamSubscription<bool>? _streamAllVerified;
|
||||
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.group != null) {
|
||||
stream = twonlyDB.groupsDao
|
||||
.watchGroupContact(widget.group!.groupId)
|
||||
.listen((contacts) {
|
||||
if (contacts.length == 1) {
|
||||
contact = contacts.first;
|
||||
}
|
||||
_streamAllVerified = twonlyDB.keyVerificationDao
|
||||
.watchAllGroupMembersVerified(widget.group!.groupId)
|
||||
.listen((update) {
|
||||
setState(() {
|
||||
isVerified = contacts.every((t) => t.verified);
|
||||
_isVerified = update;
|
||||
});
|
||||
});
|
||||
} else if (widget.contact != null) {
|
||||
isVerified = widget.contact!.verified;
|
||||
contact = widget.contact;
|
||||
_streamContactVerification = twonlyDB.keyVerificationDao
|
||||
.watchContactVerification(widget.contact!.userId)
|
||||
.listen((update) {
|
||||
setState(() {
|
||||
_isVerified = update.isNotEmpty;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
|
|
@ -56,15 +58,16 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
stream?.cancel();
|
||||
_streamAllVerified?.cancel();
|
||||
_streamContactVerification?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isVerified && widget.showOnlyIfVerified) return Container();
|
||||
if (!_isVerified && widget.showOnlyIfVerified) return Container();
|
||||
return GestureDetector(
|
||||
onTap: (contact == null || !widget.clickable)
|
||||
onTap: (!widget.clickable)
|
||||
? null
|
||||
: () => context.push(Routes.settingsHelpFaqVerifyBadge),
|
||||
child: ColoredBox(
|
||||
|
|
@ -77,7 +80,7 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
|||
bottom: 3,
|
||||
),
|
||||
child: SvgIcon(
|
||||
assetPath: isVerified
|
||||
assetPath: _isVerified
|
||||
? SvgIcons.verifiedGreen
|
||||
: SvgIcons.verifiedRed,
|
||||
size: widget.size,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import 'dart:io';
|
|||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -16,7 +14,6 @@ import 'package:twonly/locator.dart';
|
|||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.dart';
|
||||
|
|
@ -48,6 +45,7 @@ class MainCameraController {
|
|||
bool initCameraStarted = true;
|
||||
Map<int, ScannedVerifiedContact> contactsVerified = {};
|
||||
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
||||
Set<String> _handledProfileLinks = {};
|
||||
String? scannedUrl;
|
||||
GlobalKey zoomButtonKey = GlobalKey();
|
||||
GlobalKey cameraPreviewKey = GlobalKey();
|
||||
|
|
@ -340,45 +338,33 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
for (final barcode in barcodes) {
|
||||
if (barcode.displayValue != null) {
|
||||
if (barcode.displayValue!.startsWith('http://') ||
|
||||
barcode.displayValue!.startsWith('https://')) {
|
||||
scannedUrl = barcode.displayValue;
|
||||
if (sharedLinkForPreview == null) {
|
||||
timeSharedLinkWasSetWithQr = clock.now();
|
||||
setSharedLinkForPreview(Uri.parse(scannedUrl!));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (barcode.rawBytes == null) continue;
|
||||
if (barcode.displayValue == null) continue;
|
||||
final link = barcode.displayValue!;
|
||||
|
||||
final profile = parseQrCodeData(barcode.rawBytes!);
|
||||
if (link.startsWith(QrCodeUtils.linkPrefix)) {
|
||||
if (_handledProfileLinks.contains(link)) continue;
|
||||
_handledProfileLinks.add(link);
|
||||
|
||||
if (profile == null) continue;
|
||||
final res = await QrCodeUtils.handleQrCodeLink(link);
|
||||
if (res == null) continue;
|
||||
final (profile, contact, verificationOk) = res;
|
||||
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
profile.userId.toInt(),
|
||||
if (contact == null) {
|
||||
if (scannedNewProfiles[profile.userId.toInt()] == null) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contact != null && contact.accepted) {
|
||||
if (contactsVerified[contact.userId] == null) {
|
||||
final storedPublicKey = await getPublicKeyFromContact(
|
||||
contact.userId,
|
||||
);
|
||||
if (storedPublicKey != null) {
|
||||
final verificationOk = profile.publicIdentityKey.equals(
|
||||
storedPublicKey.toList(),
|
||||
);
|
||||
contactsVerified[contact.userId] = ScannedVerifiedContact(
|
||||
contact: contact,
|
||||
verificationOk: verificationOk,
|
||||
);
|
||||
if (verificationOk) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
const ContactsCompanion(verified: Value(true)),
|
||||
);
|
||||
}
|
||||
|
||||
await HapticFeedback.heavyImpact();
|
||||
if (verificationOk) {
|
||||
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar(
|
||||
|
|
@ -396,14 +382,12 @@ class MainCameraController {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (profile.username != userService.currentUser.username) {
|
||||
if (scannedNewProfiles[profile.userId.toInt()] == null) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
|
||||
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||
scannedUrl = link;
|
||||
if (sharedLinkForPreview == null) {
|
||||
timeSharedLinkWasSetWithQr = clock.now();
|
||||
setSharedLinkForPreview(Uri.parse(scannedUrl!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -130,13 +131,26 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
verified: Value(
|
||||
!(widget.publicKey == null) &&
|
||||
userdata.publicIdentityKey.equals(widget.publicKey!),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.publicKey != null &&
|
||||
mounted &&
|
||||
widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||
final markAsVerified = await showAlertDialog(
|
||||
context,
|
||||
context.lang.linkFromUsername(username),
|
||||
context.lang.linkFromUsernameLong,
|
||||
customOk: context.lang.gotLinkFromFriend,
|
||||
);
|
||||
if (markAsVerified) {
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
userdata.userId.toInt(),
|
||||
VerificationType.link,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.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.api.dart';
|
||||
|
|
@ -120,13 +122,18 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
);
|
||||
if (userdata == null) return;
|
||||
|
||||
var verified = false;
|
||||
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
|
||||
final sender = await twonlyDB.contactsDao.getContactById(
|
||||
if (userdata.publicIdentityKey.equals(widget.contact.publicIdentityKey)) {
|
||||
final verified = await twonlyDB.keyVerificationDao.isContactVerified(
|
||||
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;
|
||||
|
||||
if (verified) {
|
||||
Log.info('Verified a user which was shared by a verified contact');
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
userdata.userId.toInt(),
|
||||
VerificationType.contactSharedByVerified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||
|
|
@ -136,9 +143,6 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
verified: Value(
|
||||
verified,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:no_screenshot/no_screenshot.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
|
|
@ -103,12 +104,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
final Mutex _messageUpdateLock = Mutex();
|
||||
|
||||
Future<void> asyncLoadNextMedia(bool firstRun) async {
|
||||
final messages = twonlyDB.messagesDao.watchMediaNotOpened(
|
||||
widget.group.groupId,
|
||||
);
|
||||
|
||||
_subscription = messages.listen((messages) async {
|
||||
await _messageUpdateLock.protect(() async {
|
||||
for (final msg in messages) {
|
||||
if (_alreadyOpenedMediaIds.contains(msg.mediaId)) {
|
||||
continue;
|
||||
|
|
@ -141,6 +145,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
await loadCurrentMediaFile();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> nextMediaOrExit() async {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
|
|
@ -30,24 +33,38 @@ class ContactView extends StatefulWidget {
|
|||
class _ContactViewState extends State<ContactView> {
|
||||
Contact? _contact;
|
||||
List<GroupMember> _memberOfGroups = [];
|
||||
List<KeyVerification> _keyVerifications = [];
|
||||
|
||||
late StreamSubscription<Contact?> _contactSub;
|
||||
late StreamSubscription<(Contact, bool)?> _contactSub;
|
||||
late StreamSubscription<List<GroupMember>> _groupMemberSub;
|
||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
|
||||
_contactSub = twonlyDB.contactsDao
|
||||
.watchContactAndVerificationState(widget.userId)
|
||||
.listen((
|
||||
update,
|
||||
) {
|
||||
if (update != null) {
|
||||
setState(() {
|
||||
_contact = update;
|
||||
_contact = update.$1;
|
||||
});
|
||||
}
|
||||
});
|
||||
_groupMemberSub = twonlyDB.groupsDao
|
||||
.watchContactGroupMember(widget.userId)
|
||||
.listen((groups) async {
|
||||
_memberOfGroups = groups;
|
||||
});
|
||||
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
||||
.watchContactVerification(widget.userId)
|
||||
.listen((update) {
|
||||
setState(() {
|
||||
Log.info('Verifications: ${update.length}');
|
||||
_keyVerifications = update;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +72,7 @@ class _ContactViewState extends State<ContactView> {
|
|||
void dispose() {
|
||||
_contactSub.cancel();
|
||||
_groupMemberSub.cancel();
|
||||
_streamKeyVerifications.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +232,7 @@ class _ContactViewState extends State<ContactView> {
|
|||
RestoreFlameComp(
|
||||
contactId: widget.userId,
|
||||
),
|
||||
if (!contact.verified)
|
||||
if (_keyVerifications.isEmpty)
|
||||
BetterListTile(
|
||||
leading: VerificationBadgeComp(
|
||||
contact: contact,
|
||||
|
|
@ -226,6 +244,37 @@ class _ContactViewState extends State<ContactView> {
|
|||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (_keyVerifications.isNotEmpty)
|
||||
ExpansionTile(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
collapsedShape: const RoundedRectangleBorder(),
|
||||
leading: Padding(
|
||||
padding: EdgeInsetsGeometry.only(left: 12, right: 12),
|
||||
child: VerificationBadgeComp(
|
||||
contact: contact,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(context.lang.userVerifiedTitle),
|
||||
children: _keyVerifications
|
||||
.map(
|
||||
(kv) => ListTile(
|
||||
dense: true,
|
||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
||||
trailing: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(kv.createdAt),
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (userService.currentUser.isUserDiscoveryEnabled)
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.usersViewfinder,
|
||||
|
|
@ -279,6 +328,19 @@ class _ContactViewState extends State<ContactView> {
|
|||
}
|
||||
}
|
||||
|
||||
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
||||
return switch (type) {
|
||||
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
||||
VerificationType.secretQrToken =>
|
||||
context.lang.verificationTypeSecretQrToken,
|
||||
VerificationType.link => context.lang.verificationTypeLink,
|
||||
VerificationType.contactSharedByVerified =>
|
||||
context.lang.verificationTypeContactSharedByVerified,
|
||||
VerificationType.migratedFromOldVersion =>
|
||||
context.lang.verificationTypeMigratedFromOldVersion,
|
||||
};
|
||||
}
|
||||
|
||||
Future<String?> showNicknameChangeDialog(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:qr_flutter/qr_flutter.dart';
|
|||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.dart';
|
||||
|
|
@ -21,7 +22,7 @@ class PublicProfileView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PublicProfileViewState extends State<PublicProfileView> {
|
||||
Uint8List? _qrCode;
|
||||
String? _qrCode;
|
||||
Uint8List? _userAvatar;
|
||||
Uint8List? _publicKey;
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
_qrCode = await getProfileQrCodeData();
|
||||
_qrCode = await QrCodeUtils.publicProfileLink();
|
||||
_userAvatar = await getUserAvatar();
|
||||
_publicKey = await getUserPublicKey();
|
||||
if (mounted) setState(() {});
|
||||
|
|
@ -82,7 +83,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
|
|||
],
|
||||
),
|
||||
child: QrImageView.withQr(
|
||||
qr: QrCode.fromUint8List(
|
||||
qr: QrCode.fromData(
|
||||
data: _qrCode!,
|
||||
errorCorrectLevel: QrErrorCorrectLevel.M,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class _PrivacyViewState extends State<PrivacyView> {
|
|||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Freunde finden'),
|
||||
title: Text(context.lang.userDiscoverySettingsTitle),
|
||||
onTap: () async {
|
||||
await context.push(Routes.settingsPrivacyUserDiscovery);
|
||||
setState(() {});
|
||||
|
|
|
|||
Loading…
Reference in a new issue