import "dart:async"; import "package:email_validator/email_validator.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/api/collection/user.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/services/account/user_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import "package:photos/ui/components/dialog_widget.dart"; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/components/text_input_widget.dart"; import 'package:photos/ui/sharing/user_avator_widget.dart'; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/person_contact_linking_util.dart"; import "package:photos/utils/share_util.dart"; class LinkEmailScreen extends StatefulWidget { final String? personID; final bool isFromSaveOrEditPerson; const LinkEmailScreen( this.personID, { this.isFromSaveOrEditPerson = false, super.key, }); @override State createState() => _LinkEmailScreen(); } class _LinkEmailScreen extends State { String? _selectedEmail; String _newEmail = ''; bool _emailIsValid = false; bool isKeypadOpen = false; late List _suggestedUsers; late List _filteredUsers; final _logger = Logger('LinkEmailScreen'); final textFieldFocusNode = FocusNode(); final _textController = TextEditingController(); @override void initState() { super.initState(); _suggestedUsers = _getContacts(); _filteredUsers = _suggestedUsers; } @override void dispose() { _textController.dispose(); textFieldFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( title: Text( S.of(context).linkEmail, ), ), body: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: MenuSectionTitle( title: S.of(context).addANewEmail, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextInputWidget( hintText: S.of(context).email, textEditingController: _textController, shouldSurfaceExecutionStates: false, onChange: (value) { _filteredUsers = _suggestedUsers .where( (element) => element.email.toLowerCase().contains( _textController.text.trim().toLowerCase(), ), ) .toList(); final filterdFilesHaveSelectedEmail = _filteredUsers.any((user) => user.email == _selectedEmail); if (!filterdFilesHaveSelectedEmail) { _selectedEmail = null; } _newEmail = value.trim(); _emailIsValid = EmailValidator.validate(_newEmail); setState(() {}); }, focusNode: textFieldFocusNode, keyboardType: TextInputType.emailAddress, shouldUnfocusOnClearOrSubmit: true, autoCorrect: false, ), ), const SizedBox(height: 16), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ _filteredUsers.isNotEmpty ? MenuSectionTitle( title: S.of(context).orPickFromYourContacts, ) : const SizedBox.shrink(), Expanded( child: ListView.builder( physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { final currentUser = _filteredUsers[index]; return Column( children: [ MenuItemWidget( key: ValueKey(currentUser.email), captionedTextWidget: CaptionedTextWidget( title: currentUser.email, ), leadingIconSize: 24.0, leadingIconWidget: UserAvatarWidget( currentUser, type: AvatarType.mini, ), menuItemColor: getEnteColorScheme(context).fillFaint, pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: (_selectedEmail == currentUser.email) ? Icons.check : null, onTap: () async { textFieldFocusNode.unfocus(); if (_selectedEmail == currentUser.email) { _selectedEmail = null; } else { _selectedEmail = currentUser.email; } setState(() => {}); }, isTopBorderRadiusRemoved: index > 0, isBottomBorderRadiusRemoved: index < (_filteredUsers.length - 1), ), (index == (_filteredUsers.length - 1)) ? const SizedBox.shrink() : DividerWidget( dividerType: DividerType.menu, bgColor: getEnteColorScheme(context).fillFaint, ), ], ); }, itemCount: _filteredUsers.length, ), ), ], ), ), ), SafeArea( child: Padding( padding: const EdgeInsets.only( top: 8, bottom: 8, left: 16, right: 16, ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 8), ButtonWidget( buttonType: ButtonType.primary, buttonSize: ButtonSize.large, labelText: S.of(context).link, isDisabled: !_emailIsValid && (_selectedEmail?.isEmpty ?? true), onTap: _onLinkButtonTap, ), const SizedBox(height: 12), ], ), ), ), ], ), ); } Future _onLinkButtonTap() async { final newEmail = _emailIsValid ? _newEmail : _selectedEmail!; if (widget.isFromSaveOrEditPerson) { await _emailHoldsEnteAccount(newEmail).then((value) { if (value) { Navigator.of(context).pop(newEmail); } }); } else { try { final result = await linkEmailToPerson( newEmail, widget.personID!, context, ); if (!result) { _textController.clear(); return; } Navigator.of(context).pop(newEmail); } catch (e) { await showGenericErrorDialog( context: context, error: e, ); _logger.severe("Failed to link email to person", e); } } } List _getContacts() { final userEmailsToAviod = PersonService.instance.emailToPartialPersonDataMapCache.keys.toSet(); final ownerEmail = Configuration.instance.getEmail(); final relevantUsers = UserService.instance.getRelevantContacts() ..add(User(email: ownerEmail!)) ..removeWhere( (user) => userEmailsToAviod.contains(user.email), ); relevantUsers.sort( (a, b) => (a.email).compareTo(b.email), ); return relevantUsers; } Future _emailHoldsEnteAccount(String email) async { String? publicKey; try { publicKey = await UserService.instance.getPublicKey(email); } catch (e) { _logger.severe("Failed to get public key", e); await showGenericErrorDialog(context: context, error: e); return false; } // getPublicKey can return null when no user is associated with given // email id if (publicKey == null || publicKey == '') { await showDialogWidget( context: context, title: S.of(context).noEnteAccountExclamation, body: context.l10n.emailDoesNotHaveEnteAccount(email), icon: Icons.info_outline, isDismissible: true, buttons: [ ButtonWidget( buttonType: ButtonType.neutral, icon: Icons.adaptive.share, labelText: S.of(context).invite, isInAlert: true, onTap: () async { unawaited( shareText( S.of(context).shareTextRecommendUsingEnte, ), ); }, ), ButtonWidget( buttonType: ButtonType.secondary, labelText: S.of(context).cancel, isInAlert: true, ), ], ); return false; } else { return true; } } Future linkEmailToPerson( String email, String personID, BuildContext context, ) async { if (await checkIfEmailAlreadyAssignedToAPerson(email)) { throw Exception("Email already linked to a person"); } String? publicKey; try { publicKey = await UserService.instance.getPublicKey(email); } catch (e) { _logger.severe("Failed to get public key", e); await showGenericErrorDialog(context: context, error: e); return false; } // getPublicKey can return null when no user is associated with given // email id if (publicKey == null || publicKey == '') { await showDialogWidget( context: context, title: S.of(context).noEnteAccountExclamation, icon: Icons.info_outline, body: S.of(context).emailNoEnteAccount(email), isDismissible: true, buttons: [ ButtonWidget( buttonType: ButtonType.neutral, icon: Icons.adaptive.share, labelText: S.of(context).invite, isInAlert: true, onTap: () async { unawaited( shareText( S.of(context).shareTextRecommendUsingEnte, ), ); }, ), ], ); return false; } else { try { final personEntity = await PersonService.instance.getPerson(personID); late final PersonEntity updatedPerson; if (personEntity == null) { throw AssertionError( "Cannot link email to non-existent person. First save the person", ); } else { updatedPerson = await PersonService.instance .updateAttributes(personID, email: email); } Bus.instance.fire( PeopleChangedEvent( type: PeopleEventType.saveOrEditPerson, source: "linkEmailToPerson", person: updatedPerson, ), ); return true; } catch (e) { _logger.severe("Failed to link email to person", e); await showGenericErrorDialog(context: context, error: e); return false; } } } }