mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-25 06:44:07 +00:00
Fix: Shared contacts now correctly show the blue verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
9daf275310
commit
96eddd7480
23 changed files with 15511 additions and 241 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
- New: Promotion of sharing contacts when contact is new to twonly
|
- New: Promotion of sharing contacts when contact is new to twonly
|
||||||
- Improve: Onboarding of new users to the verification badges
|
- Improve: Onboarding of new users to the verification badges
|
||||||
- Improve: Better feedback when a QR code is scanned
|
- Improve: Better feedback when a QR code is scanned
|
||||||
|
- Fix: Shared contacts now correctly show the blue verification badge
|
||||||
- Fix: Suppressed link previews for scanned QR codes
|
- Fix: Suppressed link previews for scanned QR codes
|
||||||
- Fix: Black screen on iOS when a link is clicked
|
- Fix: Black screen on iOS when a link is clicked
|
||||||
- Fix: Fixed size of the typing indicator to prevent the chat from glitching
|
- Fix: Fixed size of the typing indicator to prevent the chat from glitching
|
||||||
|
|
|
||||||
|
|
@ -57,18 +57,70 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isContactVerified(int contactId) async {
|
Future<bool> isContactVerified(int contactId) async {
|
||||||
final row =
|
final verifierKv = alias(keyVerifications, 'verifierKv');
|
||||||
await (select(keyVerifications)
|
final query = select(keyVerifications).join([
|
||||||
..where((kv) => kv.contactId.equals(contactId))
|
leftOuterJoin(
|
||||||
..limit(1))
|
verifierKv,
|
||||||
.getSingleOrNull();
|
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
|
||||||
return row != null;
|
),
|
||||||
|
])..where(keyVerifications.contactId.equals(contactId));
|
||||||
|
|
||||||
|
final rows = await query.get();
|
||||||
|
for (final row in rows) {
|
||||||
|
final kv = row.readTable(keyVerifications);
|
||||||
|
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
|
||||||
|
if (kv.type == VerificationType.contactSharedByVerified) {
|
||||||
|
if (hasVerifierKv) return true;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<KeyVerification>> watchContactVerification(int contactId) {
|
Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
|
||||||
return (select(
|
int contactId,
|
||||||
keyVerifications,
|
) {
|
||||||
)..where((kv) => kv.contactId.equals(contactId))).watch();
|
final verifier = alias(contacts, 'verifier');
|
||||||
|
final verifierKv = alias(keyVerifications, 'verifierKv');
|
||||||
|
final query = select(keyVerifications).join([
|
||||||
|
leftOuterJoin(
|
||||||
|
verifier,
|
||||||
|
verifier.userId.equalsExp(keyVerifications.verifiedBy),
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
verifierKv,
|
||||||
|
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
|
||||||
|
),
|
||||||
|
])..where(keyVerifications.contactId.equals(contactId));
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
final uniqueKvs =
|
||||||
|
<int, (KeyVerification, Contact?, bool isVerifierVerified)>{};
|
||||||
|
|
||||||
|
for (final row in rows) {
|
||||||
|
final kv = row.readTable(keyVerifications);
|
||||||
|
final contact = row.readTableOrNull(verifier);
|
||||||
|
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
|
||||||
|
|
||||||
|
final existing = uniqueKvs[kv.verificationId];
|
||||||
|
if (existing == null || hasVerifierKv) {
|
||||||
|
uniqueKvs[kv.verificationId] = (kv, contact, hasVerifierKv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueKvs.values
|
||||||
|
.where((item) {
|
||||||
|
final kv = item.$1;
|
||||||
|
final isVerifierVerified = item.$3;
|
||||||
|
if (kv.type == VerificationType.contactSharedByVerified) {
|
||||||
|
return isVerifierVerified;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((item) => (item.$1, item.$2))
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<KeyVerification>> getContactVerification(int contactId) async {
|
Future<List<KeyVerification>> getContactVerification(int contactId) async {
|
||||||
|
|
@ -207,12 +259,17 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addKeyVerification(int contactId, VerificationType type) async {
|
Future<void> addKeyVerification(
|
||||||
|
int contactId,
|
||||||
|
VerificationType type, {
|
||||||
|
int? verifiedBy,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
await into(keyVerifications).insertOnConflictUpdate(
|
await into(keyVerifications).insertOnConflictUpdate(
|
||||||
KeyVerificationsCompanion(
|
KeyVerificationsCompanion(
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
type: Value(type),
|
type: Value(type),
|
||||||
|
verifiedBy: Value(verifiedBy),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled) {
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
|
|
|
||||||
3073
lib/src/database/schemas/twonly_db/drift_schema_v19.json
Normal file
3073
lib/src/database/schemas/twonly_db/drift_schema_v19.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -59,6 +59,11 @@ class KeyVerifications extends Table {
|
||||||
onDelete: KeyAction.cascade,
|
onDelete: KeyAction.cascade,
|
||||||
)();
|
)();
|
||||||
TextColumn get type => textEnum<VerificationType>()();
|
TextColumn get type => textEnum<VerificationType>()();
|
||||||
|
IntColumn get verifiedBy => integer().nullable().references(
|
||||||
|
Contacts,
|
||||||
|
#userId,
|
||||||
|
onDelete: KeyAction.cascade,
|
||||||
|
)();
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 18;
|
int get schemaVersion => 19;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
final connection = driftDatabase(
|
final connection = driftDatabase(
|
||||||
|
|
@ -239,6 +239,12 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
schema.contacts.askForFriendPromotions,
|
schema.contacts.askForFriendPromotions,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
from18To19: (m, schema) async {
|
||||||
|
await m.addColumn(
|
||||||
|
schema.keyVerifications,
|
||||||
|
schema.keyVerifications.verifiedBy,
|
||||||
|
);
|
||||||
|
},
|
||||||
)(m, from, to);
|
)(m, from, to);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9766,6 +9766,20 @@ class $KeyVerificationsTable extends KeyVerifications
|
||||||
type: DriftSqlType.string,
|
type: DriftSqlType.string,
|
||||||
requiredDuringInsert: true,
|
requiredDuringInsert: true,
|
||||||
).withConverter<VerificationType>($KeyVerificationsTable.$convertertype);
|
).withConverter<VerificationType>($KeyVerificationsTable.$convertertype);
|
||||||
|
static const VerificationMeta _verifiedByMeta = const VerificationMeta(
|
||||||
|
'verifiedBy',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> verifiedBy = GeneratedColumn<int>(
|
||||||
|
'verified_by',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES contacts (user_id) ON DELETE CASCADE',
|
||||||
|
),
|
||||||
|
);
|
||||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||||
'createdAt',
|
'createdAt',
|
||||||
);
|
);
|
||||||
|
|
@ -9783,6 +9797,7 @@ class $KeyVerificationsTable extends KeyVerifications
|
||||||
verificationId,
|
verificationId,
|
||||||
contactId,
|
contactId,
|
||||||
type,
|
type,
|
||||||
|
verifiedBy,
|
||||||
createdAt,
|
createdAt,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
|
|
@ -9814,6 +9829,12 @@ class $KeyVerificationsTable extends KeyVerifications
|
||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_contactIdMeta);
|
context.missing(_contactIdMeta);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('verified_by')) {
|
||||||
|
context.handle(
|
||||||
|
_verifiedByMeta,
|
||||||
|
verifiedBy.isAcceptableOrUnknown(data['verified_by']!, _verifiedByMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('created_at')) {
|
if (data.containsKey('created_at')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_createdAtMeta,
|
_createdAtMeta,
|
||||||
|
|
@ -9843,6 +9864,10 @@ class $KeyVerificationsTable extends KeyVerifications
|
||||||
data['${effectivePrefix}type'],
|
data['${effectivePrefix}type'],
|
||||||
)!,
|
)!,
|
||||||
),
|
),
|
||||||
|
verifiedBy: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}verified_by'],
|
||||||
|
),
|
||||||
createdAt: attachedDatabase.typeMapping.read(
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}created_at'],
|
data['${effectivePrefix}created_at'],
|
||||||
|
|
@ -9863,11 +9888,13 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
final int verificationId;
|
final int verificationId;
|
||||||
final int contactId;
|
final int contactId;
|
||||||
final VerificationType type;
|
final VerificationType type;
|
||||||
|
final int? verifiedBy;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
const KeyVerification({
|
const KeyVerification({
|
||||||
required this.verificationId,
|
required this.verificationId,
|
||||||
required this.contactId,
|
required this.contactId,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.verifiedBy,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
|
|
@ -9880,6 +9907,9 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
$KeyVerificationsTable.$convertertype.toSql(type),
|
$KeyVerificationsTable.$convertertype.toSql(type),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || verifiedBy != null) {
|
||||||
|
map['verified_by'] = Variable<int>(verifiedBy);
|
||||||
|
}
|
||||||
map['created_at'] = Variable<DateTime>(createdAt);
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
@ -9889,6 +9919,9 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
verificationId: Value(verificationId),
|
verificationId: Value(verificationId),
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
type: Value(type),
|
type: Value(type),
|
||||||
|
verifiedBy: verifiedBy == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(verifiedBy),
|
||||||
createdAt: Value(createdAt),
|
createdAt: Value(createdAt),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -9904,6 +9937,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
type: $KeyVerificationsTable.$convertertype.fromJson(
|
type: $KeyVerificationsTable.$convertertype.fromJson(
|
||||||
serializer.fromJson<String>(json['type']),
|
serializer.fromJson<String>(json['type']),
|
||||||
),
|
),
|
||||||
|
verifiedBy: serializer.fromJson<int?>(json['verifiedBy']),
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -9916,6 +9950,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
'type': serializer.toJson<String>(
|
'type': serializer.toJson<String>(
|
||||||
$KeyVerificationsTable.$convertertype.toJson(type),
|
$KeyVerificationsTable.$convertertype.toJson(type),
|
||||||
),
|
),
|
||||||
|
'verifiedBy': serializer.toJson<int?>(verifiedBy),
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -9924,11 +9959,13 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
int? verificationId,
|
int? verificationId,
|
||||||
int? contactId,
|
int? contactId,
|
||||||
VerificationType? type,
|
VerificationType? type,
|
||||||
|
Value<int?> verifiedBy = const Value.absent(),
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
}) => KeyVerification(
|
}) => KeyVerification(
|
||||||
verificationId: verificationId ?? this.verificationId,
|
verificationId: verificationId ?? this.verificationId,
|
||||||
contactId: contactId ?? this.contactId,
|
contactId: contactId ?? this.contactId,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
verifiedBy: verifiedBy.present ? verifiedBy.value : this.verifiedBy,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
);
|
);
|
||||||
KeyVerification copyWithCompanion(KeyVerificationsCompanion data) {
|
KeyVerification copyWithCompanion(KeyVerificationsCompanion data) {
|
||||||
|
|
@ -9938,6 +9975,9 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
: this.verificationId,
|
: this.verificationId,
|
||||||
contactId: data.contactId.present ? data.contactId.value : this.contactId,
|
contactId: data.contactId.present ? data.contactId.value : this.contactId,
|
||||||
type: data.type.present ? data.type.value : this.type,
|
type: data.type.present ? data.type.value : this.type,
|
||||||
|
verifiedBy: data.verifiedBy.present
|
||||||
|
? data.verifiedBy.value
|
||||||
|
: this.verifiedBy,
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -9948,13 +9988,15 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
..write('verificationId: $verificationId, ')
|
..write('verificationId: $verificationId, ')
|
||||||
..write('contactId: $contactId, ')
|
..write('contactId: $contactId, ')
|
||||||
..write('type: $type, ')
|
..write('type: $type, ')
|
||||||
|
..write('verifiedBy: $verifiedBy, ')
|
||||||
..write('createdAt: $createdAt')
|
..write('createdAt: $createdAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(verificationId, contactId, type, createdAt);
|
int get hashCode =>
|
||||||
|
Object.hash(verificationId, contactId, type, verifiedBy, createdAt);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|
@ -9962,6 +10004,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
|
||||||
other.verificationId == this.verificationId &&
|
other.verificationId == this.verificationId &&
|
||||||
other.contactId == this.contactId &&
|
other.contactId == this.contactId &&
|
||||||
other.type == this.type &&
|
other.type == this.type &&
|
||||||
|
other.verifiedBy == this.verifiedBy &&
|
||||||
other.createdAt == this.createdAt);
|
other.createdAt == this.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9969,17 +10012,20 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
|
||||||
final Value<int> verificationId;
|
final Value<int> verificationId;
|
||||||
final Value<int> contactId;
|
final Value<int> contactId;
|
||||||
final Value<VerificationType> type;
|
final Value<VerificationType> type;
|
||||||
|
final Value<int?> verifiedBy;
|
||||||
final Value<DateTime> createdAt;
|
final Value<DateTime> createdAt;
|
||||||
const KeyVerificationsCompanion({
|
const KeyVerificationsCompanion({
|
||||||
this.verificationId = const Value.absent(),
|
this.verificationId = const Value.absent(),
|
||||||
this.contactId = const Value.absent(),
|
this.contactId = const Value.absent(),
|
||||||
this.type = const Value.absent(),
|
this.type = const Value.absent(),
|
||||||
|
this.verifiedBy = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
});
|
});
|
||||||
KeyVerificationsCompanion.insert({
|
KeyVerificationsCompanion.insert({
|
||||||
this.verificationId = const Value.absent(),
|
this.verificationId = const Value.absent(),
|
||||||
required int contactId,
|
required int contactId,
|
||||||
required VerificationType type,
|
required VerificationType type,
|
||||||
|
this.verifiedBy = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
}) : contactId = Value(contactId),
|
}) : contactId = Value(contactId),
|
||||||
type = Value(type);
|
type = Value(type);
|
||||||
|
|
@ -9987,12 +10033,14 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
|
||||||
Expression<int>? verificationId,
|
Expression<int>? verificationId,
|
||||||
Expression<int>? contactId,
|
Expression<int>? contactId,
|
||||||
Expression<String>? type,
|
Expression<String>? type,
|
||||||
|
Expression<int>? verifiedBy,
|
||||||
Expression<DateTime>? createdAt,
|
Expression<DateTime>? createdAt,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (verificationId != null) 'verification_id': verificationId,
|
if (verificationId != null) 'verification_id': verificationId,
|
||||||
if (contactId != null) 'contact_id': contactId,
|
if (contactId != null) 'contact_id': contactId,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
|
if (verifiedBy != null) 'verified_by': verifiedBy,
|
||||||
if (createdAt != null) 'created_at': createdAt,
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -10001,12 +10049,14 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
|
||||||
Value<int>? verificationId,
|
Value<int>? verificationId,
|
||||||
Value<int>? contactId,
|
Value<int>? contactId,
|
||||||
Value<VerificationType>? type,
|
Value<VerificationType>? type,
|
||||||
|
Value<int?>? verifiedBy,
|
||||||
Value<DateTime>? createdAt,
|
Value<DateTime>? createdAt,
|
||||||
}) {
|
}) {
|
||||||
return KeyVerificationsCompanion(
|
return KeyVerificationsCompanion(
|
||||||
verificationId: verificationId ?? this.verificationId,
|
verificationId: verificationId ?? this.verificationId,
|
||||||
contactId: contactId ?? this.contactId,
|
contactId: contactId ?? this.contactId,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
verifiedBy: verifiedBy ?? this.verifiedBy,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -10025,6 +10075,9 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
|
||||||
$KeyVerificationsTable.$convertertype.toSql(type.value),
|
$KeyVerificationsTable.$convertertype.toSql(type.value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (verifiedBy.present) {
|
||||||
|
map['verified_by'] = Variable<int>(verifiedBy.value);
|
||||||
|
}
|
||||||
if (createdAt.present) {
|
if (createdAt.present) {
|
||||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
}
|
}
|
||||||
|
|
@ -10037,6 +10090,7 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
|
||||||
..write('verificationId: $verificationId, ')
|
..write('verificationId: $verificationId, ')
|
||||||
..write('contactId: $contactId, ')
|
..write('contactId: $contactId, ')
|
||||||
..write('type: $type, ')
|
..write('type: $type, ')
|
||||||
|
..write('verifiedBy: $verifiedBy, ')
|
||||||
..write('createdAt: $createdAt')
|
..write('createdAt: $createdAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
@ -12786,6 +12840,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
||||||
),
|
),
|
||||||
result: [TableUpdate('key_verifications', kind: UpdateKind.delete)],
|
result: [TableUpdate('key_verifications', kind: UpdateKind.delete)],
|
||||||
),
|
),
|
||||||
|
WritePropagation(
|
||||||
|
on: TableUpdateQuery.onTableName(
|
||||||
|
'contacts',
|
||||||
|
limitUpdateKind: UpdateKind.delete,
|
||||||
|
),
|
||||||
|
result: [TableUpdate('key_verifications', kind: UpdateKind.delete)],
|
||||||
|
),
|
||||||
WritePropagation(
|
WritePropagation(
|
||||||
on: TableUpdateQuery.onTableName(
|
on: TableUpdateQuery.onTableName(
|
||||||
'user_discovery_announced_users',
|
'user_discovery_announced_users',
|
||||||
|
|
@ -13036,29 +13097,6 @@ final class $$ContactsTableReferences
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static MultiTypedResultKey<$KeyVerificationsTable, List<KeyVerification>>
|
|
||||||
_keyVerificationsRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable(
|
|
||||||
db.keyVerifications,
|
|
||||||
aliasName: $_aliasNameGenerator(
|
|
||||||
db.contacts.userId,
|
|
||||||
db.keyVerifications.contactId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$$KeyVerificationsTableProcessedTableManager get keyVerificationsRefs {
|
|
||||||
final manager =
|
|
||||||
$$KeyVerificationsTableTableManager($_db, $_db.keyVerifications).filter(
|
|
||||||
(f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!),
|
|
||||||
);
|
|
||||||
|
|
||||||
final cache = $_typedResult.readTableOrNull(
|
|
||||||
_keyVerificationsRefsTable($_db),
|
|
||||||
);
|
|
||||||
return ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: cache),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static MultiTypedResultKey<
|
static MultiTypedResultKey<
|
||||||
$UserDiscoveryUserRelationsTable,
|
$UserDiscoveryUserRelationsTable,
|
||||||
List<UserDiscoveryUserRelation>
|
List<UserDiscoveryUserRelation>
|
||||||
|
|
@ -13463,31 +13501,6 @@ class $$ContactsTableFilterComposer
|
||||||
return f(composer);
|
return f(composer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression<bool> keyVerificationsRefs(
|
|
||||||
Expression<bool> Function($$KeyVerificationsTableFilterComposer f) f,
|
|
||||||
) {
|
|
||||||
final $$KeyVerificationsTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.userId,
|
|
||||||
referencedTable: $db.keyVerifications,
|
|
||||||
getReferencedColumn: (t) => t.contactId,
|
|
||||||
builder:
|
|
||||||
(
|
|
||||||
joinBuilder, {
|
|
||||||
$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
}) => $$KeyVerificationsTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: $db.keyVerifications,
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return f(composer);
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression<bool> userDiscoveryUserRelationsRefs(
|
Expression<bool> userDiscoveryUserRelationsRefs(
|
||||||
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
|
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
|
||||||
f,
|
f,
|
||||||
|
|
@ -13965,31 +13978,6 @@ class $$ContactsTableAnnotationComposer
|
||||||
return f(composer);
|
return f(composer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression<T> keyVerificationsRefs<T extends Object>(
|
|
||||||
Expression<T> Function($$KeyVerificationsTableAnnotationComposer a) f,
|
|
||||||
) {
|
|
||||||
final $$KeyVerificationsTableAnnotationComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.userId,
|
|
||||||
referencedTable: $db.keyVerifications,
|
|
||||||
getReferencedColumn: (t) => t.contactId,
|
|
||||||
builder:
|
|
||||||
(
|
|
||||||
joinBuilder, {
|
|
||||||
$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
}) => $$KeyVerificationsTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: $db.keyVerifications,
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return f(composer);
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
|
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
|
||||||
Expression<T> Function(
|
Expression<T> Function(
|
||||||
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
|
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
|
||||||
|
|
@ -14125,7 +14113,6 @@ class $$ContactsTableTableManager
|
||||||
bool receiptsRefs,
|
bool receiptsRefs,
|
||||||
bool messageActionsRefs,
|
bool messageActionsRefs,
|
||||||
bool groupHistoriesRefs,
|
bool groupHistoriesRefs,
|
||||||
bool keyVerificationsRefs,
|
|
||||||
bool userDiscoveryUserRelationsRefs,
|
bool userDiscoveryUserRelationsRefs,
|
||||||
bool userDiscoveryOtherPromotionsRefs,
|
bool userDiscoveryOtherPromotionsRefs,
|
||||||
bool userDiscoveryOwnPromotionsRefs,
|
bool userDiscoveryOwnPromotionsRefs,
|
||||||
|
|
@ -14244,7 +14231,6 @@ class $$ContactsTableTableManager
|
||||||
receiptsRefs = false,
|
receiptsRefs = false,
|
||||||
messageActionsRefs = false,
|
messageActionsRefs = false,
|
||||||
groupHistoriesRefs = false,
|
groupHistoriesRefs = false,
|
||||||
keyVerificationsRefs = false,
|
|
||||||
userDiscoveryUserRelationsRefs = false,
|
userDiscoveryUserRelationsRefs = false,
|
||||||
userDiscoveryOtherPromotionsRefs = false,
|
userDiscoveryOtherPromotionsRefs = false,
|
||||||
userDiscoveryOwnPromotionsRefs = false,
|
userDiscoveryOwnPromotionsRefs = false,
|
||||||
|
|
@ -14260,7 +14246,6 @@ class $$ContactsTableTableManager
|
||||||
if (receiptsRefs) db.receipts,
|
if (receiptsRefs) db.receipts,
|
||||||
if (messageActionsRefs) db.messageActions,
|
if (messageActionsRefs) db.messageActions,
|
||||||
if (groupHistoriesRefs) db.groupHistories,
|
if (groupHistoriesRefs) db.groupHistories,
|
||||||
if (keyVerificationsRefs) db.keyVerifications,
|
|
||||||
if (userDiscoveryUserRelationsRefs)
|
if (userDiscoveryUserRelationsRefs)
|
||||||
db.userDiscoveryUserRelations,
|
db.userDiscoveryUserRelations,
|
||||||
if (userDiscoveryOtherPromotionsRefs)
|
if (userDiscoveryOtherPromotionsRefs)
|
||||||
|
|
@ -14419,27 +14404,6 @@ class $$ContactsTableTableManager
|
||||||
),
|
),
|
||||||
typedResults: items,
|
typedResults: items,
|
||||||
),
|
),
|
||||||
if (keyVerificationsRefs)
|
|
||||||
await $_getPrefetchedData<
|
|
||||||
Contact,
|
|
||||||
$ContactsTable,
|
|
||||||
KeyVerification
|
|
||||||
>(
|
|
||||||
currentTable: table,
|
|
||||||
referencedTable: $$ContactsTableReferences
|
|
||||||
._keyVerificationsRefsTable(db),
|
|
||||||
managerFromTypedResult: (p0) =>
|
|
||||||
$$ContactsTableReferences(
|
|
||||||
db,
|
|
||||||
table,
|
|
||||||
p0,
|
|
||||||
).keyVerificationsRefs,
|
|
||||||
referencedItemsForCurrentItem:
|
|
||||||
(item, referencedItems) => referencedItems.where(
|
|
||||||
(e) => e.contactId == item.userId,
|
|
||||||
),
|
|
||||||
typedResults: items,
|
|
||||||
),
|
|
||||||
if (userDiscoveryUserRelationsRefs)
|
if (userDiscoveryUserRelationsRefs)
|
||||||
await $_getPrefetchedData<
|
await $_getPrefetchedData<
|
||||||
Contact,
|
Contact,
|
||||||
|
|
@ -14552,7 +14516,6 @@ typedef $$ContactsTableProcessedTableManager =
|
||||||
bool receiptsRefs,
|
bool receiptsRefs,
|
||||||
bool messageActionsRefs,
|
bool messageActionsRefs,
|
||||||
bool groupHistoriesRefs,
|
bool groupHistoriesRefs,
|
||||||
bool keyVerificationsRefs,
|
|
||||||
bool userDiscoveryUserRelationsRefs,
|
bool userDiscoveryUserRelationsRefs,
|
||||||
bool userDiscoveryOtherPromotionsRefs,
|
bool userDiscoveryOtherPromotionsRefs,
|
||||||
bool userDiscoveryOwnPromotionsRefs,
|
bool userDiscoveryOwnPromotionsRefs,
|
||||||
|
|
@ -21202,6 +21165,7 @@ typedef $$KeyVerificationsTableCreateCompanionBuilder =
|
||||||
Value<int> verificationId,
|
Value<int> verificationId,
|
||||||
required int contactId,
|
required int contactId,
|
||||||
required VerificationType type,
|
required VerificationType type,
|
||||||
|
Value<int?> verifiedBy,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
});
|
});
|
||||||
typedef $$KeyVerificationsTableUpdateCompanionBuilder =
|
typedef $$KeyVerificationsTableUpdateCompanionBuilder =
|
||||||
|
|
@ -21209,6 +21173,7 @@ typedef $$KeyVerificationsTableUpdateCompanionBuilder =
|
||||||
Value<int> verificationId,
|
Value<int> verificationId,
|
||||||
Value<int> contactId,
|
Value<int> contactId,
|
||||||
Value<VerificationType> type,
|
Value<VerificationType> type,
|
||||||
|
Value<int?> verifiedBy,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -21239,6 +21204,28 @@ final class $$KeyVerificationsTableReferences
|
||||||
manager.$state.copyWith(prefetchedData: [item]),
|
manager.$state.copyWith(prefetchedData: [item]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static $ContactsTable _verifiedByTable(_$TwonlyDB db) =>
|
||||||
|
db.contacts.createAlias(
|
||||||
|
$_aliasNameGenerator(
|
||||||
|
db.keyVerifications.verifiedBy,
|
||||||
|
db.contacts.userId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$$ContactsTableProcessedTableManager? get verifiedBy {
|
||||||
|
final $_column = $_itemColumn<int>('verified_by');
|
||||||
|
if ($_column == null) return null;
|
||||||
|
final manager = $$ContactsTableTableManager(
|
||||||
|
$_db,
|
||||||
|
$_db.contacts,
|
||||||
|
).filter((f) => f.userId.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_verifiedByTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$KeyVerificationsTableFilterComposer
|
class $$KeyVerificationsTableFilterComposer
|
||||||
|
|
@ -21288,6 +21275,29 @@ class $$KeyVerificationsTableFilterComposer
|
||||||
);
|
);
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$$ContactsTableFilterComposer get verifiedBy {
|
||||||
|
final $$ContactsTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.verifiedBy,
|
||||||
|
referencedTable: $db.contacts,
|
||||||
|
getReferencedColumn: (t) => t.userId,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => $$ContactsTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: $db.contacts,
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$KeyVerificationsTableOrderingComposer
|
class $$KeyVerificationsTableOrderingComposer
|
||||||
|
|
@ -21336,6 +21346,29 @@ class $$KeyVerificationsTableOrderingComposer
|
||||||
);
|
);
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$$ContactsTableOrderingComposer get verifiedBy {
|
||||||
|
final $$ContactsTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.verifiedBy,
|
||||||
|
referencedTable: $db.contacts,
|
||||||
|
getReferencedColumn: (t) => t.userId,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => $$ContactsTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: $db.contacts,
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$KeyVerificationsTableAnnotationComposer
|
class $$KeyVerificationsTableAnnotationComposer
|
||||||
|
|
@ -21380,6 +21413,29 @@ class $$KeyVerificationsTableAnnotationComposer
|
||||||
);
|
);
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$$ContactsTableAnnotationComposer get verifiedBy {
|
||||||
|
final $$ContactsTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.verifiedBy,
|
||||||
|
referencedTable: $db.contacts,
|
||||||
|
getReferencedColumn: (t) => t.userId,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => $$ContactsTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: $db.contacts,
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$KeyVerificationsTableTableManager
|
class $$KeyVerificationsTableTableManager
|
||||||
|
|
@ -21395,7 +21451,7 @@ class $$KeyVerificationsTableTableManager
|
||||||
$$KeyVerificationsTableUpdateCompanionBuilder,
|
$$KeyVerificationsTableUpdateCompanionBuilder,
|
||||||
(KeyVerification, $$KeyVerificationsTableReferences),
|
(KeyVerification, $$KeyVerificationsTableReferences),
|
||||||
KeyVerification,
|
KeyVerification,
|
||||||
PrefetchHooks Function({bool contactId})
|
PrefetchHooks Function({bool contactId, bool verifiedBy})
|
||||||
> {
|
> {
|
||||||
$$KeyVerificationsTableTableManager(
|
$$KeyVerificationsTableTableManager(
|
||||||
_$TwonlyDB db,
|
_$TwonlyDB db,
|
||||||
|
|
@ -21415,11 +21471,13 @@ class $$KeyVerificationsTableTableManager
|
||||||
Value<int> verificationId = const Value.absent(),
|
Value<int> verificationId = const Value.absent(),
|
||||||
Value<int> contactId = const Value.absent(),
|
Value<int> contactId = const Value.absent(),
|
||||||
Value<VerificationType> type = const Value.absent(),
|
Value<VerificationType> type = const Value.absent(),
|
||||||
|
Value<int?> verifiedBy = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
}) => KeyVerificationsCompanion(
|
}) => KeyVerificationsCompanion(
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
type: type,
|
type: type,
|
||||||
|
verifiedBy: verifiedBy,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
|
|
@ -21427,11 +21485,13 @@ class $$KeyVerificationsTableTableManager
|
||||||
Value<int> verificationId = const Value.absent(),
|
Value<int> verificationId = const Value.absent(),
|
||||||
required int contactId,
|
required int contactId,
|
||||||
required VerificationType type,
|
required VerificationType type,
|
||||||
|
Value<int?> verifiedBy = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
}) => KeyVerificationsCompanion.insert(
|
}) => KeyVerificationsCompanion.insert(
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
type: type,
|
type: type,
|
||||||
|
verifiedBy: verifiedBy,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
|
|
@ -21442,7 +21502,7 @@ class $$KeyVerificationsTableTableManager
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
prefetchHooksCallback: ({contactId = false}) {
|
prefetchHooksCallback: ({contactId = false, verifiedBy = false}) {
|
||||||
return PrefetchHooks(
|
return PrefetchHooks(
|
||||||
db: db,
|
db: db,
|
||||||
explicitlyWatchedTables: [],
|
explicitlyWatchedTables: [],
|
||||||
|
|
@ -21477,6 +21537,21 @@ class $$KeyVerificationsTableTableManager
|
||||||
)
|
)
|
||||||
as T;
|
as T;
|
||||||
}
|
}
|
||||||
|
if (verifiedBy) {
|
||||||
|
state =
|
||||||
|
state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.verifiedBy,
|
||||||
|
referencedTable:
|
||||||
|
$$KeyVerificationsTableReferences
|
||||||
|
._verifiedByTable(db),
|
||||||
|
referencedColumn:
|
||||||
|
$$KeyVerificationsTableReferences
|
||||||
|
._verifiedByTable(db)
|
||||||
|
.userId,
|
||||||
|
)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
@ -21501,7 +21576,7 @@ typedef $$KeyVerificationsTableProcessedTableManager =
|
||||||
$$KeyVerificationsTableUpdateCompanionBuilder,
|
$$KeyVerificationsTableUpdateCompanionBuilder,
|
||||||
(KeyVerification, $$KeyVerificationsTableReferences),
|
(KeyVerification, $$KeyVerificationsTableReferences),
|
||||||
KeyVerification,
|
KeyVerification,
|
||||||
PrefetchHooks Function({bool contactId})
|
PrefetchHooks Function({bool contactId, bool verifiedBy})
|
||||||
>;
|
>;
|
||||||
typedef $$VerificationTokensTableCreateCompanionBuilder =
|
typedef $$VerificationTokensTableCreateCompanionBuilder =
|
||||||
VerificationTokensCompanion Function({
|
VerificationTokensCompanion Function({
|
||||||
|
|
|
||||||
|
|
@ -9524,6 +9524,483 @@ i1.GeneratedColumn<int> _column_247(String aliasedName) =>
|
||||||
type: i1.DriftSqlType.int,
|
type: i1.DriftSqlType.int,
|
||||||
$customConstraints: 'NULL CHECK (ask_for_friend_promotions IN (0, 1))',
|
$customConstraints: 'NULL CHECK (ask_for_friend_promotions IN (0, 1))',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema19 extends i0.VersionedSchema {
|
||||||
|
Schema19({required super.database}) : super(version: 19);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
contacts,
|
||||||
|
groups,
|
||||||
|
mediaFiles,
|
||||||
|
messages,
|
||||||
|
messageHistories,
|
||||||
|
reactions,
|
||||||
|
groupMembers,
|
||||||
|
receipts,
|
||||||
|
receivedReceipts,
|
||||||
|
signalIdentityKeyStores,
|
||||||
|
signalPreKeyStores,
|
||||||
|
signalSenderKeyStores,
|
||||||
|
signalSessionStores,
|
||||||
|
signalSignedPreKeyStores,
|
||||||
|
messageActions,
|
||||||
|
groupHistories,
|
||||||
|
keyVerifications,
|
||||||
|
verificationTokens,
|
||||||
|
userDiscoveryAnnouncedUsers,
|
||||||
|
userDiscoveryUserRelations,
|
||||||
|
userDiscoveryOtherPromotions,
|
||||||
|
userDiscoveryOwnPromotions,
|
||||||
|
userDiscoveryShares,
|
||||||
|
shortcuts,
|
||||||
|
shortcutMembers,
|
||||||
|
];
|
||||||
|
late final Shape53 contacts = Shape53(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'contacts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_106,
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_211,
|
||||||
|
_column_212,
|
||||||
|
_column_247,
|
||||||
|
_column_213,
|
||||||
|
_column_214,
|
||||||
|
_column_215,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape23 groups = Shape23(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'groups',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
_column_130,
|
||||||
|
_column_131,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_118,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
_column_138,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
_column_142,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape51 mediaFiles = Shape51(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'media_files',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(media_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
_column_146,
|
||||||
|
_column_147,
|
||||||
|
_column_148,
|
||||||
|
_column_149,
|
||||||
|
_column_239,
|
||||||
|
_column_240,
|
||||||
|
_column_207,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
_column_153,
|
||||||
|
_column_154,
|
||||||
|
_column_155,
|
||||||
|
_column_156,
|
||||||
|
_column_157,
|
||||||
|
_column_244,
|
||||||
|
_column_245,
|
||||||
|
_column_118,
|
||||||
|
_column_241,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 messages = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_144,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_165,
|
||||||
|
_column_153,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_169,
|
||||||
|
_column_118,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 messageHistories = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_161,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 reactions = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'reactions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
|
||||||
|
columns: [_column_174, _column_176, _column_177, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 groupMembers = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_178,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_209,
|
||||||
|
_column_210,
|
||||||
|
_column_181,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape37 receipts = Shape37(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
_column_208,
|
||||||
|
_column_187,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape30 receivedReceipts = Shape30(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'received_receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [_column_182, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape31 signalIdentityKeyStores = Shape31(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_identity_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_194, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 signalPreKeyStores = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
|
||||||
|
columns: [_column_195, _column_196, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 signalSenderKeyStores = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_sender_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
|
||||||
|
columns: [_column_197, _column_198],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape33 signalSessionStores = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_session_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_199, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 signalSignedPreKeyStores = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_signed_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'],
|
||||||
|
columns: [_column_242, _column_243, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape34 messageActions = Shape34(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_actions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
|
||||||
|
columns: [_column_174, _column_183, _column_144, _column_200],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 groupHistories = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_history_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_201,
|
||||||
|
_column_158,
|
||||||
|
_column_202,
|
||||||
|
_column_203,
|
||||||
|
_column_204,
|
||||||
|
_column_205,
|
||||||
|
_column_206,
|
||||||
|
_column_144,
|
||||||
|
_column_200,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape54 keyVerifications = Shape54(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'key_verifications',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_216,
|
||||||
|
_column_183,
|
||||||
|
_column_144,
|
||||||
|
_column_248,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 verificationTokens = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'verification_tokens',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_217, _column_218, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape52 userDiscoveryAnnouncedUsers = Shape52(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_announced_users',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_219,
|
||||||
|
_column_220,
|
||||||
|
_column_221,
|
||||||
|
_column_222,
|
||||||
|
_column_223,
|
||||||
|
_column_224,
|
||||||
|
_column_246,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 userDiscoveryUserRelations = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_user_relations',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
|
||||||
|
columns: [_column_225, _column_226, _column_227],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 userDiscoveryOtherPromotions = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_other_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_226,
|
||||||
|
_column_228,
|
||||||
|
_column_229,
|
||||||
|
_column_230,
|
||||||
|
_column_231,
|
||||||
|
_column_227,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 userDiscoveryOwnPromotions = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_own_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_232, _column_183, _column_233],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 userDiscoveryShares = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_shares',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_234, _column_235, _column_175],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 shortcuts = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcuts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_173, _column_236, _column_237],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 shortcutMembers = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcut_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'],
|
||||||
|
columns: [_column_238, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape54 extends i0.VersionedTable {
|
||||||
|
Shape54({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get verificationId =>
|
||||||
|
columnsByName['verification_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get contactId =>
|
||||||
|
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get verifiedBy =>
|
||||||
|
columnsByName['verified_by']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_248(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'verified_by',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE',
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
|
@ -9542,6 +10019,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -9630,6 +10108,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from17To18(migrator, schema);
|
await from17To18(migrator, schema);
|
||||||
return 18;
|
return 18;
|
||||||
|
case 18:
|
||||||
|
final schema = Schema19(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from18To19(migrator, schema);
|
||||||
|
return 19;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -9654,6 +10137,7 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
|
@ -9673,5 +10157,6 @@ i1.OnUpgrade stepByStep({
|
||||||
from15To16: from15To16,
|
from15To16: from15To16,
|
||||||
from16To17: from16To17,
|
from16To17: from16To17,
|
||||||
from17To18: from17To18,
|
from17To18: from17To18,
|
||||||
|
from18To19: from18To19,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -914,6 +914,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Verified by {username}'**
|
/// **'Verified by {username}'**
|
||||||
String contactVerifiedBy(Object username);
|
String contactVerifiedBy(Object username);
|
||||||
|
|
||||||
|
/// No description provided for @contactSharedByUnknown.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Shared by a verified contact (username not available)'**
|
||||||
|
String get contactSharedByUnknown;
|
||||||
|
|
||||||
/// No description provided for @verificationTypeQrScanned.
|
/// No description provided for @verificationTypeQrScanned.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return 'Verifiziert von $username';
|
return 'Verifiziert von $username';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactSharedByUnknown =>
|
||||||
|
'Geteilt von einem verifizierten Kontakt (Benutzername nicht verfügbar)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return 'Verified by $username';
|
return 'Verified by $username';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactSharedByUnknown =>
|
||||||
|
'Shared by a verified contact (username not available)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit b508873f4a46cda607d03ee91c66a7907e22bf0a
|
Subproject commit 7686a412d9911bafe7585007b4eb867270e4fde4
|
||||||
|
|
@ -2,8 +2,11 @@ import 'package:clock/clock.dart' show clock;
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.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/data.pb.dart'
|
||||||
|
as pb_data;
|
||||||
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/utils.api.dart';
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
|
import 'package:twonly/src/services/key_verification.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
Future<void> handleAdditionalDataMessage(
|
Future<void> handleAdditionalDataMessage(
|
||||||
|
|
@ -28,6 +31,25 @@ Future<void> handleAdditionalDataMessage(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final additionalData = pb_data.AdditionalMessageData.fromBuffer(
|
||||||
|
message.additionalMessageData,
|
||||||
|
);
|
||||||
|
if (additionalData.type == pb_data.AdditionalMessageData_Type.CONTACTS) {
|
||||||
|
for (final sharedContact in additionalData.contacts) {
|
||||||
|
await KeyVerificationService.verifySharedContact(
|
||||||
|
contactId: sharedContact.userId.toInt(),
|
||||||
|
sharedPublicIdentityKey: sharedContact.publicIdentityKey,
|
||||||
|
senderId: fromUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
|
'Failed to parse additional message data or verify shared contacts: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final msg = await twonlyDB.messagesDao.insertMessage(
|
final msg = await twonlyDB.messagesDao.insertMessage(
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
messageId: Value(message.senderMessageId),
|
messageId: Value(message.senderMessageId),
|
||||||
|
|
@ -46,6 +68,8 @@ Future<void> handleAdditionalDataMessage(
|
||||||
fromTimestamp(message.timestamp),
|
fromTimestamp(message.timestamp),
|
||||||
);
|
);
|
||||||
if (msg != null) {
|
if (msg != null) {
|
||||||
Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}');
|
Log.info(
|
||||||
|
'[$receiptId] Inserted a new text message with ID: ${msg.messageId}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,29 @@ class KeyVerificationService {
|
||||||
|
|
||||||
Log.error('No valid secret token could be found...');
|
Log.error('No valid secret token could be found...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> verifySharedContact({
|
||||||
|
required int contactId,
|
||||||
|
required List<int> sharedPublicIdentityKey,
|
||||||
|
required int senderId,
|
||||||
|
}) async {
|
||||||
|
final publicIdentityKey = await getPublicKeyFromContact(contactId);
|
||||||
|
if (publicIdentityKey == null) {
|
||||||
|
Log.info('No public key stored for contact $contactId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicIdentityKey.equals(sharedPublicIdentityKey)) {
|
||||||
|
Log.info('Verified a user which was shared by a contact');
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||||
|
contactId,
|
||||||
|
VerificationType.contactSharedByVerified,
|
||||||
|
verifiedBy: senderId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.error('Public identity keys do not match for contact $contactId');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<int>> _createVerificationBytes(
|
Future<List<int>> _createVerificationBytes(
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
/// A premium popup dialog shown when a new user profile is scanned via QR code.
|
|
||||||
/// Allows the user to request connection ("Anfragen") or cancel ("Abbrechen").
|
|
||||||
class AddContactDialog extends StatelessWidget {
|
class AddContactDialog extends StatelessWidget {
|
||||||
const AddContactDialog({
|
const AddContactDialog({
|
||||||
required this.profile,
|
required this.username,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PublicProfile profile;
|
final String username;
|
||||||
|
|
||||||
/// Utility method to easily present this dialog.
|
static Future<bool?> show(BuildContext context, String username) {
|
||||||
/// Returns `true` if the user chose to request the contact, `false` otherwise.
|
|
||||||
static Future<bool?> show(BuildContext context, PublicProfile profile) {
|
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => AddContactDialog(profile: profile),
|
builder: (context) => AddContactDialog(username: username),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +43,7 @@ class AddContactDialog extends StatelessWidget {
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
profile.username,
|
username,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
@ -59,7 +54,7 @@ class AddContactDialog extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
context.lang.userFoundBody(profile.username),
|
context.lang.userFoundBody(username),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
|
||||||
|
|
||||||
class UserContextMenu extends StatelessWidget {
|
|
||||||
const UserContextMenu({
|
|
||||||
required this.contact,
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
final Widget child;
|
|
||||||
final Contact contact;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ContextMenu(
|
|
||||||
items: [
|
|
||||||
ContextMenuItem(
|
|
||||||
title: context.lang.contextMenuUserProfile,
|
|
||||||
onTap: () => context.push(Routes.profileContact(contact.userId)),
|
|
||||||
icon: FontAwesomeIcons.user,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
|
|
@ -33,10 +34,17 @@ class VerificationBadgeComp extends StatefulWidget {
|
||||||
|
|
||||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
bool _isVerified = false;
|
bool _isVerified = false;
|
||||||
|
bool _isSharedVerified = false;
|
||||||
int _verifiedByTransferredTrustCount = 0;
|
int _verifiedByTransferredTrustCount = 0;
|
||||||
|
int _sharedByVerifiedCount = 0;
|
||||||
|
int _transferredTrustBaseCount = 0;
|
||||||
|
|
||||||
|
List<(KeyVerification, Contact?)> _keyVerifications = [];
|
||||||
|
List<(Contact, DateTime)> _transferredTrust = [];
|
||||||
|
|
||||||
StreamSubscription<VerificationStatus>? _streamAllVerified;
|
StreamSubscription<VerificationStatus>? _streamAllVerified;
|
||||||
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
StreamSubscription<List<(KeyVerification, Contact?)>>?
|
||||||
|
_streamContactVerification;
|
||||||
StreamSubscription<List<(Contact, DateTime)>>? _streamTransferredTrust;
|
StreamSubscription<List<(Contact, DateTime)>>? _streamTransferredTrust;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -45,6 +53,33 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateVerificationCounts() {
|
||||||
|
setState(() {
|
||||||
|
final sharedVerifications = _keyVerifications
|
||||||
|
.where(
|
||||||
|
(pair) => pair.$1.type == VerificationType.contactSharedByVerified,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_isVerified = _keyVerifications.any(
|
||||||
|
(pair) => pair.$1.type != VerificationType.contactSharedByVerified,
|
||||||
|
);
|
||||||
|
_isSharedVerified = sharedVerifications.isNotEmpty;
|
||||||
|
_sharedByVerifiedCount = sharedVerifications.length;
|
||||||
|
|
||||||
|
final sharedByVerifierIds = sharedVerifications
|
||||||
|
.where((pair) => pair.$1.verifiedBy != null)
|
||||||
|
.map((pair) => pair.$1.verifiedBy!)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
_transferredTrustBaseCount = _transferredTrust
|
||||||
|
.where((tt) => !sharedByVerifierIds.contains(tt.$1.userId))
|
||||||
|
.length;
|
||||||
|
_verifiedByTransferredTrustCount =
|
||||||
|
_sharedByVerifiedCount + _transferredTrustBaseCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
var group = widget.group;
|
var group = widget.group;
|
||||||
var contact = widget.contact;
|
var contact = widget.contact;
|
||||||
|
|
@ -64,6 +99,7 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVerified = false;
|
_isVerified = false;
|
||||||
|
_isSharedVerified = false;
|
||||||
_verifiedByTransferredTrustCount = 0;
|
_verifiedByTransferredTrustCount = 0;
|
||||||
if (update == VerificationStatus.trusted) {
|
if (update == VerificationStatus.trusted) {
|
||||||
_isVerified = true;
|
_isVerified = true;
|
||||||
|
|
@ -78,18 +114,16 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
.watchContactVerification(contact.userId)
|
.watchContactVerification(contact.userId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
_keyVerifications = update;
|
||||||
_isVerified = update.isNotEmpty;
|
_updateVerificationCounts();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||||
.watchTransferredTrustVerifications(contact.userId)
|
.watchTransferredTrustVerifications(contact.userId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
_transferredTrust = update;
|
||||||
_verifiedByTransferredTrustCount = update.length;
|
_updateVerificationCounts();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else if (widget.isVerifiedByTransferredTrust != null) {
|
} else if (widget.isVerifiedByTransferredTrust != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -109,6 +143,7 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_isVerified &&
|
if (!_isVerified &&
|
||||||
|
!_isSharedVerified &&
|
||||||
_verifiedByTransferredTrustCount == 0 &&
|
_verifiedByTransferredTrustCount == 0 &&
|
||||||
widget.showOnlyIfVerified) {
|
widget.showOnlyIfVerified) {
|
||||||
return Container();
|
return Container();
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ class MainCameraController {
|
||||||
unawaited(HapticFeedback.heavyImpact());
|
unawaited(HapticFeedback.heavyImpact());
|
||||||
final shouldRequest = await AddContactDialog.show(
|
final shouldRequest = await AddContactDialog.show(
|
||||||
context,
|
context,
|
||||||
profile,
|
profile.username,
|
||||||
);
|
);
|
||||||
if (shouldRequest == true && context.mounted) {
|
if (shouldRequest == true && context.mounted) {
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
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:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.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/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
import 'package:twonly/src/services/api/utils.api.dart';
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
|
import 'package:twonly/src/services/key_verification.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
|
||||||
|
|
@ -117,9 +117,23 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
);
|
);
|
||||||
if (userdata == null) return;
|
if (userdata == null) return;
|
||||||
|
|
||||||
|
final username = utf8.decode(userdata.username);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final shouldRequest = await AddContactDialog.show(context, username);
|
||||||
|
if (shouldRequest != true) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
|
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||||
ContactsCompanion(
|
ContactsCompanion(
|
||||||
username: Value(utf8.decode(userdata.username)),
|
username: Value(username),
|
||||||
userId: Value(userdata.userId.toInt()),
|
userId: Value(userdata.userId.toInt()),
|
||||||
requested: const Value(false),
|
requested: const Value(false),
|
||||||
blocked: const Value(false),
|
blocked: const Value(false),
|
||||||
|
|
@ -127,19 +141,11 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userdata.publicIdentityKey.equals(widget.contact.publicIdentityKey)) {
|
await KeyVerificationService.verifySharedContact(
|
||||||
final verified = await twonlyDB.keyVerificationDao.isContactVerified(
|
contactId: userdata.userId.toInt(),
|
||||||
widget.message.senderId!,
|
sharedPublicIdentityKey: widget.contact.publicIdentityKey,
|
||||||
);
|
senderId: widget.message.senderId!,
|
||||||
|
);
|
||||||
if (verified) {
|
|
||||||
Log.info('Verified a user which was shared by a verified contact');
|
|
||||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
|
||||||
userdata.userId.toInt(),
|
|
||||||
VerificationType.contactSharedByVerified,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -170,7 +176,6 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
const FaIcon(
|
||||||
FontAwesomeIcons.user,
|
FontAwesomeIcons.user,
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ class VerificationExpansionTileComp extends StatefulWidget {
|
||||||
|
|
||||||
class _VerificationExpansionTileCompState
|
class _VerificationExpansionTileCompState
|
||||||
extends State<VerificationExpansionTileComp> {
|
extends State<VerificationExpansionTileComp> {
|
||||||
List<KeyVerification> _keyVerifications = [];
|
List<(KeyVerification, Contact?)> _keyVerifications = [];
|
||||||
List<(Contact, DateTime)> _transferredTrust = [];
|
List<(Contact, DateTime)> _transferredTrust = [];
|
||||||
|
|
||||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
late StreamSubscription<List<(KeyVerification, Contact?)>>
|
||||||
|
_streamKeyVerifications;
|
||||||
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -63,7 +64,11 @@ class _VerificationExpansionTileCompState
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
String _verificationTypeLabel(
|
||||||
|
BuildContext context,
|
||||||
|
VerificationType type,
|
||||||
|
Contact? verifier,
|
||||||
|
) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
||||||
VerificationType.secretQrToken =>
|
VerificationType.secretQrToken =>
|
||||||
|
|
@ -72,7 +77,9 @@ class _VerificationExpansionTileCompState
|
||||||
),
|
),
|
||||||
VerificationType.link => context.lang.verificationTypeLink,
|
VerificationType.link => context.lang.verificationTypeLink,
|
||||||
VerificationType.contactSharedByVerified =>
|
VerificationType.contactSharedByVerified =>
|
||||||
context.lang.verificationTypeContactSharedByVerified,
|
verifier != null
|
||||||
|
? context.lang.contactVerifiedBy(getContactDisplayName(verifier))
|
||||||
|
: context.lang.contactSharedByUnknown,
|
||||||
VerificationType.migratedFromOldVersion =>
|
VerificationType.migratedFromOldVersion =>
|
||||||
context.lang.verificationTypeMigratedFromOldVersion,
|
context.lang.verificationTypeMigratedFromOldVersion,
|
||||||
};
|
};
|
||||||
|
|
@ -94,6 +101,17 @@ class _VerificationExpansionTileCompState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final sharedVerifierIds = _keyVerifications
|
||||||
|
.where((pair) =>
|
||||||
|
pair.$1.type == VerificationType.contactSharedByVerified &&
|
||||||
|
pair.$1.verifiedBy != null)
|
||||||
|
.map((pair) => pair.$1.verifiedBy!)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
final filteredTransferredTrust = _transferredTrust
|
||||||
|
.where((tt) => !sharedVerifierIds.contains(tt.$1.userId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
shape: const RoundedRectangleBorder(),
|
shape: const RoundedRectangleBorder(),
|
||||||
backgroundColor: context.color.surfaceContainer,
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
|
@ -108,65 +126,62 @@ class _VerificationExpansionTileCompState
|
||||||
title: Text(context.lang.userVerifiedTitle),
|
title: Text(context.lang.userVerifiedTitle),
|
||||||
children: [
|
children: [
|
||||||
..._keyVerifications.map(
|
..._keyVerifications.map(
|
||||||
(kv) => ListTile(
|
(pair) {
|
||||||
dense: true,
|
final kv = pair.$1;
|
||||||
contentPadding: const EdgeInsets.only(left: 16),
|
final verifier = pair.$2;
|
||||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
return ListTile(
|
||||||
trailing: Row(
|
dense: true,
|
||||||
mainAxisSize: MainAxisSize.min,
|
contentPadding: const EdgeInsets.only(left: 16),
|
||||||
children: [
|
title:
|
||||||
Text(
|
kv.type == VerificationType.contactSharedByVerified &&
|
||||||
DateFormat.yMd(
|
verifier != null
|
||||||
Localizations.localeOf(context).toString(),
|
? _VerifiedByContactRow(contact: verifier)
|
||||||
).format(kv.createdAt),
|
: Text(_verificationTypeLabel(context, kv.type, verifier)),
|
||||||
style: TextStyle(
|
trailing: Row(
|
||||||
color: context.color.onSurfaceVariant,
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 13,
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMd(
|
||||||
|
Localizations.localeOf(context).toString(),
|
||||||
|
).format(kv.createdAt),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
constraints: const BoxConstraints(),
|
||||||
constraints: const BoxConstraints(),
|
iconSize: 8,
|
||||||
iconSize: 8,
|
icon: FaIcon(
|
||||||
icon: FaIcon(
|
FontAwesomeIcons.trash,
|
||||||
FontAwesomeIcons.trash,
|
size: 8,
|
||||||
size: 8,
|
color: context.color.onSurfaceVariant,
|
||||||
color: context.color.onSurfaceVariant,
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirm = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.deleteVerificationTitle,
|
||||||
|
context.lang.deleteVerificationBody,
|
||||||
|
);
|
||||||
|
if (confirm) {
|
||||||
|
await twonlyDB.keyVerificationDao
|
||||||
|
.deleteKeyVerificationById(
|
||||||
|
kv.verificationId,
|
||||||
|
widget.contact.userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
],
|
||||||
final confirm = await showAlertDialog(
|
),
|
||||||
context,
|
);
|
||||||
context.lang.deleteVerificationTitle,
|
},
|
||||||
context.lang.deleteVerificationBody,
|
|
||||||
);
|
|
||||||
if (confirm) {
|
|
||||||
await twonlyDB.keyVerificationDao
|
|
||||||
.deleteKeyVerificationById(
|
|
||||||
kv.verificationId,
|
|
||||||
widget.contact.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
..._transferredTrust.map(
|
...filteredTransferredTrust.map(
|
||||||
(tt) => ListTile(
|
(tt) => ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Row(
|
title: _VerifiedByContactRow(contact: tt.$1),
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.lang.contactVerifiedBy(
|
|
||||||
getContactDisplayName(tt.$1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VerificationBadgeComp(
|
|
||||||
contact: tt.$1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
DateFormat.yMd(
|
DateFormat.yMd(
|
||||||
Localizations.localeOf(context).toString(),
|
Localizations.localeOf(context).toString(),
|
||||||
|
|
@ -182,3 +197,27 @@ class _VerificationExpansionTileCompState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A reusable row that shows "Verified by [contact name]" with the contact's
|
||||||
|
/// [VerificationBadgeComp] and navigates to their profile on tap.
|
||||||
|
class _VerifiedByContactRow extends StatelessWidget {
|
||||||
|
const _VerifiedByContactRow({required this.contact});
|
||||||
|
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.push(Routes.profileContact(contact.userId)),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.contactVerifiedBy(getContactDisplayName(contact)),
|
||||||
|
),
|
||||||
|
VerificationBadgeComp(contact: contact),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
|
|
||||||
class PrivacyView extends StatefulWidget {
|
class PrivacyView extends StatefulWidget {
|
||||||
const PrivacyView({super.key});
|
const PrivacyView({super.key});
|
||||||
|
|
@ -68,6 +70,7 @@ class _PrivacyViewState extends State<PrivacyView> {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.contactVerifyNumberTitle),
|
title: Text(context.lang.contactVerifyNumberTitle),
|
||||||
subtitle: Text(context.lang.contactVerifyNumberSubtitle),
|
subtitle: Text(context.lang.contactVerifyNumberSubtitle),
|
||||||
|
trailing: const _VerificationBadgeTriangle(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
@ -117,3 +120,44 @@ class _PrivacyViewState extends State<PrivacyView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _VerificationBadgeTriangle extends StatelessWidget {
|
||||||
|
const _VerificationBadgeTriangle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 9,
|
||||||
|
child: SvgIcon(
|
||||||
|
assetPath: SvgIcons.verifiedGreen,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
child: SvgIcon(
|
||||||
|
assetPath: SvgIcons.verifiedGreen,
|
||||||
|
size: 14,
|
||||||
|
color: colorVerificationBadgeYellow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SvgIcon(
|
||||||
|
assetPath: SvgIcons.verifiedRed,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import 'schema_v15.dart' as v15;
|
||||||
import 'schema_v16.dart' as v16;
|
import 'schema_v16.dart' as v16;
|
||||||
import 'schema_v17.dart' as v17;
|
import 'schema_v17.dart' as v17;
|
||||||
import 'schema_v18.dart' as v18;
|
import 'schema_v18.dart' as v18;
|
||||||
|
import 'schema_v19.dart' as v19;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -63,6 +64,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v17.DatabaseAtV17(db);
|
return v17.DatabaseAtV17(db);
|
||||||
case 18:
|
case 18:
|
||||||
return v18.DatabaseAtV18(db);
|
return v18.DatabaseAtV18(db);
|
||||||
|
case 19:
|
||||||
|
return v19.DatabaseAtV19(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
|
|
@ -87,5 +90,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
16,
|
16,
|
||||||
17,
|
17,
|
||||||
18,
|
18,
|
||||||
|
19,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10723
test/drift/twonly_db/generated/schema_v19.dart
Normal file
10723
test/drift/twonly_db/generated/schema_v19.dart
Normal file
File diff suppressed because it is too large
Load diff
693
test/services/key_verification_service_test.dart
Normal file
693
test/services/key_verification_service_test.dart
Normal file
|
|
@ -0,0 +1,693 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||||
|
import 'package:twonly/src/services/api.service.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (!Platform.isMacOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await locator.reset();
|
||||||
|
locator
|
||||||
|
..registerSingleton<TwonlyDB>(
|
||||||
|
TwonlyDB.forTesting(
|
||||||
|
DatabaseConnection(
|
||||||
|
NativeDatabase.memory(),
|
||||||
|
closeStreamsSynchronously: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..registerSingleton<UserService>(UserService())
|
||||||
|
..registerSingleton<ApiService>(ApiService());
|
||||||
|
|
||||||
|
// isUserDiscoveryEnabled defaults to false, so no Rust bridge calls happen
|
||||||
|
// in addKeyVerification / deleteKeyVerification.
|
||||||
|
userService.currentUser = UserData(
|
||||||
|
userId: 1,
|
||||||
|
username: 'me',
|
||||||
|
displayName: 'Me',
|
||||||
|
subscriptionPlan: 'Free',
|
||||||
|
currentSetupPage: null,
|
||||||
|
appVersion: 100,
|
||||||
|
);
|
||||||
|
userService.isUserCreated = true;
|
||||||
|
AppEnvironment.initTesting();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await twonlyDB.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> insertContact(int userId, {String? username}) async {
|
||||||
|
await twonlyDB.contactsDao.insertContact(
|
||||||
|
ContactsCompanion.insert(
|
||||||
|
userId: Value(userId),
|
||||||
|
username: username ?? 'user$userId',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addDirectVerification(
|
||||||
|
int contactId,
|
||||||
|
VerificationType type,
|
||||||
|
) async {
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(contactId, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addSharedVerification(
|
||||||
|
int contactId,
|
||||||
|
int sharedByContactId,
|
||||||
|
) async {
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||||
|
contactId,
|
||||||
|
VerificationType.contactSharedByVerified,
|
||||||
|
verifiedBy: sharedByContactId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Verification Tokens ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – Verification Tokens', () {
|
||||||
|
test(
|
||||||
|
'insertVerificationToken stores token; getRecentVerificationTokens returns it',
|
||||||
|
() async {
|
||||||
|
final token = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||||
|
await twonlyDB.keyVerificationDao.insertVerificationToken(token);
|
||||||
|
|
||||||
|
final tokens = await twonlyDB.keyVerificationDao
|
||||||
|
.getRecentVerificationTokens();
|
||||||
|
expect(tokens.length, 1);
|
||||||
|
expect(tokens.first.token, token);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('multiple tokens are all returned when recent', () async {
|
||||||
|
final t1 = Uint8List.fromList(List.generate(16, (i) => i));
|
||||||
|
final t2 = Uint8List.fromList(List.generate(16, (i) => i + 16));
|
||||||
|
await twonlyDB.keyVerificationDao.insertVerificationToken(t1);
|
||||||
|
await twonlyDB.keyVerificationDao.insertVerificationToken(t2);
|
||||||
|
|
||||||
|
final tokens = await twonlyDB.keyVerificationDao
|
||||||
|
.getRecentVerificationTokens();
|
||||||
|
expect(tokens.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Direct Verification ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – Direct Verification', () {
|
||||||
|
test(
|
||||||
|
'addKeyVerification stores an entry; getContactVerification returns it',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
expect(verifications.length, 1);
|
||||||
|
expect(verifications.first.type, VerificationType.secretQrToken);
|
||||||
|
expect(verifications.first.contactId, 10);
|
||||||
|
expect(verifications.first.verifiedBy, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('isContactVerified returns true for secretQrToken', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isContactVerified returns true for qrScanned', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.qrScanned);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isContactVerified returns true for link', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified returns false when no verification exists',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('multiple direct verifications are stored independently', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
expect(verifications.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Shared / Transitive Verification (Blue Badge) ──────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – Transitive Trust (Blue Badge)', () {
|
||||||
|
// Scenario setup:
|
||||||
|
// alice (userId=2) – may or may not be directly verified
|
||||||
|
// bob (userId=3) – shared by alice (contactSharedByVerified, verifiedBy=2)
|
||||||
|
//
|
||||||
|
// Rule: bob's shared verification only "counts" if alice is herself verified.
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified is false when sharer (alice) is NOT verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
// Bob shared by Alice, but Alice is not verified
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified is true (blue badge) when sharer (alice) IS verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
// Alice verified directly
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
// Bob shared by verified Alice
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified updates reactively when sharer becomes verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
// Bob not yet verified (alice not verified)
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), false);
|
||||||
|
|
||||||
|
// Alice gets verified
|
||||||
|
await addDirectVerification(2, VerificationType.qrScanned);
|
||||||
|
// Now Bob is transitively verified
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'direct verification takes precedence regardless of sharer state',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
// Bob has both a direct and a shared (unverified sharer) verification
|
||||||
|
await addDirectVerification(3, VerificationType.link);
|
||||||
|
await addSharedVerification(3, 2); // alice not verified
|
||||||
|
|
||||||
|
// Direct verification makes bob verified
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── watchContactVerification ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – watchContactVerification', () {
|
||||||
|
test(
|
||||||
|
'emits the direct verification entry with no verifier contact',
|
||||||
|
() async {
|
||||||
|
await insertContact(10, username: 'alice');
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(10)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries.length, 1);
|
||||||
|
final (kv, verifierContact) = entries.first;
|
||||||
|
expect(kv.type, VerificationType.secretQrToken);
|
||||||
|
expect(verifierContact, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'filters out shared verification when sharer is NOT verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addSharedVerification(3, 2); // alice not verified
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(3)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns shared verification entry with verifier contact when sharer IS verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(3)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries.length, 1);
|
||||||
|
final (kv, verifierContact) = entries.first;
|
||||||
|
expect(kv.type, VerificationType.contactSharedByVerified);
|
||||||
|
expect(verifierContact, isNotNull);
|
||||||
|
expect(verifierContact!.username, 'alice');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'emits mixed entries: direct (no filter) + shared (filtered by verifier state)',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'charlie');
|
||||||
|
await insertContact(4, username: 'bob');
|
||||||
|
|
||||||
|
// Bob has a direct verification
|
||||||
|
await addDirectVerification(4, VerificationType.link);
|
||||||
|
// Bob is also shared by Alice (unverified) – should NOT appear
|
||||||
|
await addSharedVerification(4, 2);
|
||||||
|
// Bob is also shared by Charlie (verified) – SHOULD appear
|
||||||
|
await addDirectVerification(3, VerificationType.qrScanned);
|
||||||
|
await addSharedVerification(4, 3);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(4)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
// Expect: direct + shared-by-charlie (2 entries, shared-by-alice filtered)
|
||||||
|
expect(entries.length, 2);
|
||||||
|
final types = entries.map((e) => e.$1.type).toSet();
|
||||||
|
expect(types, contains(VerificationType.link));
|
||||||
|
expect(types, contains(VerificationType.contactSharedByVerified));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('emits empty list for contact with no verifications', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(10)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getFirstVerificationTypeByContacts ─────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – getFirstVerificationTypeByContacts', () {
|
||||||
|
test(
|
||||||
|
'returns a map with the earliest verification type per contact',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await insertContact(20);
|
||||||
|
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(20, VerificationType.link);
|
||||||
|
|
||||||
|
final map = await twonlyDB.keyVerificationDao
|
||||||
|
.getFirstVerificationTypeByContacts();
|
||||||
|
|
||||||
|
expect(map[10], VerificationType.secretQrToken);
|
||||||
|
expect(map[20], VerificationType.link);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('returns an empty map when no verifications exist', () async {
|
||||||
|
final map = await twonlyDB.keyVerificationDao
|
||||||
|
.getFirstVerificationTypeByContacts();
|
||||||
|
expect(map, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns only the first verification type when multiple exist',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
|
||||||
|
// Insert two types for the same contact
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
final map = await twonlyDB.keyVerificationDao
|
||||||
|
.getFirstVerificationTypeByContacts();
|
||||||
|
|
||||||
|
// Only the first-inserted (earliest createdAt) should be in the map
|
||||||
|
expect(map.length, 1);
|
||||||
|
expect(map.containsKey(10), true);
|
||||||
|
// The first inserted was secretQrToken
|
||||||
|
expect(map[10], VerificationType.secretQrToken);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── deleteKeyVerification ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – deleteKeyVerification', () {
|
||||||
|
test('removes all verifications for a contact', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await twonlyDB.keyVerificationDao.getContactVerification(10),
|
||||||
|
hasLength(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerification(10);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await twonlyDB.keyVerificationDao.getContactVerification(10),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified returns false after deleteKeyVerification',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerification(10);
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('deleting one contact does not affect other contacts', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await insertContact(20);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(20, VerificationType.qrScanned);
|
||||||
|
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerification(10);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), false);
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(20), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'deleting sharer invalidates transitive trust for shared contacts',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
|
||||||
|
// Remove Alice's verification
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerification(2);
|
||||||
|
|
||||||
|
// Bob's transitive trust should now be invalid
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── deleteKeyVerificationById ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – deleteKeyVerificationById', () {
|
||||||
|
test('removes only the specified verification entry', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
var verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
expect(verifications.length, 2);
|
||||||
|
|
||||||
|
final idToDelete = verifications.first.verificationId;
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerificationById(
|
||||||
|
idToDelete,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
verifications = await twonlyDB.keyVerificationDao.getContactVerification(
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
expect(verifications.length, 1);
|
||||||
|
expect(verifications.first.verificationId, isNot(idToDelete));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified remains true if another direct verification exists',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerificationById(
|
||||||
|
verifications.first.verificationId,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'isContactVerified returns false after deleting the last verification',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerificationById(
|
||||||
|
verifications.first.verificationId,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── watchAllGroupMembersVerified ───────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationDao – watchAllGroupMembersVerified', () {
|
||||||
|
const groupId = 'test-group-abc';
|
||||||
|
|
||||||
|
Future<void> setupGroup(List<int> memberIds) async {
|
||||||
|
await twonlyDB.groupsDao.createNewGroup(
|
||||||
|
GroupsCompanion.insert(
|
||||||
|
groupId: groupId,
|
||||||
|
groupName: 'Trust Test Group',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (final id in memberIds) {
|
||||||
|
await twonlyDB.groupsDao.insertOrUpdateGroupMember(
|
||||||
|
GroupMembersCompanion.insert(groupId: groupId, contactId: id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('notTrusted when no member is verified', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await insertContact(20);
|
||||||
|
await setupGroup([10, 20]);
|
||||||
|
|
||||||
|
final status = await twonlyDB.keyVerificationDao
|
||||||
|
.watchAllGroupMembersVerified(groupId)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(status, VerificationStatus.notTrusted);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trusted when all members are directly verified', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await insertContact(20);
|
||||||
|
await addDirectVerification(10, VerificationType.secretQrToken);
|
||||||
|
await addDirectVerification(20, VerificationType.qrScanned);
|
||||||
|
await setupGroup([10, 20]);
|
||||||
|
|
||||||
|
final status = await twonlyDB.keyVerificationDao
|
||||||
|
.watchAllGroupMembersVerified(groupId)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(status, VerificationStatus.trusted);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notTrusted for empty group', () async {
|
||||||
|
await twonlyDB.groupsDao.createNewGroup(
|
||||||
|
GroupsCompanion.insert(groupId: groupId, groupName: 'Empty Group'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final status = await twonlyDB.keyVerificationDao
|
||||||
|
.watchAllGroupMembersVerified(groupId)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(status, VerificationStatus.notTrusted);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'single member group – trusted when that member is verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.link);
|
||||||
|
await setupGroup([10]);
|
||||||
|
|
||||||
|
final status = await twonlyDB.keyVerificationDao
|
||||||
|
.watchAllGroupMembersVerified(groupId)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(status, VerificationStatus.trusted);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Full Transitive Trust Scenario ─────────────────────────────────────────
|
||||||
|
|
||||||
|
group('KeyVerificationService – Full Transitive Trust Scenario', () {
|
||||||
|
// Simulates the complete "blue badge" flow:
|
||||||
|
// 1. Alice (2) is directly verified by me via QR code.
|
||||||
|
// 2. Alice shares Bob (3) – addKeyVerification(3, contactSharedByVerified, verifiedBy: 2)
|
||||||
|
// 3. Bob should receive the blue verification badge (isContactVerified = true)
|
||||||
|
// because Alice (the sharer) is herself verified.
|
||||||
|
//
|
||||||
|
// 4. Charlie (4) is shared by Dave (5) who is NOT verified.
|
||||||
|
// 5. Charlie should NOT receive a badge.
|
||||||
|
//
|
||||||
|
// 6. Eve (6) is shared by both Dave (5, unverified) and Alice (2, verified).
|
||||||
|
// 7. Eve should receive a badge (at least one verified sharer).
|
||||||
|
|
||||||
|
test('bob gets blue badge because alice (sharer) is verified', () async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'charlie gets no badge because dave (sharer) is NOT verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(4, username: 'charlie');
|
||||||
|
await insertContact(5, username: 'dave');
|
||||||
|
|
||||||
|
await addSharedVerification(4, 5);
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(4), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'eve gets blue badge because alice (one of her sharers) is verified',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(5, username: 'dave');
|
||||||
|
await insertContact(6, username: 'eve');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
// Dave is NOT verified
|
||||||
|
await addSharedVerification(6, 5); // dave shares eve (does not count)
|
||||||
|
await addSharedVerification(6, 2); // alice shares eve (counts!)
|
||||||
|
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(6), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'watchContactVerification shows alice as verifier for bob (blue badge)',
|
||||||
|
() async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(3)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries.length, 1);
|
||||||
|
final (kv, verifierContact) = entries.first;
|
||||||
|
expect(kv.type, VerificationType.contactSharedByVerified);
|
||||||
|
expect(verifierContact?.username, 'alice');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'watchContactVerification shows no entries for charlie (sharer unverified)',
|
||||||
|
() async {
|
||||||
|
await insertContact(4, username: 'charlie');
|
||||||
|
await insertContact(5, username: 'dave');
|
||||||
|
|
||||||
|
await addSharedVerification(4, 5);
|
||||||
|
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(4)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
expect(entries, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('removing alice revokes bob blue badge transitively', () async {
|
||||||
|
await insertContact(2, username: 'alice');
|
||||||
|
await insertContact(3, username: 'bob');
|
||||||
|
|
||||||
|
await addDirectVerification(2, VerificationType.secretQrToken);
|
||||||
|
await addSharedVerification(3, 2);
|
||||||
|
|
||||||
|
// Confirm bob is verified
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), true);
|
||||||
|
|
||||||
|
// Revoke alice's verification
|
||||||
|
await twonlyDB.keyVerificationDao.deleteKeyVerification(2);
|
||||||
|
|
||||||
|
// Bob should lose the blue badge
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(3), false);
|
||||||
|
|
||||||
|
// watchContactVerification should also be empty for bob
|
||||||
|
final entries = await twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(3)
|
||||||
|
.first;
|
||||||
|
expect(entries, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue