diff --git a/lib/ui/home/memories_widget.dart b/lib/ui/home/memories_widget.dart index 1e0ccfec1..11984d8d6 100644 --- a/lib/ui/home/memories_widget.dart +++ b/lib/ui/home/memories_widget.dart @@ -6,6 +6,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/memory.dart'; import 'package:photos/services/memories_service.dart'; import "package:photos/ui/actions/file/file_actions.dart"; +import "package:photos/ui/extents_page_view.dart"; import 'package:photos/ui/viewer/file/file_widget.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; import 'package:photos/utils/date_time_util.dart'; @@ -431,7 +432,7 @@ class _FullScreenMemoryState extends State { Widget _buildSwiper() { _pageController = PageController(initialPage: _index); - return PageView.builder( + return ExtentsPageView.extents( itemBuilder: (BuildContext context, int index) { if (index < widget.memories.length - 1) { final nextFile = widget.memories[index + 1].file; diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 0b709092c..e535ae144 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -25,6 +25,7 @@ import 'package:photos/ui/settings/social_section_widget.dart'; import 'package:photos/ui/settings/storage_card_widget.dart'; import 'package:photos/ui/settings/support_section_widget.dart'; import 'package:photos/ui/settings/theme_switch_widget.dart'; +import "package:photos/ui/sharing/verify_identity_dialog.dart"; import "package:photos/utils/navigation_util.dart"; class SettingsPage extends StatelessWidget { @@ -51,23 +52,31 @@ class SettingsPage extends StatelessWidget { final enteTextTheme = getEnteTextTheme(context); final List contents = []; contents.add( - Container( - constraints: const BoxConstraints(maxWidth: 350), - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment.centerLeft, - child: AnimatedBuilder( - // [AnimatedBuilder] accepts any [Listenable] subtype. - animation: emailNotifier, - builder: (BuildContext context, Widget? child) { - return Text( - emailNotifier.value!, - style: enteTextTheme.body.copyWith( - color: colorScheme.textMuted, - overflow: TextOverflow.ellipsis, - ), - ); - }, + GestureDetector( + onDoubleTap: () { + _showVerifyIdentityDialog(context); + }, + onLongPress: () { + _showVerifyIdentityDialog(context); + }, + child: Container( + constraints: const BoxConstraints(maxWidth: 350), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: Alignment.centerLeft, + child: AnimatedBuilder( + // [AnimatedBuilder] accepts any [Listenable] subtype. + animation: emailNotifier, + builder: (BuildContext context, Widget? child) { + return Text( + emailNotifier.value!, + style: enteTextTheme.body.copyWith( + color: colorScheme.textMuted, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), ), ), ), @@ -156,4 +165,13 @@ class SettingsPage extends StatelessWidget { ), ); } + + Future _showVerifyIdentityDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return VerifyIdentifyDialog(self: true); + }, + ); + } } diff --git a/lib/ui/sharing/add_partipant_page.dart b/lib/ui/sharing/add_partipant_page.dart index 5c1ae2554..51e73a5ff 100644 --- a/lib/ui/sharing/add_partipant_page.dart +++ b/lib/ui/sharing/add_partipant_page.dart @@ -13,6 +13,8 @@ import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/sharing/user_avator_widget.dart'; +import "package:photos/ui/sharing/verify_identity_dialog.dart"; +import "package:photos/utils/dialog_util.dart"; class AddParticipantPage extends StatefulWidget { final Collection collection; @@ -167,7 +169,7 @@ class _AddParticipantPage extends State { right: 16, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 8), ButtonWidget( @@ -196,7 +198,38 @@ class _AddParticipantPage extends State { } }, ), - const SizedBox(height: 20), + const SizedBox(height: 12), + GestureDetector( + onTap: () async { + if ((selectedEmail == '' && !_emailIsValid)) { + await showErrorDialog( + context, + "Invalid email address", + "Please enter a valid email address.", + ); + return; + } + final emailToAdd = + selectedEmail == '' ? _email : selectedEmail; + showDialog( + context: context, + builder: (BuildContext context) { + return VerifyIdentifyDialog( + self: false, + email: emailToAdd, + ); + }, + ); + }, + child: Text( + "Verify", + textAlign: TextAlign.center, + style: enteTextTheme.smallMuted.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 12), ], ), ), diff --git a/lib/ui/sharing/verify_identity_dialog.dart b/lib/ui/sharing/verify_identity_dialog.dart new file mode 100644 index 000000000..650ab717b --- /dev/null +++ b/lib/ui/sharing/verify_identity_dialog.dart @@ -0,0 +1,209 @@ +import "dart:convert"; + +import 'package:bip39/bip39.dart' as bip39; +import "package:crypto/crypto.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:logging/logging.dart"; +import "package:photos/core/configuration.dart"; +import "package:photos/services/user_service.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/components/button_widget.dart"; +import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/utils/share_util.dart"; + +class VerifyIdentifyDialog extends StatefulWidget { + // email id of the user who's verification ID is being displayed for + // verification + final String email; + + // self is true when the user is viewing their own verification ID + final bool self; + + VerifyIdentifyDialog({ + Key? key, + required this.self, + this.email = '', + }) : super(key: key) { + if (!self && email.isEmpty) { + throw ArgumentError("email cannot be empty when self is false"); + } + } + + @override + State createState() => _VerifyIdentifyDialogState(); +} + +class _VerifyIdentifyDialogState extends State { + final bool doesUserExist = true; + + @override + Widget build(BuildContext context) { + final textStyle = getEnteTextTheme(context); + final String subTitle = widget.self + ? "This is your Verification ID" + : "This is ${widget.email}'s Verification ID"; + final String bottomText = widget.self + ? "Someone sharing albums with you should see the same ID on their " + "device." + : "Please ask them to long-press their email address on the settings " + "screen, and verify that the IDs on both devices match."; + + final AlertDialog alert = AlertDialog( + title: Text(widget.self ? "Verification ID" : "Verify ${widget.email}"), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: _getPublicKey(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final publicKey = snapshot.data!; + if (publicKey.isEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${widget.email} does not have an ente " + "account.\n" + "\nSend them an invite to share photos.", + ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + icon: Icons.adaptive.share, + labelText: "Send invite", + isInAlert: true, + onTap: () async { + shareText( + "Download ente so we can easily share original quality photos" + " and videos\n\nhttps://ente.io/", + ); + }, + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + subTitle, + style: textStyle.bodyMuted, + ), + const SizedBox(height: 20), + _verificationIDWidget(context, publicKey), + const SizedBox(height: 16), + Text( + bottomText, + style: textStyle.bodyMuted, + ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + isInAlert: true, + labelText: widget.self ? "OK" : "Done", + ), + ], + ); + } + } else if (snapshot.hasError) { + Logger("VerificationID") + .severe("failed to end userID", snapshot.error); + return Text( + "Something went wrong", + style: textStyle.bodyMuted, + ); + } else { + return const SizedBox( + height: 200, + child: EnteLoadingWidget(), + ); + } + }, + ), + ], + ), + ); + return alert; + } + + Future _getPublicKey() async { + if (widget.self) { + return Configuration.instance.getKeyAttributes()!.publicKey; + } + final String? userPublicKey = + await UserService.instance.getPublicKey(widget.email); + if (userPublicKey == null) { + // user not found + return ""; + } + return userPublicKey; + } + + Widget _verificationIDWidget(BuildContext context, String publicKey) { + final colorScheme = getEnteColorScheme(context); + final textStyle = getEnteTextTheme(context); + final String verificationID = _generateVerificationID(publicKey); + return DottedBorder( + color: colorScheme.strokeMuted, + //color of dotted/dash line + strokeWidth: 1, + + dashPattern: const [12, 6], + radius: const Radius.circular(8), + //dash patterns, 10 is dash width, 6 is space width + child: Column( + children: [ + GestureDetector( + onTap: () async { + if (verificationID.isEmpty) { + return; + } + await Clipboard.setData( + ClipboardData(text: verificationID), + ); + shareText( + widget.self + ? "Here's my verification ID: " + "$verificationID for ente.io." + : "Hey, " + "can you confirm that " + "this is your ente.io verification " + "ID: $verificationID", + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(2), + ), + color: colorScheme.backgroundElevated2, + ), + padding: const EdgeInsets.all(20), + width: double.infinity, + child: Text( + verificationID, + style: textStyle.bodyBold, + ), + ), + ), + ], + ), + ); + } + + String _generateVerificationID(String publicKey) { + final inputBytes = base64.decode(publicKey); + final shaValue = sha256.convert(inputBytes); + return bip39.generateMnemonic( + strength: 256, + randomBytes: (int size) { + return Uint8List.fromList(shaValue.bytes); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0b23a8530..90327b459 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,7 +249,7 @@ packages: source: hosted version: "0.3.3+4" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 diff --git a/pubspec.yaml b/pubspec.yaml index 94eb50b08..ae0fec3c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: collection: # dart computer: ^2.0.0 confetti: ^0.6.0 + crypto: ^3.0.2 connectivity_plus: ^3.0.3 cupertino_icons: ^1.0.0 device_info: ^2.0.2