ente/mobile/lib/ui/viewer/people/link_email_screen.dart
Neeraj Gupta be6ca79c2d move
2025-03-03 17:21:31 +05:30

385 lines
13 KiB
Dart

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<StatefulWidget> createState() => _LinkEmailScreen();
}
class _LinkEmailScreen extends State<LinkEmailScreen> {
String? _selectedEmail;
String _newEmail = '';
bool _emailIsValid = false;
bool isKeypadOpen = false;
late List<User> _suggestedUsers;
late List<User> _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<void> _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<User> _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<bool> _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<bool> 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;
}
}
}
}