From cda7df36f3453a16ac64b9c189c30808cdb32ca6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 9 Jul 2026 18:09:16 +0200 Subject: [PATCH] Improve: Visibility of the verification badge --- CHANGELOG.md | 1 + lib/app.dart | 2 - .../database/daos/key_verification.dao.dart | 48 +++++ .../generated/app_localizations.dart | 50 ++++- .../generated/app_localizations_de.dart | 37 +++- .../generated/app_localizations_en.dart | 37 +++- lib/src/localization/translations | 2 +- lib/src/providers/routing.provider.dart | 12 +- .../components/profile_qr_code.comp.dart | 51 +++++ .../verification_badge_info.comp.dart | 108 +++++++--- .../views/camera/camera_qr_scanner.view.dart | 38 +++- .../views/chats/chat_messages.view.dart | 29 ++- .../in_chat_group_overview.dart | 203 ++++++++++++++++++ .../views/contact/add_new_contact.view.dart | 48 +---- .../verification_expansion_tile.comp.dart | 17 +- .../visual/views/onboarding/setup.view.dart | 7 - .../setup/verification_badge.setup.dart | 27 --- lib/src/visual/views/public_profile.view.dart | 10 +- .../help/faq/verification_badge_faq.view.dart | 8 +- 19 files changed, 588 insertions(+), 147 deletions(-) create mode 100644 lib/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart delete mode 100644 lib/src/visual/views/onboarding/setup/verification_badge.setup.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 38773913..6b5c069f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.3.5 +- Improve: Visibility of the verification badge - Fix: Performance issue caused by an out-of-sync Signal session - Fix: Multiple smaller bug fixes diff --git a/lib/app.dart b/lib/app.dart index c5151d5c..8a0dc086 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,8 +11,6 @@ import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/passwordless_recovery.service.dart' - show PasswordlessRecoveryService; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/pow.dart'; diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index 20fc47d8..eef18d6e 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -259,6 +259,54 @@ class KeyVerificationDao extends DatabaseAccessor }); } + Stream watchUnverifiedGroupMembersCount(String groupId) { + final gm = groupMembers; + final directKv = alias(keyVerifications, 'directKv'); + final ur = userDiscoveryUserRelations; + final verifierKv = alias(keyVerifications, 'verifierKv'); + + final query = select(gm).join([ + leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)), + leftOuterJoin( + ur, + ur.announcedUserId.equalsExp(gm.contactId) & + ur.publicKeyVerifiedTimestamp.isNotNull() & + ur.fromContactId.equalsExp(gm.contactId).not(), + ), + leftOuterJoin( + verifierKv, + verifierKv.contactId.equalsExp(ur.fromContactId), + ), + ])..where(gm.groupId.equals(groupId)); + + return query.watch().map((rows) { + if (rows.isEmpty) return 0; + + final memberTrustMap = {}; + + for (final row in rows) { + final contactId = row.readTable(gm).contactId; + final isDirect = row.readTableOrNull(directKv) != null; + final isPartial = row.readTableOrNull(verifierKv) != null; + + final current = + memberTrustMap[contactId] ?? (direct: false, partial: false); + memberTrustMap[contactId] = ( + direct: current.direct || isDirect, + partial: current.partial || isPartial, + ); + } + + var count = 0; + for (final trust in memberTrustMap.values) { + if (!trust.direct && !trust.partial) { + count++; + } + } + return count; + }); + } + Future addKeyVerification( int contactId, VerificationType type, { diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 4fcaac54..98324cbb 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -896,18 +896,48 @@ abstract class AppLocalizations { /// **'Verify contacts'** String get contactVerifyNumberTitle; + /// No description provided for @verifyUserIdentity. + /// + /// In en, this message translates to: + /// **'Verify {username}\'s identity'** + String verifyUserIdentity(Object username); + /// No description provided for @contactVerifyNumberSubtitle. /// /// In en, this message translates to: /// **'Verify the identity of your contacts to make sure you are texting the right person.'** String get contactVerifyNumberSubtitle; + /// No description provided for @inChatContactNotVerified. + /// + /// In en, this message translates to: + /// **'Contact not verified.'** + String get inChatContactNotVerified; + + /// No description provided for @groupMembersNotVerified. + /// + /// In en, this message translates to: + /// **'{count} members are not verified.'** + String groupMembersNotVerified(Object count); + /// No description provided for @userVerifiedTitle. /// /// In en, this message translates to: /// **'Contact verified'** String get userVerifiedTitle; + /// No description provided for @scanUserQrCode. + /// + /// In en, this message translates to: + /// **'Scan {username}\'s QR code'** + String scanUserQrCode(Object username); + + /// No description provided for @openOwnQrCode. + /// + /// In en, this message translates to: + /// **'Open your own QR code'** + String get openOwnQrCode; + /// No description provided for @contactVerifiedBy. /// /// In en, this message translates to: @@ -2384,6 +2414,12 @@ abstract class AppLocalizations { /// **'Let a friend scan this QR code to add you'** String get addContactQrSheetSubtext; + /// No description provided for @letUserScanQrCode. + /// + /// In en, this message translates to: + /// **'Let {username} scan this QR code'** + String letUserScanQrCode(Object username); + /// No description provided for @finishSetupCardTitle. /// /// In en, this message translates to: @@ -2741,7 +2777,7 @@ abstract class AppLocalizations { /// No description provided for @verificationBadgeGeneralDesc. /// /// In en, this message translates to: - /// **'The badge *protects you from scammers and attackers*. It will be displayed next to a contact that has been *manually verified* by you or a friend.'** + /// **'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.'** String get verificationBadgeGeneralDesc; /// No description provided for @verificationBadgeGreenDesc. @@ -2768,6 +2804,18 @@ abstract class AppLocalizations { /// **'Scan now'** String get scanNow; + /// No description provided for @qrScannerVerifyHint. + /// + /// In en, this message translates to: + /// **'To verify a contact ask them to open their QR code (Chat Lists > QR code button at the bottom right)'** + String get qrScannerVerifyHint; + + /// No description provided for @qrScannerVerifyUserHint. + /// + /// In en, this message translates to: + /// **'To verify {username} ask them to open their QR code (Chat Lists > QR code button at the bottom right)'** + String qrScannerVerifyUserHint(Object username); + /// No description provided for @openQrCode. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 51c7913e..0a35d2aa 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -435,13 +435,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactVerifyNumberTitle => 'Kontakte verifizieren'; + @override + String verifyUserIdentity(Object username) { + return 'Identität von $username verifizieren'; + } + @override String get contactVerifyNumberSubtitle => 'Überprüfe die Identität deiner Kontakte, um sicherzugehen, dass du mit der richtigen Person schreibst.'; + @override + String get inChatContactNotVerified => 'Kontakt nicht verifiziert.'; + + @override + String groupMembersNotVerified(Object count) { + return '$count Mitglieder sind nicht verifiziert.'; + } + @override String get userVerifiedTitle => 'Kontakt verifiziert'; + @override + String scanUserQrCode(Object username) { + return 'QR-Code von $username scannen'; + } + + @override + String get openOwnQrCode => 'Eigenen QR-Code öffnen'; + @override String contactVerifiedBy(Object username) { return 'Verifiziert von $username'; @@ -1317,6 +1338,11 @@ class AppLocalizationsDe extends AppLocalizations { String get addContactQrSheetSubtext => 'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen'; + @override + String letUserScanQrCode(Object username) { + return 'Lass $username diesen QR-Code scannen'; + } + @override String get finishSetupCardTitle => 'Profil vervollständigen'; @@ -1542,7 +1568,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'Der Haken *schützt dich vor Betrügern und Angreifern*. Es wird neben einem Kontakt angezeigt, der von dir oder einem Freund *manuell überprüft* wurde.'; + 'Der Haken gibt dir die Sicherheit, dass du mit der *richtigen Person* schreibst und deine *Nachrichten vertraulich* bleiben.'; @override String get verificationBadgeGreenDesc => @@ -1559,6 +1585,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanNow => 'Jetzt scannen'; + @override + String get qrScannerVerifyHint => + 'Um einen Kontakt zu verifizieren, bitte ihn, seinen QR-Code zu öffnen (Chat-Liste > QR-Code-Button unten rechts)'; + + @override + String qrScannerVerifyUserHint(Object username) { + return 'Um $username zu verifizieren, bitte ihn, seinen QR-Code zu öffnen (Chat-Liste > QR-Code-Button unten rechts)'; + } + @override String get openQrCode => 'QR-Code öffnen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 80ecf519..c935ec67 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -431,13 +431,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactVerifyNumberTitle => 'Verify contacts'; + @override + String verifyUserIdentity(Object username) { + return 'Verify $username\'s identity'; + } + @override String get contactVerifyNumberSubtitle => 'Verify the identity of your contacts to make sure you are texting the right person.'; + @override + String get inChatContactNotVerified => 'Contact not verified.'; + + @override + String groupMembersNotVerified(Object count) { + return '$count members are not verified.'; + } + @override String get userVerifiedTitle => 'Contact verified'; + @override + String scanUserQrCode(Object username) { + return 'Scan $username\'s QR code'; + } + + @override + String get openOwnQrCode => 'Open your own QR code'; + @override String contactVerifiedBy(Object username) { return 'Verified by $username'; @@ -1309,6 +1330,11 @@ class AppLocalizationsEn extends AppLocalizations { String get addContactQrSheetSubtext => 'Let a friend scan this QR code to add you'; + @override + String letUserScanQrCode(Object username) { + return 'Let $username scan this QR code'; + } + @override String get finishSetupCardTitle => 'Complete your profile'; @@ -1529,7 +1555,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'The badge *protects you from scammers and attackers*. It will be displayed next to a contact that has been *manually verified* by you or a friend.'; + 'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.'; @override String get verificationBadgeGreenDesc => @@ -1546,6 +1572,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanNow => 'Scan now'; + @override + String get qrScannerVerifyHint => + 'To verify a contact ask them to open their QR code (Chat Lists > QR code button at the bottom right)'; + + @override + String qrScannerVerifyUserHint(Object username) { + return 'To verify $username ask them to open their QR code (Chat Lists > QR code button at the bottom right)'; + } + @override String get openQrCode => 'Open QR code'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 147f44e6..27b598a1 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 147f44e608f39f04af3b3be9aee1ad662e5a84e1 +Subproject commit 27b598a151a0122f05670d5d8fb6dcfd5f440127 diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 1412bec8..083cc501 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -143,7 +143,13 @@ final routerProvider = GoRouter( GoRoute( path: Routes.cameraQRScanner, builder: (context, state) { - return const QrCodeScannerView(); + final extra = state.extra as Map?; + final contact = extra?['contact'] as Contact?; + final openToVerify = extra?['openToVerify'] as bool? ?? false; + return QrCodeScannerView( + contact: contact, + openToVerify: openToVerify, + ); }, ), @@ -248,7 +254,9 @@ final routerProvider = GoRouter( routes: [ GoRoute( path: 'verifybadge', - builder: (context, state) => const VerificationBadeFaqView(), + builder: (context, state) => VerificationBadeFaqView( + contact: state.extra as Contact?, + ), ), ], ), diff --git a/lib/src/visual/components/profile_qr_code.comp.dart b/lib/src/visual/components/profile_qr_code.comp.dart index 5c194703..4d21e0fb 100644 --- a/lib/src/visual/components/profile_qr_code.comp.dart +++ b/lib/src/visual/components/profile_qr_code.comp.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; @@ -16,6 +18,55 @@ class ProfileQrCodeComp extends StatefulWidget { final double size; final bool showAvatar; + static Future showSheet(BuildContext context, {Contact? contact}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return Container( + decoration: BoxDecoration( + color: context.color.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: double.infinity), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: context.color.onSurface.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 24), + Text( + contact != null + ? context.lang.letUserScanQrCode( + getContactDisplayName(contact), + ) + : context.lang.addContactQrSheetSubtext, + style: TextStyle( + fontSize: 14, + color: context.color.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + const ProfileQrCodeComp(), + const SizedBox(height: 34), + ], + ), + ); + }, + ); + } + @override State createState() => _ProfileQrCodeCompState(); } diff --git a/lib/src/visual/components/verification_badge_info.comp.dart b/lib/src/visual/components/verification_badge_info.comp.dart index 5d0bbc6c..5dc04fcb 100644 --- a/lib/src/visual/components/verification_badge_info.comp.dart +++ b/lib/src/visual/components/verification_badge_info.comp.dart @@ -4,8 +4,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart' import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart'; import 'package:twonly/src/visual/themes/light.dart'; @@ -15,12 +18,60 @@ const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238); class VerificationBadgeInfo extends StatelessWidget { const VerificationBadgeInfo({ this.displayButtons = false, + this.contact, super.key, }); final bool displayButtons; + final Contact? contact; @override Widget build(BuildContext context) { + final scanButton = MyButton( + variant: MyButtonVariant.primaryDense, + onPressed: () => context.push( + Routes.cameraQRScanner, + extra: contact != null + ? { + 'contact': contact, + 'openToVerify': true, + } + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon(FontAwesomeIcons.camera), + const SizedBox(width: 6), + Text( + contact != null + ? context.lang.scanUserQrCode( + getContactDisplayName(contact!), + ) + : context.lang.scanNow, + ), + ], + ), + ); + + final openButton = MyButton( + variant: MyButtonVariant.primaryDense, + onPressed: () => ProfileQrCodeComp.showSheet(context, contact: contact), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon(FontAwesomeIcons.qrcode), + const SizedBox(width: 6), + Text( + contact != null + ? context.lang.openOwnQrCode + : context.lang.openQrCode, + ), + ], + ), + ); + return Column( children: [ RichText( @@ -40,43 +91,36 @@ class VerificationBadgeInfo extends StatelessWidget { icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40), description: context.lang.verificationBadgeGreenDesc, boldTextColor: primaryColor, - onTap: () => context.push(Routes.cameraQRScanner), + onTap: () => context.push( + Routes.cameraQRScanner, + extra: contact != null + ? { + 'contact': contact, + 'openToVerify': true, + } + : null, + ), ), if (displayButtons) Padding( padding: const EdgeInsets.only(bottom: 20), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicWidth( - child: MyButton( - variant: MyButtonVariant.primaryDense, - onPressed: () => context.push(Routes.cameraQRScanner), - child: Row( - children: [ - const FaIcon(FontAwesomeIcons.camera), - const SizedBox(width: 6), - Text(context.lang.scanNow), - ], - ), + child: contact != null + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + scanButton, + const SizedBox(height: 8), + openButton, + ], + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicWidth(child: scanButton), + const SizedBox(width: 8), + IntrinsicWidth(child: openButton), + ], ), - ), - const SizedBox(width: 8), - IntrinsicWidth( - child: MyButton( - variant: MyButtonVariant.primaryDense, - onPressed: () => context.push(Routes.settingsPublicProfile), - child: Row( - children: [ - const FaIcon(FontAwesomeIcons.qrcode), - const SizedBox(width: 6), - Text(context.lang.openQrCode), - ], - ), - ), - ), - ], - ), ), if (userService.currentUser.securityProfile != SecurityProfile.strict || userService.currentUser.isUserDiscoveryEnabled) diff --git a/lib/src/visual/views/camera/camera_qr_scanner.view.dart b/lib/src/visual/views/camera/camera_qr_scanner.view.dart index 06aa8ee9..ce5b550a 100644 --- a/lib/src/visual/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/visual/views/camera/camera_qr_scanner.view.dart @@ -1,12 +1,23 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; class QrCodeScannerView extends StatefulWidget { - const QrCodeScannerView({super.key}); + const QrCodeScannerView({ + this.contact, + this.openToVerify = false, + super.key, + }); + + final Contact? contact; + final bool openToVerify; + @override State createState() => QrCodeScannerViewState(); } @@ -56,6 +67,31 @@ class QrCodeScannerViewState extends State { isVisible: true, ), ), + if (widget.openToVerify) + Positioned( + left: 16, + right: 16, + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + widget.contact != null + ? context.lang.qrScannerVerifyUserHint( + getContactDisplayName(widget.contact!), + ) + : context.lang.qrScannerVerifyHint, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ), ], ), ); diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index 0d39fb2e..10b06b38 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -25,6 +25,7 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/blink.com import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_date_chip.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart'; @@ -89,15 +90,6 @@ class _ChatMessagesViewState extends State Mutex protectMessageUpdating = Mutex(); - // @override - // void didChangeAppLifecycleState(AppLifecycleState state) { - // if (state == AppLifecycleState.resumed) { - // protectMessageUpdating.protect(() async { - // await setMessages(allMessages, groupActions); - // }); - // } - // } - bool _isViewActive() { if (!mounted) return false; return !AppState.isAppInBackground && @@ -326,11 +318,14 @@ class _ChatMessagesViewState extends State child: Column( children: [ Expanded( - child: ScrollablePositionedList.builder( - reverse: true, - itemCount: messages.length + 1 + 1, - itemScrollController: itemScrollController, - itemBuilder: (context, i) { + child: Align( + alignment: Alignment.topCenter, + child: ScrollablePositionedList.builder( + shrinkWrap: true, + reverse: true, + itemCount: messages.length + 1 + 1, + itemScrollController: itemScrollController, + itemBuilder: (context, i) { if (i == 0) { return userService.currentUser.typingIndicators ? TypingIndicator(group: group) @@ -338,8 +333,9 @@ class _ChatMessagesViewState extends State } i -= 1; if (i == messages.length) { - return const Padding( - padding: EdgeInsetsGeometry.only(top: 10), + return Padding( + padding: const EdgeInsets.only(top: 10), + child: InChatGroupOverview(group: group), ); } if (messages[i].isDate) { @@ -386,6 +382,7 @@ class _ChatMessagesViewState extends State }, ), ), + ), if (quotesMessage != null) Container( padding: const EdgeInsets.only( diff --git a/lib/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart b/lib/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart new file mode 100644 index 00000000..ebc2f3df --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart @@ -0,0 +1,203 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/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/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; + +class InChatGroupOverview extends StatefulWidget { + const InChatGroupOverview({ + required this.group, + super.key, + }); + + final Group group; + + @override + State createState() => _InChatGroupOverviewState(); +} + +class _InChatGroupOverviewState extends State { + Contact? _directContact; + StreamSubscription? _verificationSub; + StreamSubscription? _unverifiedCountSub; + bool _isVerified = true; + int _unverifiedCount = 0; + + @override + void initState() { + super.initState(); + _initVerificationCheck(); + } + + Future _initVerificationCheck() async { + if (widget.group.isDirectChat) { + final contacts = await twonlyDB.groupsDao.getGroupContact( + widget.group.groupId, + ); + if (contacts.isNotEmpty) { + _directContact = contacts.first; + _verificationSub = twonlyDB.keyVerificationDao + .watchContactVerification(_directContact!.userId) + .listen((verifications) { + if (mounted) { + setState(() { + _isVerified = verifications.any( + (v) => + v.$1.type != VerificationType.contactSharedByVerified, + ); + }); + } + }); + } + } else { + _verificationSub = twonlyDB.keyVerificationDao + .watchAllGroupMembersVerified(widget.group.groupId) + .listen((status) { + if (mounted) { + setState(() { + _isVerified = status == VerificationStatus.trusted; + }); + } + }); + _unverifiedCountSub = twonlyDB.keyVerificationDao + .watchUnverifiedGroupMembersCount(widget.group.groupId) + .listen((count) { + if (mounted) { + setState(() { + _unverifiedCount = count; + }); + } + }); + } + } + + @override + void dispose() { + _verificationSub?.cancel(); + _unverifiedCountSub?.cancel(); + super.dispose(); + } + + void _onVerifyPressed() { + if (widget.group.isDirectChat && _directContact != null) { + context.push( + Routes.settingsHelpFaqVerifyBadge, + extra: _directContact, + ); + } else { + context.push(Routes.profileGroup(widget.group.groupId)); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + child: Center( + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + Container( + margin: const EdgeInsets.only(top: 40), + constraints: const BoxConstraints(maxWidth: 250), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + borderRadius: BorderRadius.circular(24), + ), + padding: const EdgeInsets.only( + top: 56, + left: 14, + right: 14, + bottom: 14, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.group.groupName, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 8), + VerificationBadgeComp( + group: widget.group, + size: 20, + clickable: false, + ), + ], + ), + if (!_isVerified) ...[ + const SizedBox(height: 5), + Card( + elevation: 0, + color: Theme.of( + context, + ).colorScheme.error.withValues(alpha: 0.1), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 14, + ), + child: Column( + children: [ + Text( + widget.group.isDirectChat + ? context.lang.inChatContactNotVerified + : context.lang.groupMembersNotVerified( + _unverifiedCount, + ), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onErrorContainer, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: _onVerifyPressed, + child: Text(context.lang.unverifiedWarningButton), + ), + ], + ), + ), + ), + ], + ], + ), + ), + Positioned( + top: 0, + child: SizedBox( + width: 80, + height: 80, + child: AvatarIcon(group: widget.group, fontSize: 40), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/views/contact/add_new_contact.view.dart b/lib/src/visual/views/contact/add_new_contact.view.dart index 615aaa1c..dfd3acf4 100644 --- a/lib/src/visual/views/contact/add_new_contact.view.dart +++ b/lib/src/visual/views/contact/add_new_contact.view.dart @@ -102,52 +102,6 @@ class _SearchUsernameView extends State { await SharePlus.instance.share(params); } - void _showMyQrCode() { - // ignore: inference_failure_on_function_invocation - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return Container( - decoration: BoxDecoration( - color: context.color.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(24), - ), - ), - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: double.infinity), - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.color.onSurface.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(height: 24), - const ProfileQrCodeComp(), - const SizedBox(height: 24), - Text( - context.lang.addContactQrSheetSubtext, - style: TextStyle( - fontSize: 14, - color: context.color.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ], - ), - ); - }, - ); - } - @override void dispose() { _contactsStream.cancel(); @@ -344,7 +298,7 @@ class _SearchUsernameView extends State { Expanded( child: MyButton( variant: MyButtonVariant.secondaryDense, - onPressed: _showMyQrCode, + onPressed: () => ProfileQrCodeComp.showSheet(context), child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart index a8cd0bde..fc91aea1 100644 --- a/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart +++ b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart @@ -93,18 +93,25 @@ class _VerificationExpansionTileCompState contact: widget.contact, size: 20, ), - text: context.lang.contactVerifyNumberTitle, + text: context.lang.verifyUserIdentity( + getContactDisplayName(widget.contact), + ), onTap: () async { - await context.push(Routes.settingsHelpFaqVerifyBadge); + await context.push( + Routes.settingsHelpFaqVerifyBadge, + extra: widget.contact, + ); if (mounted) setState(() {}); }, ); } final sharedVerifierIds = _keyVerifications - .where((pair) => - pair.$1.type == VerificationType.contactSharedByVerified && - pair.$1.verifiedBy != null) + .where( + (pair) => + pair.$1.type == VerificationType.contactSharedByVerified && + pair.$1.verifiedBy != null, + ) .map((pair) => pair.$1.verifiedBy!) .toSet(); diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 57f4704e..bbd4bd13 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -12,13 +12,11 @@ import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/profile_selection.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/security_profile.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/share_your_friends.setup.dart'; -import 'package:twonly/src/visual/views/onboarding/setup/verification_badge.setup.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; enum SetupPages { profile, backup, - verificationBadge, profileSelection, securityProfile, shareYourFriends, @@ -40,21 +38,18 @@ extension SetupPagesExtension on SetupPages { return [ SetupPages.profile, SetupPages.backup, - SetupPages.verificationBadge, SetupPages.profileSelection, ]; case SetupProfile.maximum: return [ SetupPages.profile, SetupPages.backup, - SetupPages.verificationBadge, SetupPages.profileSelection, ]; case SetupProfile.customized: return [ SetupPages.profile, SetupPages.backup, - SetupPages.verificationBadge, SetupPages.profileSelection, SetupPages.securityProfile, SetupPages.shareYourFriends, @@ -225,8 +220,6 @@ class _SetupViewState extends State { return const ProfileSelectionSetup(); case SetupPages.securityProfile: return const SecurityProfileSetup(); - case SetupPages.verificationBadge: - return const VerificationBadgeSetupPage(); case SetupPages.shareYourFriends: return ShareYourFriendsSetupPage(state: state); case SetupPages.letYourFriendsFindYou: diff --git a/lib/src/visual/views/onboarding/setup/verification_badge.setup.dart b/lib/src/visual/views/onboarding/setup/verification_badge.setup.dart deleted file mode 100644 index 555dfb4d..00000000 --- a/lib/src/visual/views/onboarding/setup/verification_badge.setup.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/verification_badge_info.comp.dart'; -import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; - -class VerificationBadgeSetupPage extends StatelessWidget { - const VerificationBadgeSetupPage({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.lang.onboardingVerificationBadgeTitle, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 30), - const VerificationBadgeInfo(), - const SizedBox(height: 60), - const NextButtonComp(), - ], - ); - } -} diff --git a/lib/src/visual/views/public_profile.view.dart b/lib/src/visual/views/public_profile.view.dart index cda31a15..c9b588be 100644 --- a/lib/src/visual/views/public_profile.view.dart +++ b/lib/src/visual/views/public_profile.view.dart @@ -83,7 +83,12 @@ class _PublicProfileViewState extends State { BetterListTile( leading: const FaIcon(FontAwesomeIcons.qrcode), text: context.lang.scanOtherProfile, - onTap: () => context.push(Routes.cameraQRScanner), + onTap: () => context.push( + Routes.cameraQRScanner, + extra: { + 'openToVerify': true, + }, + ), ), BetterListTile( leading: const FaIcon( @@ -98,7 +103,8 @@ class _PublicProfileViewState extends State { ), onTap: () { final params = ShareParams( - text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', + text: + 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', ); SharePlus.instance.share(params); }, diff --git a/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart b/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart index c5010cc3..1970ee57 100644 --- a/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart +++ b/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/verification_badge_info.comp.dart'; class VerificationBadeFaqView extends StatefulWidget { - const VerificationBadeFaqView({super.key}); + const VerificationBadeFaqView({super.key, this.contact}); + + final Contact? contact; @override State createState() => @@ -19,9 +22,10 @@ class _VerificationBadeFaqViewState extends State { ), body: ListView( padding: const EdgeInsets.all(40), - children: const [ + children: [ VerificationBadgeInfo( displayButtons: true, + contact: widget.contact, ), ], ),