mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-25 06:44:07 +00:00
better feedback in case a qr code was shown
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
0e377d0ebc
commit
75079a790c
9 changed files with 239 additions and 140 deletions
|
|
@ -2567,7 +2567,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verifiedPublicKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The public key of {username} has been verified and is valid.'**
|
||||
/// **'The identity of {username} has been successfully verified.'**
|
||||
String verifiedPublicKey(Object username);
|
||||
|
||||
/// No description provided for @memoriesAYearAgo.
|
||||
|
|
|
|||
|
|
@ -1429,7 +1429,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String verifiedPublicKey(Object username) {
|
||||
return 'Der öffentliche Schlüssel von $username wurde überprüft und ist gültig.';
|
||||
return 'Die Identität von $username wurde erfolgreich überprüft.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1418,7 +1418,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String verifiedPublicKey(Object username) {
|
||||
return 'The public key of $username has been verified and is valid.';
|
||||
return 'The identity of $username has been successfully verified.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 673f6d8c3036d64060b1114912bd5bf5515d5420
|
||||
Subproject commit 2e608f2d5acc26ee001877b673e361884fb3d0ca
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -13,7 +14,7 @@ import 'package:twonly/src/services/signal/identity.signal.dart';
|
|||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||
|
||||
class KeyVerificationService {
|
||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||
|
|
@ -77,12 +78,14 @@ class KeyVerificationService {
|
|||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context != null && context.mounted && contact != null) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.secretQrTokenVerifiedSnackbar(
|
||||
getContactDisplayName(contact),
|
||||
unawaited(
|
||||
VerificationSuccessDialog.show(
|
||||
context,
|
||||
contact,
|
||||
message: context.lang.secretQrTokenVerifiedSnackbar(
|
||||
getContactDisplayName(contact),
|
||||
),
|
||||
),
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
92
lib/src/visual/components/add_contact_dialog.comp.dart
Normal file
92
lib/src/visual/components/add_contact_dialog.comp.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
/// A premium popup dialog shown when a new user profile is scanned via QR code.
|
||||
/// Allows the user to request connection ("Anfragen") or cancel ("Abbrechen").
|
||||
class AddContactDialog extends StatelessWidget {
|
||||
const AddContactDialog({
|
||||
required this.profile,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PublicProfile profile;
|
||||
|
||||
/// Utility method to easily present this dialog.
|
||||
/// Returns `true` if the user chose to request the contact, `false` otherwise.
|
||||
static Future<bool?> show(BuildContext context, PublicProfile profile) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AddContactDialog(profile: profile),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const AvatarIcon(
|
||||
fontSize: 16,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.username,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
context.lang.userFoundBody(profile.username),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondary,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.lang.friendSuggestionsRequest),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import 'package:flutter/material.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/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_animation.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
/// A premium popup dialog shown when a contact's public key has been successfully verified.
|
||||
class VerificationSuccessDialog extends StatelessWidget {
|
||||
const VerificationSuccessDialog({
|
||||
required this.contact,
|
||||
this.message,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Contact contact;
|
||||
final String? message;
|
||||
|
||||
/// Utility method to easily present this dialog.
|
||||
static Future<void> show(BuildContext context, Contact contact, {String? message}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => VerificationSuccessDialog(contact: contact, message: message),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = getContactDisplayName(contact);
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 28,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AvatarIcon(
|
||||
contactId: contact.userId,
|
||||
fontSize: 16,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
displayName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const VerificationSuccessAnimation(
|
||||
size: 160,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
message ?? context.lang.verifiedPublicKey(displayName),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
MyButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.lang.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
|
|
@ -27,113 +23,14 @@ class CameraScannedOverlay extends StatelessWidget {
|
|||
width: 150,
|
||||
child: ListView(
|
||||
children: [
|
||||
...mainController.scannedNewProfiles.values.map(
|
||||
(c) => _buildScannedProfileTile(context, c),
|
||||
),
|
||||
...mainController.contactsVerified.values.map(
|
||||
(c) => _buildVerifiedContactTile(context, c),
|
||||
),
|
||||
if (mainController.scannedUrl != null) _buildScannedUrlTile(context, mainController.scannedUrl!),
|
||||
if (mainController.scannedUrl != null)
|
||||
_buildScannedUrlTile(context, mainController.scannedUrl!),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScannedProfileTile(BuildContext context, ScannedNewProfile c) {
|
||||
if (c.isLoading) return Container();
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
c.isLoading = true;
|
||||
mainController.setState?.call();
|
||||
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.requestedUserToastText(c.profile.username),
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
if (await addNewContactFromPublicProfile(c.profile) && context.mounted) {
|
||||
// showSnackbar(
|
||||
// context,
|
||||
// context.lang.requestedUserToastText(c.profile.username),
|
||||
// level: SnackbarLevel.success,
|
||||
// );
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: context.color.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(c.profile.username),
|
||||
Expanded(child: Container()),
|
||||
if (c.isLoading)
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.userPlus,
|
||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
size: 17,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerifiedContactTile(
|
||||
BuildContext context,
|
||||
ScannedVerifiedContact c,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: context.color.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AvatarIcon(
|
||||
contactId: c.contact.userId,
|
||||
fontSize: 14,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
getContactDisplayName(c.contact, maxLength: 9),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
width: 30,
|
||||
child: Lottie.asset(
|
||||
c.verificationOk ? 'assets/animations/success.lottie' : 'assets/animations/failed.lottie',
|
||||
repeat: false,
|
||||
onLoaded: (p0) {
|
||||
Future.delayed(const Duration(seconds: 4), () {
|
||||
mainController.setState?.call();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScannedUrlTile(BuildContext context, String url) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/helpers/screenshot.helper.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/face_filters.dart';
|
||||
|
|
@ -141,7 +142,8 @@ class MainCameraController {
|
|||
|
||||
if (init) {
|
||||
for (; cameraId < AppEnvironment.cameras.length; cameraId++) {
|
||||
if (AppEnvironment.cameras[cameraId].lensDirection == CameraLensDirection.back) {
|
||||
if (AppEnvironment.cameras[cameraId].lensDirection ==
|
||||
CameraLensDirection.back) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +159,9 @@ class MainCameraController {
|
|||
AppEnvironment.cameras[cameraId],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: hasMic,
|
||||
imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,
|
||||
imageFormatGroup: Platform.isAndroid
|
||||
? ImageFormatGroup.nv21
|
||||
: ImageFormatGroup.bgra8888,
|
||||
);
|
||||
try {
|
||||
_initializeFuture = cameraController?.initialize();
|
||||
|
|
@ -210,10 +214,14 @@ class MainCameraController {
|
|||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
||||
);
|
||||
if (cameraController == null) return;
|
||||
selectedCameraDetails.maxAvailableZoom = await cameraController?.getMaxZoomLevel() ?? 1;
|
||||
selectedCameraDetails.minAvailableZoom = await cameraController?.getMinZoomLevel() ?? 1;
|
||||
selectedCameraDetails.maxAvailableZoom =
|
||||
await cameraController?.getMaxZoomLevel() ?? 1;
|
||||
selectedCameraDetails.minAvailableZoom =
|
||||
await cameraController?.getMinZoomLevel() ?? 1;
|
||||
selectedCameraDetails
|
||||
..isZoomAble = selectedCameraDetails.maxAvailableZoom != selectedCameraDetails.minAvailableZoom
|
||||
..isZoomAble =
|
||||
selectedCameraDetails.maxAvailableZoom !=
|
||||
selectedCameraDetails.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = cameraId;
|
||||
|
||||
|
|
@ -235,7 +243,8 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
Future<void> onTapDown(TapDownDetails details) async {
|
||||
final box = cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final box =
|
||||
cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
|
||||
|
|
@ -251,7 +260,8 @@ class MainCameraController {
|
|||
await cameraController?.setFocusPoint(Offset(dx, dy));
|
||||
await cameraController?.setFocusMode(FocusMode.auto);
|
||||
} catch (e) {
|
||||
if (e is CameraException && (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
|
||||
if (e is CameraException &&
|
||||
(e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
|
||||
Log.info('Focus point or mode not supported on this device');
|
||||
} else {
|
||||
Log.warn(e);
|
||||
|
|
@ -292,7 +302,8 @@ class MainCameraController {
|
|||
if (inputImage == null) return;
|
||||
_processBarcode(inputImage);
|
||||
// check if front camera is selected
|
||||
if (cameraController?.description.lensDirection == CameraLensDirection.front) {
|
||||
if (cameraController?.description.lensDirection ==
|
||||
CameraLensDirection.front) {
|
||||
if (_currentFilterType != FaceFilterType.none) {
|
||||
_processFaces(inputImage);
|
||||
}
|
||||
|
|
@ -311,14 +322,16 @@ class MainCameraController {
|
|||
if (Platform.isIOS) {
|
||||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||
} else if (Platform.isAndroid) {
|
||||
var rotationCompensation = _orientations[cameraController!.value.deviceOrientation];
|
||||
var rotationCompensation =
|
||||
_orientations[cameraController!.value.deviceOrientation];
|
||||
if (rotationCompensation == null) return null;
|
||||
if (camera.lensDirection == CameraLensDirection.front) {
|
||||
// front-facing
|
||||
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
|
||||
} else {
|
||||
// back-facing
|
||||
rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
|
||||
rotationCompensation =
|
||||
(sensorOrientation - rotationCompensation + 360) % 360;
|
||||
}
|
||||
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
||||
}
|
||||
|
|
@ -360,7 +373,9 @@ class MainCameraController {
|
|||
if (_isBusy) return;
|
||||
_isBusy = true;
|
||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||
if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) {
|
||||
if (inputImage.metadata?.size != null &&
|
||||
inputImage.metadata?.rotation != null &&
|
||||
cameraController != null) {
|
||||
final painter = BarcodeDetectorPainter(
|
||||
barcodes,
|
||||
inputImage.metadata!.size,
|
||||
|
|
@ -397,11 +412,21 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
if (contact == null || contact.deletedByUser) {
|
||||
if (scannedNewProfiles[profile.userId.toInt()] == null) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
|
||||
profile: profile,
|
||||
final context = cameraPreviewKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
final shouldRequest = await AddContactDialog.show(
|
||||
context,
|
||||
profile,
|
||||
);
|
||||
if (shouldRequest == true && context.mounted) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.requestedUserToastText(profile.username),
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
await addNewContactFromPublicProfile(profile);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -412,16 +437,10 @@ class MainCameraController {
|
|||
verificationOk: verificationOk,
|
||||
);
|
||||
|
||||
await HapticFeedback.heavyImpact();
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
final context = cameraPreviewKey.currentContext;
|
||||
if (verificationOk && context != null && context.mounted) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.verifiedPublicKey(
|
||||
getContactDisplayName(contact),
|
||||
),
|
||||
level: SnackbarLevel.success,
|
||||
);
|
||||
await VerificationSuccessDialog.show(context, contact);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
|
@ -444,7 +463,9 @@ class MainCameraController {
|
|||
if (_isBusyFaces) return;
|
||||
_isBusyFaces = true;
|
||||
final faces = await _faceDetector.processImage(inputImage);
|
||||
if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) {
|
||||
if (inputImage.metadata?.size != null &&
|
||||
inputImage.metadata?.rotation != null &&
|
||||
cameraController != null) {
|
||||
if (faces.isNotEmpty) {
|
||||
CustomPainter? painter;
|
||||
switch (_currentFilterType) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue