add_partipant_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import 'package:email_validator/email_validator.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:photos/core/configuration.dart';
  4. import 'package:photos/models/collection.dart';
  5. import 'package:photos/services/collections_service.dart';
  6. import 'package:photos/theme/ente_theme.dart';
  7. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  8. import 'package:photos/ui/components/buttons/button_widget.dart';
  9. import 'package:photos/ui/components/captioned_text_widget.dart';
  10. import 'package:photos/ui/components/divider_widget.dart';
  11. import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
  12. import 'package:photos/ui/components/menu_section_description_widget.dart';
  13. import 'package:photos/ui/components/menu_section_title.dart';
  14. import 'package:photos/ui/components/models/button_type.dart';
  15. import 'package:photos/ui/sharing/user_avator_widget.dart';
  16. import "package:photos/ui/sharing/verify_identity_dialog.dart";
  17. import "package:photos/utils/dialog_util.dart";
  18. class AddParticipantPage extends StatefulWidget {
  19. final Collection collection;
  20. final bool isAddingViewer;
  21. const AddParticipantPage(this.collection, this.isAddingViewer, {super.key});
  22. @override
  23. State<StatefulWidget> createState() => _AddParticipantPage();
  24. }
  25. class _AddParticipantPage extends State<AddParticipantPage> {
  26. String selectedEmail = '';
  27. String _email = '';
  28. bool isEmailListEmpty = false;
  29. bool _emailIsValid = false;
  30. bool isKeypadOpen = false;
  31. late CollectionActions collectionActions;
  32. // Focus nodes are necessary
  33. final textFieldFocusNode = FocusNode();
  34. final _textController = TextEditingController();
  35. @override
  36. void initState() {
  37. collectionActions = CollectionActions(CollectionsService.instance);
  38. super.initState();
  39. }
  40. @override
  41. void dispose() {
  42. _textController.dispose();
  43. super.dispose();
  44. }
  45. @override
  46. Widget build(BuildContext context) {
  47. isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
  48. final enteTextTheme = getEnteTextTheme(context);
  49. final enteColorScheme = getEnteColorScheme(context);
  50. final List<User> suggestedUsers = _getSuggestedUser();
  51. isEmailListEmpty = suggestedUsers.isEmpty;
  52. return Scaffold(
  53. resizeToAvoidBottomInset: isKeypadOpen,
  54. appBar: AppBar(
  55. title: Text(widget.isAddingViewer ? "Add viewer" : "Add collaborator"),
  56. ),
  57. body: Column(
  58. mainAxisAlignment: MainAxisAlignment.start,
  59. crossAxisAlignment: CrossAxisAlignment.start,
  60. children: [
  61. const SizedBox(height: 12),
  62. Padding(
  63. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  64. child: Text(
  65. "Add a new email",
  66. style: enteTextTheme.small
  67. .copyWith(color: enteColorScheme.textMuted),
  68. ),
  69. ),
  70. const SizedBox(height: 4),
  71. Padding(
  72. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  73. child: _getEmailField(),
  74. ),
  75. (isEmailListEmpty && widget.isAddingViewer)
  76. ? const Expanded(child: SizedBox.shrink())
  77. : Expanded(
  78. child: Padding(
  79. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  80. child: Column(
  81. children: [
  82. !isEmailListEmpty
  83. ? const MenuSectionTitle(
  84. title: "Or pick an existing one",
  85. )
  86. : const SizedBox.shrink(),
  87. Expanded(
  88. child: ListView.builder(
  89. itemBuilder: (context, index) {
  90. if (index >= suggestedUsers.length) {
  91. return const Padding(
  92. padding: EdgeInsets.symmetric(
  93. vertical: 8.0,
  94. ),
  95. child: MenuSectionDescriptionWidget(
  96. content:
  97. "Collaborators can add photos and videos to the shared album.",
  98. ),
  99. );
  100. }
  101. final currentUser = suggestedUsers[index];
  102. return Column(
  103. children: [
  104. MenuItemWidget(
  105. captionedTextWidget: CaptionedTextWidget(
  106. title: currentUser.email,
  107. ),
  108. leadingIconSize: 24.0,
  109. leadingIconWidget: UserAvatarWidget(
  110. currentUser,
  111. type: AvatarType.mini,
  112. ),
  113. menuItemColor:
  114. getEnteColorScheme(context).fillFaint,
  115. pressedColor:
  116. getEnteColorScheme(context).fillFaint,
  117. trailingIcon:
  118. (selectedEmail == currentUser.email)
  119. ? Icons.check
  120. : null,
  121. onTap: () async {
  122. textFieldFocusNode.unfocus();
  123. if (selectedEmail == currentUser.email) {
  124. selectedEmail = '';
  125. } else {
  126. selectedEmail = currentUser.email;
  127. }
  128. setState(() => {});
  129. // showShortToast(context, "yet to implement");
  130. },
  131. isTopBorderRadiusRemoved: index > 0,
  132. isBottomBorderRadiusRemoved:
  133. index < (suggestedUsers.length - 1),
  134. ),
  135. (index == (suggestedUsers.length - 1))
  136. ? const SizedBox.shrink()
  137. : DividerWidget(
  138. dividerType: DividerType.menu,
  139. bgColor: getEnteColorScheme(context)
  140. .fillFaint,
  141. ),
  142. ],
  143. );
  144. },
  145. itemCount: suggestedUsers.length +
  146. (widget.isAddingViewer ? 0 : 1),
  147. // physics: const ClampingScrollPhysics(),
  148. ),
  149. ),
  150. ],
  151. ),
  152. ),
  153. ),
  154. SafeArea(
  155. child: Padding(
  156. padding: const EdgeInsets.only(
  157. top: 8,
  158. bottom: 8,
  159. left: 16,
  160. right: 16,
  161. ),
  162. child: Column(
  163. crossAxisAlignment: CrossAxisAlignment.center,
  164. children: [
  165. const SizedBox(height: 8),
  166. ButtonWidget(
  167. buttonType: ButtonType.primary,
  168. buttonSize: ButtonSize.large,
  169. labelText: widget.isAddingViewer
  170. ? "Add viewer"
  171. : "Add collaborator",
  172. isDisabled: (selectedEmail == '' && !_emailIsValid),
  173. onTap: (selectedEmail == '' && !_emailIsValid)
  174. ? null
  175. : () async {
  176. final emailToAdd =
  177. selectedEmail == '' ? _email : selectedEmail;
  178. final result =
  179. await collectionActions.addEmailToCollection(
  180. context,
  181. widget.collection,
  182. emailToAdd,
  183. widget.isAddingViewer
  184. ? CollectionParticipantRole.viewer
  185. : CollectionParticipantRole.collaborator,
  186. );
  187. if (result && mounted) {
  188. Navigator.of(context).pop(true);
  189. }
  190. },
  191. ),
  192. const SizedBox(height: 12),
  193. GestureDetector(
  194. onTap: () async {
  195. if ((selectedEmail == '' && !_emailIsValid)) {
  196. await showErrorDialog(
  197. context,
  198. "Invalid email address",
  199. "Please enter a valid email address.",
  200. );
  201. return;
  202. }
  203. final emailToAdd =
  204. selectedEmail == '' ? _email : selectedEmail;
  205. showDialog(
  206. context: context,
  207. builder: (BuildContext context) {
  208. return VerifyIdentifyDialog(
  209. self: false,
  210. email: emailToAdd,
  211. );
  212. },
  213. );
  214. },
  215. child: Text(
  216. "Verify",
  217. textAlign: TextAlign.center,
  218. style: enteTextTheme.smallMuted.copyWith(
  219. decoration: TextDecoration.underline,
  220. ),
  221. ),
  222. ),
  223. const SizedBox(height: 12),
  224. ],
  225. ),
  226. ),
  227. ),
  228. ],
  229. ),
  230. );
  231. }
  232. void clearFocus() {
  233. _textController.clear();
  234. _email = _textController.text;
  235. _emailIsValid = false;
  236. textFieldFocusNode.unfocus();
  237. setState(() => {});
  238. }
  239. Widget _getEmailField() {
  240. return TextFormField(
  241. controller: _textController,
  242. focusNode: textFieldFocusNode,
  243. style: getEnteTextTheme(context).body,
  244. autofillHints: const [AutofillHints.email],
  245. decoration: InputDecoration(
  246. focusedBorder: OutlineInputBorder(
  247. borderRadius: const BorderRadius.all(Radius.circular(4.0)),
  248. borderSide:
  249. BorderSide(color: getEnteColorScheme(context).strokeMuted),
  250. ),
  251. fillColor: getEnteColorScheme(context).fillFaint,
  252. filled: true,
  253. hintText: 'Enter email',
  254. contentPadding: const EdgeInsets.symmetric(
  255. horizontal: 16,
  256. vertical: 14,
  257. ),
  258. border: UnderlineInputBorder(
  259. borderSide: BorderSide.none,
  260. borderRadius: BorderRadius.circular(4),
  261. ),
  262. prefixIcon: Icon(
  263. Icons.email_outlined,
  264. color: getEnteColorScheme(context).strokeMuted,
  265. ),
  266. suffixIcon: _email == ''
  267. ? null
  268. : IconButton(
  269. onPressed: clearFocus,
  270. icon: Icon(
  271. Icons.cancel,
  272. color: getEnteColorScheme(context).strokeMuted,
  273. ),
  274. ),
  275. ),
  276. onChanged: (value) {
  277. if (selectedEmail != '') {
  278. selectedEmail = '';
  279. }
  280. _email = value.trim();
  281. _emailIsValid = EmailValidator.validate(_email);
  282. setState(() {});
  283. },
  284. autocorrect: false,
  285. keyboardType: TextInputType.emailAddress,
  286. //initialValue: _email,
  287. textInputAction: TextInputAction.next,
  288. );
  289. }
  290. List<User> _getSuggestedUser() {
  291. final List<User> suggestedUsers = [];
  292. final Set<int> existingUserIDs = {};
  293. final int ownerID = Configuration.instance.getUserID()!;
  294. for (final User? u in widget.collection.sharees ?? []) {
  295. if (u != null && u.id != null) {
  296. existingUserIDs.add(u.id!);
  297. }
  298. }
  299. for (final c in CollectionsService.instance.getActiveCollections()) {
  300. if (c.owner?.id == ownerID) {
  301. for (final User? u in c.sharees ?? []) {
  302. if (u != null && u.id != null && !existingUserIDs.contains(u.id)) {
  303. existingUserIDs.add(u.id!);
  304. suggestedUsers.add(u);
  305. }
  306. }
  307. } else if (c.owner != null &&
  308. c.owner!.id != null &&
  309. !existingUserIDs.contains(c.owner!.id!)) {
  310. existingUserIDs.add(c.owner!.id!);
  311. suggestedUsers.add(c.owner!);
  312. }
  313. }
  314. if (_textController.text.trim().isNotEmpty) {
  315. suggestedUsers.removeWhere(
  316. (element) => !element.email
  317. .toLowerCase()
  318. .contains(_textController.text.trim().toLowerCase()),
  319. );
  320. }
  321. suggestedUsers.sort((a, b) => a.email.compareTo(b.email));
  322. return suggestedUsers;
  323. }
  324. }