mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 05:42:11 +00:00
improve qr code verifications
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
65d188c4f2
commit
304190387d
16 changed files with 336 additions and 125 deletions
|
|
@ -27,7 +27,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
KeyVerificationDao(super.db);
|
||||
|
||||
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
||||
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
|
||||
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
|
||||
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
|
||||
return (select(
|
||||
verificationTokens,
|
||||
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
||||
|
|
@ -223,4 +224,31 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteKeyVerification(int contactId) async {
|
||||
try {
|
||||
await (delete(keyVerifications)..where((kv) => kv.contactId.equals(contactId))).go();
|
||||
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||
contactId: contactId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteKeyVerificationById(int verificationId, int contactId) async {
|
||||
try {
|
||||
await (delete(keyVerifications)..where((kv) => kv.verificationId.equals(verificationId))).go();
|
||||
final remaining = await getContactVerification(contactId);
|
||||
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
|
||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||
contactId: contactId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -911,8 +911,8 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationTypeSecretQrToken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The other person scanned your QR code.'**
|
||||
String get verificationTypeSecretQrToken;
|
||||
/// **'{username} has scanned your QR code.'**
|
||||
String verificationTypeSecretQrToken(Object username);
|
||||
|
||||
/// No description provided for @verificationTypeLink.
|
||||
///
|
||||
|
|
@ -2699,7 +2699,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationBadgeGeneralDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'**
|
||||
/// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'**
|
||||
String get verificationBadgeGeneralDesc;
|
||||
|
||||
/// No description provided for @verificationBadgeGreenDesc.
|
||||
|
|
@ -2720,6 +2720,24 @@ abstract class AppLocalizations {
|
|||
/// **'A contact whose identity has *not* yet been verified.'**
|
||||
String get verificationBadgeRedDesc;
|
||||
|
||||
/// No description provided for @deleteVerificationTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete verification?'**
|
||||
String get deleteVerificationTitle;
|
||||
|
||||
/// No description provided for @deleteVerificationBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to delete this verification?'**
|
||||
String get deleteVerificationBody;
|
||||
|
||||
/// No description provided for @secretQrTokenVerifiedSnackbar.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{username} has scanned your QR code and is now verified.'**
|
||||
String secretQrTokenVerifiedSnackbar(Object username);
|
||||
|
||||
/// No description provided for @chatEntryFlameRestored.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -447,8 +447,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||
|
||||
@override
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'Die andere Person hat deinen QR-Code gescannt.';
|
||||
String verificationTypeSecretQrToken(Object username) {
|
||||
return '$username hat deinen QR-Code gescannt.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||
|
|
@ -1501,7 +1502,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.';
|
||||
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
|
|
@ -1515,6 +1516,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||
|
||||
@override
|
||||
String get deleteVerificationTitle => 'Verifizierung löschen?';
|
||||
|
||||
@override
|
||||
String get deleteVerificationBody =>
|
||||
'Möchtest du diese Verifizierung wirklich löschen?';
|
||||
|
||||
@override
|
||||
String secretQrTokenVerifiedSnackbar(Object username) {
|
||||
return '$username hat deinen QR-Code gescannt und ist nun verifiziert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
return '$count Flammen wiederhergestellt';
|
||||
|
|
|
|||
|
|
@ -442,8 +442,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'The other person scanned your QR code.';
|
||||
String verificationTypeSecretQrToken(Object username) {
|
||||
return '$username has scanned your QR code.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Verified via link.';
|
||||
|
|
@ -1486,7 +1487,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.';
|
||||
'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
|
|
@ -1500,6 +1501,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'A contact whose identity has *not* yet been verified.';
|
||||
|
||||
@override
|
||||
String get deleteVerificationTitle => 'Delete verification?';
|
||||
|
||||
@override
|
||||
String get deleteVerificationBody =>
|
||||
'Are you sure you want to delete this verification?';
|
||||
|
||||
@override
|
||||
String secretQrTokenVerifiedSnackbar(Object username) {
|
||||
return '$username has scanned your QR code and is now verified.';
|
||||
}
|
||||
|
||||
@override
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
return '$count flames restored';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93
|
||||
Subproject commit 0675e74501d6610a84273232517652db25965e3f
|
||||
|
|
@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? signedPrekeySignature,
|
||||
$fixnum.Int64? signedPrekeyId,
|
||||
$core.List<$core.int>? secretVerificationToken,
|
||||
$fixnum.Int64? timestamp,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
|
|
@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
||||
if (secretVerificationToken != null)
|
||||
result.secretVerificationToken = secretVerificationToken;
|
||||
if (timestamp != null) result.timestamp = timestamp;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
||||
..a<$core.List<$core.int>>(
|
||||
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
||||
..aInt64(9, _omitFieldNames ? '' : 'timestamp')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -230,6 +233,15 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$core.bool hasSecretVerificationToken() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearSecretVerificationToken() => $_clearField(8);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$fixnum.Int64 get timestamp => $_getI64(8);
|
||||
@$pb.TagNumber(9)
|
||||
set timestamp($fixnum.Int64 value) => $_setInt64(8, value);
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasTimestamp() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearTimestamp() => $_clearField(9);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -77,9 +77,19 @@ const PublicProfile$json = {
|
|||
'10': 'secretVerificationToken',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'timestamp',
|
||||
'3': 9,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'9': 1,
|
||||
'10': 'timestamp',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_secret_verification_token'},
|
||||
{'1': '_timestamp'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
|
|||
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
||||
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
||||
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');
|
||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX'
|
||||
'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA==');
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ message PublicProfile {
|
|||
bytes signed_prekey_signature = 6;
|
||||
int64 signed_prekey_id = 7;
|
||||
optional bytes secret_verification_token = 8;
|
||||
optional int64 timestamp = 9;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
|
|
@ -47,7 +48,10 @@ import 'package:twonly/src/visual/views/settings/subscription/subscription.view.
|
|||
import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
|
||||
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
||||
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final routerProvider = GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.home,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ import 'dart:typed_data';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
|
||||
class KeyVerificationService {
|
||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||
|
|
@ -70,6 +73,18 @@ class KeyVerificationService {
|
|||
VerificationType.secretQrToken,
|
||||
);
|
||||
Log.info('Contact was verified via secretQrToken');
|
||||
|
||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context != null && context.mounted && contact != null) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.secretQrTokenVerifiedSnackbar(
|
||||
getContactDisplayName(contact),
|
||||
),
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart' show ListExtensions;
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
|
@ -41,6 +42,7 @@ class QrCodeUtils {
|
|||
signedPrekeySignature: signedPreKey.signature,
|
||||
signedPrekeyId: Int64(signedPreKey.id),
|
||||
secretVerificationToken: secretVerificationToken,
|
||||
timestamp: Int64(clock.now().millisecondsSinceEpoch),
|
||||
);
|
||||
|
||||
final data = publicProfile.writeToBuffer();
|
||||
|
|
@ -94,7 +96,18 @@ class QrCodeUtils {
|
|||
);
|
||||
|
||||
if (verificationOk) {
|
||||
if (profile.hasSecretVerificationToken()) {
|
||||
var useSecretVerificationToken = profile.hasSecretVerificationToken();
|
||||
if (profile.hasTimestamp()) {
|
||||
// Only notify the scanned user if the QR code was generated within the last 10 minutes.
|
||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||
profile.timestamp.toInt(),
|
||||
);
|
||||
final tenMinutesAgo = clock.now().subtract(const Duration(minutes: 10));
|
||||
if (timestamp.isBefore(tenMinutesAgo)) {
|
||||
useSecretVerificationToken = false;
|
||||
}
|
||||
}
|
||||
if (useSecretVerificationToken) {
|
||||
unawaited(
|
||||
KeyVerificationService.handleScannedVerificationToken(
|
||||
contact.userId,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,12 @@ void _showOverlay({
|
|||
required Duration displayDuration,
|
||||
required void Function(AnimationController) onAnimationControllerInit,
|
||||
}) {
|
||||
final overlayState = Overlay.maybeOf(context);
|
||||
var overlayState = Overlay.maybeOf(context);
|
||||
if (overlayState == null) {
|
||||
if (context is StatefulElement && context.state is NavigatorState) {
|
||||
overlayState = (context.state as NavigatorState).overlay;
|
||||
}
|
||||
}
|
||||
if (overlayState == null) return;
|
||||
|
||||
late OverlayEntry overlayEntry;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class CameraScannedOverlay extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
getContactDisplayName(c.contact, maxLength: 13),
|
||||
getContactDisplayName(c.contact, maxLength: 9),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
ColoredBox(
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ import 'package:drift/drift.dart' hide Column;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -20,6 +18,7 @@ import 'package:twonly/src/visual/components/snackbar.dart';
|
|||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
||||
|
||||
|
|
@ -35,13 +34,9 @@ class ContactView extends StatefulWidget {
|
|||
class _ContactViewState extends State<ContactView> {
|
||||
Contact? _contact;
|
||||
List<GroupMember> _memberOfGroups = [];
|
||||
List<KeyVerification> _keyVerifications = [];
|
||||
List<(Contact, DateTime)> _transferredTrust = [];
|
||||
|
||||
late StreamSubscription<Contact?> _streamContact;
|
||||
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -63,30 +58,12 @@ class _ContactViewState extends State<ContactView> {
|
|||
_memberOfGroups = groups;
|
||||
});
|
||||
});
|
||||
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
||||
.watchContactVerification(widget.userId)
|
||||
.listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_keyVerifications = update;
|
||||
});
|
||||
});
|
||||
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||
.watchTransferredTrustVerifications(widget.userId)
|
||||
.listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_transferredTrust = update;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamContact.cancel();
|
||||
_streamMemberOfGroups.cancel();
|
||||
_streamKeyVerifications.cancel();
|
||||
_streamTransferredTrust.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -260,74 +237,8 @@ class _ContactViewState extends State<ContactView> {
|
|||
RestoreFlameComp(
|
||||
contactId: widget.userId,
|
||||
),
|
||||
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty)
|
||||
BetterListTile(
|
||||
leading: VerificationBadgeComp(
|
||||
VerificationExpansionTileComp(
|
||||
contact: contact,
|
||||
size: 20,
|
||||
),
|
||||
text: context.lang.contactVerifyNumberTitle,
|
||||
onTap: () async {
|
||||
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty)
|
||||
ExpansionTile(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
collapsedShape: const RoundedRectangleBorder(),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 12, right: 12),
|
||||
child: VerificationBadgeComp(
|
||||
contact: contact,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(context.lang.userVerifiedTitle),
|
||||
children: [
|
||||
..._keyVerifications.map(
|
||||
(kv) => ListTile(
|
||||
dense: true,
|
||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
||||
trailing: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(kv.createdAt),
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
..._transferredTrust.map(
|
||||
(tt) => ListTile(
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
context.lang.contactVerifiedBy(
|
||||
getContactDisplayName(tt.$1),
|
||||
),
|
||||
),
|
||||
VerificationBadgeComp(
|
||||
contact: tt.$1,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(tt.$2),
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (userService.currentUser.isUserDiscoveryEnabled)
|
||||
if (userService.currentUser.userDiscoveryRequiresManualApproval &&
|
||||
|
|
@ -408,19 +319,6 @@ class _ContactViewState extends State<ContactView> {
|
|||
}
|
||||
}
|
||||
|
||||
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
||||
return switch (type) {
|
||||
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
||||
VerificationType.secretQrToken =>
|
||||
context.lang.verificationTypeSecretQrToken,
|
||||
VerificationType.link => context.lang.verificationTypeLink,
|
||||
VerificationType.contactSharedByVerified =>
|
||||
context.lang.verificationTypeContactSharedByVerified,
|
||||
VerificationType.migratedFromOldVersion =>
|
||||
context.lang.verificationTypeMigratedFromOldVersion,
|
||||
};
|
||||
}
|
||||
|
||||
Future<String?> showNicknameChangeDialog(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||
|
||||
class VerificationExpansionTileComp extends StatefulWidget {
|
||||
const VerificationExpansionTileComp({
|
||||
required this.contact,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Contact contact;
|
||||
|
||||
@override
|
||||
State<VerificationExpansionTileComp> createState() =>
|
||||
_VerificationExpansionTileCompState();
|
||||
}
|
||||
|
||||
class _VerificationExpansionTileCompState
|
||||
extends State<VerificationExpansionTileComp> {
|
||||
List<KeyVerification> _keyVerifications = [];
|
||||
List<(Contact, DateTime)> _transferredTrust = [];
|
||||
|
||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
||||
.watchContactVerification(widget.contact.userId)
|
||||
.listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_keyVerifications = update;
|
||||
});
|
||||
});
|
||||
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||
.watchTransferredTrustVerifications(widget.contact.userId)
|
||||
.listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_transferredTrust = update;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamKeyVerifications.cancel();
|
||||
_streamTransferredTrust.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
||||
return switch (type) {
|
||||
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
||||
VerificationType.secretQrToken =>
|
||||
context.lang.verificationTypeSecretQrToken(
|
||||
getContactDisplayName(widget.contact),
|
||||
),
|
||||
VerificationType.link => context.lang.verificationTypeLink,
|
||||
VerificationType.contactSharedByVerified =>
|
||||
context.lang.verificationTypeContactSharedByVerified,
|
||||
VerificationType.migratedFromOldVersion =>
|
||||
context.lang.verificationTypeMigratedFromOldVersion,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) {
|
||||
return BetterListTile(
|
||||
leading: VerificationBadgeComp(
|
||||
contact: widget.contact,
|
||||
size: 20,
|
||||
),
|
||||
text: context.lang.contactVerifyNumberTitle,
|
||||
onTap: () async {
|
||||
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ExpansionTile(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
collapsedShape: const RoundedRectangleBorder(),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 12, right: 12),
|
||||
child: VerificationBadgeComp(
|
||||
contact: widget.contact,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(context.lang.userVerifiedTitle),
|
||||
children: [
|
||||
..._keyVerifications.map(
|
||||
(kv) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(kv.createdAt),
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
iconSize: 8,
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.trash,
|
||||
size: 8,
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
..._transferredTrust.map(
|
||||
(tt) => ListTile(
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
context.lang.contactVerifiedBy(
|
||||
getContactDisplayName(tt.$1),
|
||||
),
|
||||
),
|
||||
VerificationBadgeComp(
|
||||
contact: tt.$1,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(tt.$2),
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts.setup.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart';
|
||||
|
|
@ -15,7 +14,6 @@ import 'package:twonly/src/visual/views/settings/privacy/user_discovery/componen
|
|||
enum SetupPages {
|
||||
profile,
|
||||
backup,
|
||||
addNewContact,
|
||||
verificationBadge,
|
||||
shareYourFriends,
|
||||
letYourFriendsFindYou,
|
||||
|
|
@ -185,8 +183,6 @@ class _SetupViewState extends State<SetupView> {
|
|||
return const ProfileSetupPage();
|
||||
case SetupPages.backup:
|
||||
return const BackupSetupPage();
|
||||
case SetupPages.addNewContact:
|
||||
return const AddNewContactsPage();
|
||||
case SetupPages.verificationBadge:
|
||||
return const VerificationBadgeSetupPage();
|
||||
case SetupPages.shareYourFriends:
|
||||
|
|
|
|||
Loading…
Reference in a new issue