From 46c941fb8cbf12e45ca7cfee2ab8fbd9c25b7854 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 5 Feb 2025 17:28:13 +0530 Subject: [PATCH 1/2] [mob][photos] Use first letter avatar for contact in contacts section and all contacts screen if no person is linked to contact --- .../lib/models/search/search_constants.dart | 1 + mobile/lib/services/search_service.dart | 2 + mobile/lib/ui/sharing/user_avator_widget.dart | 80 ++++++++++++++++--- .../result/search_thumbnail_widget.dart | 43 +++++----- .../viewer/search_tab/contacts_section.dart | 49 +++++------- 5 files changed, 112 insertions(+), 63 deletions(-) diff --git a/mobile/lib/models/search/search_constants.dart b/mobile/lib/models/search/search_constants.dart index 2bdb03673fd..60c178192bb 100644 --- a/mobile/lib/models/search/search_constants.dart +++ b/mobile/lib/models/search/search_constants.dart @@ -2,3 +2,4 @@ const kPersonParamID = 'person_id'; const kPersonWidgetKey = 'person_widget_key'; const kClusterParamId = 'cluster_id'; const kFileID = 'file_id'; +const kContactEmail = 'contact_email'; diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index cfa708610d6..e8747bd3503 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1728,6 +1728,7 @@ class SearchService { ), params: { kPersonParamID: key.linkedPersonID, + kContactEmail: key.email, }, ), ); @@ -1803,6 +1804,7 @@ class SearchService { ), params: { kPersonParamID: key.linkedPersonID, + kContactEmail: key.email, }, ), ); diff --git a/mobile/lib/ui/sharing/user_avator_widget.dart b/mobile/lib/ui/sharing/user_avator_widget.dart index 300b83d744e..8b06f09edef 100644 --- a/mobile/lib/ui/sharing/user_avator_widget.dart +++ b/mobile/lib/ui/sharing/user_avator_widget.dart @@ -3,6 +3,7 @@ import "dart:async"; import "package:collection/collection.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/extensions/user_extension.dart"; @@ -130,7 +131,7 @@ class _UserAvatarWidgetState extends State { ); } else if (snapshot.hasError) { _logger.severe("Error loading personID", snapshot.error); - return _FirstLetterAvatar( + return _FirstLetterCircularAvatar( user: widget.user, currentUserID: widget.currentUserID, thumbnailView: widget.thumbnailView, @@ -138,7 +139,7 @@ class _UserAvatarWidgetState extends State { ); } else if (snapshot.connectionState == ConnectionState.done && snapshot.data == null) { - return _FirstLetterAvatar( + return _FirstLetterCircularAvatar( user: widget.user, currentUserID: widget.currentUserID, thumbnailView: widget.thumbnailView, @@ -150,7 +151,7 @@ class _UserAvatarWidgetState extends State { ), ), ) - : _FirstLetterAvatar( + : _FirstLetterCircularAvatar( user: widget.user, currentUserID: widget.currentUserID, thumbnailView: widget.thumbnailView, @@ -159,12 +160,12 @@ class _UserAvatarWidgetState extends State { } } -class _FirstLetterAvatar extends StatefulWidget { +class _FirstLetterCircularAvatar extends StatefulWidget { final User user; final int currentUserID; final bool thumbnailView; final AvatarType type; - const _FirstLetterAvatar({ + const _FirstLetterCircularAvatar({ required this.user, required this.currentUserID, required this.thumbnailView, @@ -172,10 +173,12 @@ class _FirstLetterAvatar extends StatefulWidget { }); @override - State<_FirstLetterAvatar> createState() => _FirstLetterAvatarState(); + State<_FirstLetterCircularAvatar> createState() => + _FirstLetterCircularAvatarState(); } -class _FirstLetterAvatarState extends State<_FirstLetterAvatar> { +class _FirstLetterCircularAvatarState + extends State<_FirstLetterCircularAvatar> { @override Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); @@ -186,13 +189,12 @@ class _FirstLetterAvatarState extends State<_FirstLetterAvatar> { : widget.user.email.substring(0, 1)) : widget.user.displayName!.substring(0, 1); Color decorationColor; - if (widget.user.id == null || - widget.user.id! <= 0 || - widget.user.id == widget.currentUserID) { + if ((widget.user.id != null && widget.user.id! < 0) || + widget.user.email == Configuration.instance.getEmail()) { decorationColor = Colors.black; } else { - decorationColor = colorScheme.avatarColors[ - (widget.user.id!).remainder(colorScheme.avatarColors.length)]; + decorationColor = colorScheme.avatarColors[(widget.user.email.length) + .remainder(colorScheme.avatarColors.length)]; } final avatarStyle = getAvatarStyle(context, widget.type); @@ -257,3 +259,57 @@ double getAvatarSize( return 18.0; } } + +class FirstLetterUserAvatar extends StatefulWidget { + final User user; + const FirstLetterUserAvatar(this.user, {super.key}); + + @override + State createState() => _FirstLetterUserAvatarState(); +} + +class _FirstLetterUserAvatarState extends State { + final currentUserEmail = Configuration.instance.getEmail(); + late User user; + + @override + void initState() { + super.initState(); + user = widget.user; + } + + @override + void didUpdateWidget(covariant FirstLetterUserAvatar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.user != widget.user) { + setState(() { + user = widget.user; + }); + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + final displayChar = (user.displayName == null || user.displayName!.isEmpty) + ? ((user.email.isEmpty) ? " " : user.email.substring(0, 1)) + : user.displayName!.substring(0, 1); + Color decorationColor; + if ((widget.user.id != null && widget.user.id! < 0) || + user.email == currentUserEmail) { + decorationColor = Colors.black; + } else { + decorationColor = colorScheme.avatarColors[ + (user.email.length).remainder(colorScheme.avatarColors.length)]; + } + return Container( + color: decorationColor, + child: Center( + child: Text( + displayChar.toUpperCase(), + style: getEnteTextTheme(context).small.copyWith(color: Colors.white), + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart b/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart index 6126f4d7626..ec0be2f4473 100644 --- a/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart +++ b/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:logging/logging.dart"; +import "package:photos/models/api/collection/user.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/search/generic_search_result.dart"; import "package:photos/models/search/search_constants.dart"; @@ -8,6 +9,7 @@ import "package:photos/models/search/search_types.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/sharing/user_avator_widget.dart"; import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; import 'package:photos/ui/viewer/search/result/person_face_widget.dart'; @@ -73,14 +75,14 @@ class _ContactSearchThumbnailWidgetState extends State { Future? _mostRecentFileOfPerson; late String? _personID; + late String _email; final _logger = Logger("_ContactSearchThumbnailWidgetState"); - late final EnteFile? _previewThumbnail; @override void initState() { super.initState(); - _previewThumbnail = widget.searchResult.previewThumbnail(); _personID = widget.searchResult.params[kPersonParamID]; + _email = widget.searchResult.params[kContactEmail]; if (_personID != null) { _mostRecentFileOfPerson = PersonService.instance.getPerson(_personID!).then((person) { @@ -118,35 +120,38 @@ class _ContactSearchThumbnailWidgetState ); } else if (snapshot.connectionState == ConnectionState.done && snapshot.data == null) { - return _previewThumbnail != null - ? ThumbnailWidget( - _previewThumbnail!, - ) - : const NoFaceOrFileContactWidget(); + return NoFaceForContactWidget( + user: User(email: _email), + ); } else if (snapshot.hasError) { _logger.severe( "Error loading personID", snapshot.error, ); - return const NoFaceOrFileContactWidget(); + return NoFaceForContactWidget( + user: User(email: _email), + ); } else { return const EnteLoadingWidget(); } }, ) - : _previewThumbnail != null - ? ThumbnailWidget( - _previewThumbnail!, - ) - : const NoFaceOrFileContactWidget(), + : NoFaceForContactWidget( + user: User(email: _email), + ), ), ); } } -class NoFaceOrFileContactWidget extends StatelessWidget { +class NoFaceForContactWidget extends StatelessWidget { + final User user; final bool addBorder; - const NoFaceOrFileContactWidget({this.addBorder = true, super.key}); + const NoFaceForContactWidget({ + this.addBorder = true, + required this.user, + super.key, + }); @override Widget build(BuildContext context) { @@ -164,13 +169,7 @@ class NoFaceOrFileContactWidget extends StatelessWidget { : null, color: enteColorScheme.fillFaint, ), - child: Center( - child: Icon( - Icons.person_2_outlined, - color: enteColorScheme.strokeMuted, - size: 24, - ), - ), + child: Center(child: FirstLetterUserAvatar(user)), ); } } diff --git a/mobile/lib/ui/viewer/search_tab/contacts_section.dart b/mobile/lib/ui/viewer/search_tab/contacts_section.dart index 72eca2ac899..8f6ed6354d9 100644 --- a/mobile/lib/ui/viewer/search_tab/contacts_section.dart +++ b/mobile/lib/ui/viewer/search_tab/contacts_section.dart @@ -6,6 +6,7 @@ import "package:logging/logging.dart"; import "package:photos/core/constants.dart"; import "package:photos/events/event.dart"; import "package:photos/generated/l10n.dart"; +import "package:photos/models/api/collection/user.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/search/generic_search_result.dart"; import "package:photos/models/search/recent_searches.dart"; @@ -14,8 +15,7 @@ import "package:photos/models/search/search_types.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; -import "package:photos/ui/viewer/file/thumbnail_widget.dart"; +import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/ui/viewer/search/result/contact_result_page.dart"; import "package:photos/ui/viewer/search/result/person_face_widget.dart"; import "package:photos/ui/viewer/search/search_section_cta.dart"; @@ -224,43 +224,34 @@ class _ContactRecommendationState extends State { } else if (snapshot.connectionState == ConnectionState.done && snapshot.data == null) { - if (widget.contactSearchResult - .previewThumbnail() != - null) { - return Hero( - tag: heroTag, - child: ThumbnailWidget( - widget.contactSearchResult - .previewThumbnail()!, - shouldShowArchiveStatus: false, - shouldShowSyncStatus: false, - ), - ); - } else { - return const NoThumbnailWidget(); - } + return FirstLetterUserAvatar( + User( + email: widget.contactSearchResult + .params[kContactEmail], + ), + ); } else if (snapshot.hasError) { _logger.severe( "Error loading personID", snapshot.error, ); - return const NoThumbnailWidget(); + return FirstLetterUserAvatar( + User( + email: widget.contactSearchResult + .params[kContactEmail], + ), + ); } else { return const EnteLoadingWidget(); } }, ) - : widget.contactSearchResult.previewThumbnail() != null - ? Hero( - tag: heroTag, - child: ThumbnailWidget( - widget.contactSearchResult - .previewThumbnail()!, - shouldShowArchiveStatus: false, - shouldShowSyncStatus: false, - ), - ) - : const NoThumbnailWidget(), + : FirstLetterUserAvatar( + User( + email: widget + .contactSearchResult.params[kContactEmail], + ), + ), ), ), const SizedBox(height: 10.5), From 1e50f5280150826aded97f6a299c9f32903ebc37 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 5 Feb 2025 17:37:21 +0530 Subject: [PATCH 2/2] [mob][photos] Throw exception and instruction on what to do instead if GenericSearchResult.previewThumbnail() is used when ResultType is 'shared'(which is the case for contacts search) --- mobile/lib/models/search/generic_search_result.dart | 5 +++++ mobile/lib/ui/viewer/search_tab/contacts_section.dart | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mobile/lib/models/search/generic_search_result.dart b/mobile/lib/models/search/generic_search_result.dart index 6c518e02898..1786120815f 100644 --- a/mobile/lib/models/search/generic_search_result.dart +++ b/mobile/lib/models/search/generic_search_result.dart @@ -33,6 +33,11 @@ class GenericSearchResult extends SearchResult { @override EnteFile? previewThumbnail() { + if (type() == ResultType.shared) { + throw Exception( + "Do not use first file as thumbnail. Use user avatar instead.", + ); + } return _files.isEmpty ? null : _files.first; } diff --git a/mobile/lib/ui/viewer/search_tab/contacts_section.dart b/mobile/lib/ui/viewer/search_tab/contacts_section.dart index 8f6ed6354d9..d92a3146845 100644 --- a/mobile/lib/ui/viewer/search_tab/contacts_section.dart +++ b/mobile/lib/ui/viewer/search_tab/contacts_section.dart @@ -171,8 +171,6 @@ class _ContactRecommendationState extends State { @override Widget build(BuildContext context) { - final heroTag = widget.contactSearchResult.heroTag() + - (widget.contactSearchResult.previewThumbnail()?.tag ?? ""); final enteTextTheme = getEnteTextTheme(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 2.5),