Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mob][photos] Use first letter person avatar if no person is linked to contact in contacts section, all contacts screen and contact search results #4975

Merged
merged 2 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mobile/lib/models/search/generic_search_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions mobile/lib/models/search/search_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 2 additions & 0 deletions mobile/lib/services/search_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,7 @@ class SearchService {
),
params: {
kPersonParamID: key.linkedPersonID,
kContactEmail: key.email,
},
),
);
Expand Down Expand Up @@ -1803,6 +1804,7 @@ class SearchService {
),
params: {
kPersonParamID: key.linkedPersonID,
kContactEmail: key.email,
},
),
);
Expand Down
80 changes: 68 additions & 12 deletions mobile/lib/ui/sharing/user_avator_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -130,15 +131,15 @@ class _UserAvatarWidgetState extends State<UserAvatarWidget> {
);
} else if (snapshot.hasError) {
_logger.severe("Error loading personID", snapshot.error);
return _FirstLetterAvatar(
return _FirstLetterCircularAvatar(
user: widget.user,
currentUserID: widget.currentUserID,
thumbnailView: widget.thumbnailView,
type: widget.type,
);
} else if (snapshot.connectionState == ConnectionState.done &&
snapshot.data == null) {
return _FirstLetterAvatar(
return _FirstLetterCircularAvatar(
user: widget.user,
currentUserID: widget.currentUserID,
thumbnailView: widget.thumbnailView,
Expand All @@ -150,7 +151,7 @@ class _UserAvatarWidgetState extends State<UserAvatarWidget> {
),
),
)
: _FirstLetterAvatar(
: _FirstLetterCircularAvatar(
user: widget.user,
currentUserID: widget.currentUserID,
thumbnailView: widget.thumbnailView,
Expand All @@ -159,23 +160,25 @@ class _UserAvatarWidgetState extends State<UserAvatarWidget> {
}
}

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,
required this.type,
});

@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);
Expand All @@ -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);
Expand Down Expand Up @@ -257,3 +259,57 @@ double getAvatarSize(
return 18.0;
}
}

class FirstLetterUserAvatar extends StatefulWidget {
final User user;
const FirstLetterUserAvatar(this.user, {super.key});

@override
State<FirstLetterUserAvatar> createState() => _FirstLetterUserAvatarState();
}

class _FirstLetterUserAvatarState extends State<FirstLetterUserAvatar> {
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),
),
),
);
}
}
43 changes: 21 additions & 22 deletions mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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';
Expand Down Expand Up @@ -73,14 +75,14 @@ class _ContactSearchThumbnailWidgetState
extends State<ContactSearchThumbnailWidget> {
Future<EnteFile?>? _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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)),
);
}
}
51 changes: 20 additions & 31 deletions mobile/lib/ui/viewer/search_tab/contacts_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -171,8 +171,6 @@ class _ContactRecommendationState extends State<ContactRecommendation> {

@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),
Expand Down Expand Up @@ -224,43 +222,34 @@ class _ContactRecommendationState extends State<ContactRecommendation> {
} 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),
Expand Down