Improve: Visibility of the verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-07-09 18:09:16 +02:00
parent cda52a5da8
commit cda7df36f3
19 changed files with 588 additions and 147 deletions

View file

@ -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

View file

@ -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';

View file

@ -259,6 +259,54 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
});
}
Stream<int> 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 = <int, ({bool direct, bool partial})>{};
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<void> addKeyVerification(
int contactId,
VerificationType type, {

View file

@ -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:

View file

@ -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';

View file

@ -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';

@ -1 +1 @@
Subproject commit 147f44e608f39f04af3b3be9aee1ad662e5a84e1
Subproject commit 27b598a151a0122f05670d5d8fb6dcfd5f440127

View file

@ -143,7 +143,13 @@ final routerProvider = GoRouter(
GoRoute(
path: Routes.cameraQRScanner,
builder: (context, state) {
return const QrCodeScannerView();
final extra = state.extra as Map<String, dynamic>?;
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?,
),
),
],
),

View file

@ -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<void> 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<ProfileQrCodeComp> createState() => _ProfileQrCodeCompState();
}

View file

@ -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)

View file

@ -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<QrCodeScannerView> createState() => QrCodeScannerViewState();
}
@ -56,6 +67,31 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
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,
),
),
),
],
),
);

View file

@ -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<ChatMessagesView>
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<ChatMessagesView>
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<ChatMessagesView>
}
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<ChatMessagesView>
},
),
),
),
if (quotesMessage != null)
Container(
padding: const EdgeInsets.only(

View file

@ -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<InChatGroupOverview> createState() => _InChatGroupOverviewState();
}
class _InChatGroupOverviewState extends State<InChatGroupOverview> {
Contact? _directContact;
StreamSubscription<dynamic>? _verificationSub;
StreamSubscription<int>? _unverifiedCountSub;
bool _isVerified = true;
int _unverifiedCount = 0;
@override
void initState() {
super.initState();
_initVerificationCheck();
}
Future<void> _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),
),
),
],
),
),
);
}
}

View file

@ -102,52 +102,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
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<AddNewUserView> {
Expanded(
child: MyButton(
variant: MyButtonVariant.secondaryDense,
onPressed: _showMyQrCode,
onPressed: () => ProfileQrCodeComp.showSheet(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [

View file

@ -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();

View file

@ -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<SetupView> {
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:

View file

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

View file

@ -83,7 +83,12 @@ class _PublicProfileViewState extends State<PublicProfileView> {
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<PublicProfileView> {
),
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);
},

View file

@ -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<VerificationBadeFaqView> createState() =>
@ -19,9 +22,10 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
),
body: ListView(
padding: const EdgeInsets.all(40),
children: const [
children: [
VerificationBadgeInfo(
displayButtons: true,
contact: widget.contact,
),
],
),