mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 02:24:07 +00:00
Improve: Visibility of the verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
cda52a5da8
commit
cda7df36f3
19 changed files with 588 additions and 147 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue