Skip to content

Commit f191080

Browse files
chrisbobbesm-sayedi
authored andcommitted
muted-users: Use placeholder for avatars of muted users, where applicable
(Done by adding an is-muted condition in Avatar and AvatarImage, with an opt-out param. Similar to how we handled users' names in a recent commit.) If a user is muted, we'll now show a placeholder where before we would have shown their real avatar, in the following places: - The sender row on messages in the message list. This and message content will get more treatment in a separate commit. - @-mention autocomplete, but we'll be excluding muted users, coming up in a separate commit. - User items in custom profile fields. - 1:1 DM items in the Direct messages ("recent DMs") page. But we'll be excluding those items there, coming up in a separate commit. We *don't* do this replacement in the following places, i.e., we'll still show the real avatar: - The header of the lightbox page. (This follows web.) - The big avatar at the top of the profile page. Co-authored-by: Sayed Mahmood Sayedi <[email protected]>
1 parent ccb5e13 commit f191080

File tree

5 files changed

+89
-8
lines changed

5 files changed

+89
-8
lines changed

lib/widgets/content.dart

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1662,18 +1662,23 @@ class Avatar extends StatelessWidget {
16621662
required this.userId,
16631663
required this.size,
16641664
required this.borderRadius,
1665+
this.replaceIfMuted = true,
16651666
});
16661667

16671668
final int userId;
16681669
final double size;
16691670
final double borderRadius;
1671+
final bool replaceIfMuted;
16701672

16711673
@override
16721674
Widget build(BuildContext context) {
16731675
return AvatarShape(
16741676
size: size,
16751677
borderRadius: borderRadius,
1676-
child: AvatarImage(userId: userId, size: size));
1678+
child: AvatarImage(
1679+
userId: userId,
1680+
size: size,
1681+
replaceIfMuted: replaceIfMuted));
16771682
}
16781683
}
16791684

@@ -1687,10 +1692,12 @@ class AvatarImage extends StatelessWidget {
16871692
super.key,
16881693
required this.userId,
16891694
required this.size,
1695+
this.replaceIfMuted = true,
16901696
});
16911697

16921698
final int userId;
16931699
final double size;
1700+
final bool replaceIfMuted;
16941701

16951702
@override
16961703
Widget build(BuildContext context) {
@@ -1701,6 +1708,10 @@ class AvatarImage extends StatelessWidget {
17011708
return const SizedBox.shrink();
17021709
}
17031710

1711+
if (replaceIfMuted && store.isUserMuted(userId)) {
1712+
return _AvatarPlaceholder(size: size);
1713+
}
1714+
17041715
final resolvedUrl = switch (user.avatarUrl) {
17051716
null => null, // TODO(#255): handle computing gravatars
17061717
var avatarUrl => store.tryResolveUrl(avatarUrl),
@@ -1721,6 +1732,32 @@ class AvatarImage extends StatelessWidget {
17211732
}
17221733
}
17231734

1735+
/// A placeholder avatar for muted users.
1736+
///
1737+
/// Wrap this with [AvatarShape].
1738+
// TODO(#1558) use this as a fallback in more places (?) and update dartdoc.
1739+
class _AvatarPlaceholder extends StatelessWidget {
1740+
const _AvatarPlaceholder({required this.size});
1741+
1742+
/// The size of the placeholder box.
1743+
///
1744+
/// This should match the `size` passed to the wrapping [AvatarShape].
1745+
/// The placeholder's icon will be scaled proportionally to this.
1746+
final double size;
1747+
1748+
@override
1749+
Widget build(BuildContext context) {
1750+
final designVariables = DesignVariables.of(context);
1751+
return DecoratedBox(
1752+
decoration: BoxDecoration(color: designVariables.avatarPlaceholderBg),
1753+
child: Icon(ZulipIcons.person,
1754+
// Where the avatar placeholder appears in the Figma,
1755+
// this is how the icon is sized proportionally to its box.
1756+
size: size * 20 / 32,
1757+
color: designVariables.avatarPlaceholderIcon));
1758+
}
1759+
}
1760+
17241761
/// A rounded square shape, to wrap an [AvatarImage] or similar.
17251762
class AvatarShape extends StatelessWidget {
17261763
const AvatarShape({

lib/widgets/lightbox.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,18 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {
195195
shape: const Border(), // Remove bottom border from [AppBarTheme]
196196
elevation: appBarElevation,
197197
title: Row(children: [
198-
Avatar(size: 36, borderRadius: 36 / 8, userId: widget.message.senderId),
198+
Avatar(
199+
size: 36,
200+
borderRadius: 36 / 8,
201+
userId: widget.message.senderId,
202+
replaceIfMuted: false,
203+
),
199204
const SizedBox(width: 8),
200205
Expanded(
201206
child: RichText(
202207
text: TextSpan(children: [
203208
TextSpan(
204-
// TODO write a test where the sender is muted
209+
// TODO write a test where the sender is muted; check this and avatar
205210
text: '${store.senderDisplayName(widget.message, replaceIfMuted: false)}\n',
206211

207212
// Restate default

lib/widgets/profile.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ class ProfilePage extends StatelessWidget {
4747
final displayEmail = store.userDisplayEmail(userId);
4848
final items = [
4949
Center(
50-
child: Avatar(userId: userId, size: 200, borderRadius: 200 / 8)),
50+
child: Avatar(
51+
userId: userId,
52+
size: 200,
53+
borderRadius: 200 / 8,
54+
replaceIfMuted: false)),
5155
const SizedBox(height: 16),
52-
// TODO write a test where the user is muted
56+
// TODO write a test where the user is muted; check this and avatar
5357
Text(store.userDisplayName(userId, replaceIfMuted: false),
5458
textAlign: TextAlign.center,
5559
style: _TextStyles.primaryFieldText

test/widgets/message_list_test.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1667,29 +1667,44 @@ void main() {
16671667
final mutedLabelFinder = find.widgetWithText(MessageWithPossibleSender,
16681668
mutedLabel);
16691669

1670+
final avatarFinder = find.byWidgetPredicate(
1671+
(widget) => widget is Avatar && widget.userId == message.senderId);
1672+
final mutedAvatarFinder = find.descendant(
1673+
of: avatarFinder,
1674+
matching: find.byIcon(ZulipIcons.person));
1675+
final nonmutedAvatarFinder = find.descendant(
1676+
of: avatarFinder,
1677+
matching: find.byType(RealmContentNetworkImage));
1678+
16701679
final senderName = store.senderDisplayName(message, replaceIfMuted: false);
16711680
assert(senderName != mutedLabel);
16721681
final senderNameFinder = find.widgetWithText(MessageWithPossibleSender,
16731682
senderName);
16741683

16751684
check(mutedLabelFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
16761685
check(senderNameFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
1686+
check(mutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
1687+
check(nonmutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
16771688
}
16781689

1679-
final user = eg.user(userId: 1, fullName: 'User');
1690+
final user = eg.user(userId: 1, fullName: 'User', avatarUrl: '/foo.png');
16801691
final message = eg.streamMessage(sender: user,
16811692
content: '<p>A message</p>', reactions: [eg.unicodeEmojiReaction]);
16821693

16831694
testWidgets('muted appearance', (tester) async {
1695+
prepareBoringImageHttpClient();
16841696
await setupMessageListPage(tester,
16851697
users: [user], mutedUserIds: [user.userId], messages: [message]);
16861698
checkMessage(message, expectIsMuted: true);
1699+
debugNetworkImageHttpClientProvider = null;
16871700
});
16881701

16891702
testWidgets('not-muted appearance', (tester) async {
1703+
prepareBoringImageHttpClient();
16901704
await setupMessageListPage(tester,
16911705
users: [user], mutedUserIds: [], messages: [message]);
16921706
checkMessage(message, expectIsMuted: false);
1707+
debugNetworkImageHttpClientProvider = null;
16931708
});
16941709
});
16951710
});

test/widgets/profile_test.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import 'package:zulip/api/model/model.dart';
88
import 'package:zulip/model/narrow.dart';
99
import 'package:zulip/model/store.dart';
1010
import 'package:zulip/widgets/content.dart';
11+
import 'package:zulip/widgets/icons.dart';
1112
import 'package:zulip/widgets/message_list.dart';
1213
import 'package:zulip/widgets/page.dart';
1314
import 'package:zulip/widgets/profile.dart';
1415

1516
import '../example_data.dart' as eg;
1617
import '../model/binding.dart';
1718
import '../model/test_store.dart';
19+
import '../test_images.dart';
1820
import '../test_navigation.dart';
1921
import 'message_list_checks.dart';
2022
import 'page_checks.dart';
@@ -246,12 +248,23 @@ void main() {
246248
});
247249

248250
testWidgets('page builds; user field with muted user', (tester) async {
251+
prepareBoringImageHttpClient();
252+
253+
Finder avatarFinder(int userId) => find.byWidgetPredicate(
254+
(widget) => widget is Avatar && widget.userId == userId);
255+
Finder mutedAvatarFinder(int userId) => find.descendant(
256+
of: avatarFinder(userId),
257+
matching: find.byIcon(ZulipIcons.person));
258+
Finder nonmutedAvatarFinder(int userId) => find.descendant(
259+
of: avatarFinder(userId),
260+
matching: find.byType(RealmContentNetworkImage));
261+
249262
final users = [
250263
eg.user(userId: 1, profileData: {
251264
0: ProfileFieldUserData(value: '[2,3]'),
252265
}),
253-
eg.user(userId: 2, fullName: 'test user2'),
254-
eg.user(userId: 3, fullName: 'test user3'),
266+
eg.user(userId: 2, fullName: 'test user2', avatarUrl: '/foo.png'),
267+
eg.user(userId: 3, fullName: 'test user3', avatarUrl: '/bar.png'),
255268
];
256269

257270
await setupPage(tester,
@@ -261,7 +274,14 @@ void main() {
261274
customProfileFields: [mkCustomProfileField(0, CustomProfileFieldType.user)]);
262275

263276
check(find.text('Muted user')).findsOne();
277+
check(mutedAvatarFinder(2)).findsOne();
278+
check(nonmutedAvatarFinder(2)).findsNothing();
279+
264280
check(find.text('test user3')).findsOne();
281+
check(mutedAvatarFinder(3)).findsNothing();
282+
check(nonmutedAvatarFinder(3)).findsOne();
283+
284+
debugNetworkImageHttpClientProvider = null;
265285
});
266286

267287
testWidgets('page builds; dm links to correct narrow', (tester) async {

0 commit comments

Comments
 (0)