Skip to content

Commit 86137cd

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 8b01b47 commit 86137cd

File tree

5 files changed

+84
-8
lines changed

5 files changed

+84
-8
lines changed

lib/widgets/content.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1666,13 +1666,15 @@ class Avatar extends StatelessWidget {
16661666
required this.borderRadius,
16671667
this.backgroundColor,
16681668
this.showPresence = true,
1669+
this.replaceIfMuted = true,
16691670
});
16701671

16711672
final int userId;
16721673
final double size;
16731674
final double borderRadius;
16741675
final Color? backgroundColor;
16751676
final bool showPresence;
1677+
final bool replaceIfMuted;
16761678

16771679
@override
16781680
Widget build(BuildContext context) {
@@ -1684,7 +1686,7 @@ class Avatar extends StatelessWidget {
16841686
borderRadius: borderRadius,
16851687
backgroundColor: backgroundColor,
16861688
userIdForPresence: showPresence ? userId : null,
1687-
child: AvatarImage(userId: userId, size: size));
1689+
child: AvatarImage(userId: userId, size: size, replaceIfMuted: replaceIfMuted));
16881690
}
16891691
}
16901692

@@ -1698,10 +1700,12 @@ class AvatarImage extends StatelessWidget {
16981700
super.key,
16991701
required this.userId,
17001702
required this.size,
1703+
this.replaceIfMuted = true,
17011704
});
17021705

17031706
final int userId;
17041707
final double size;
1708+
final bool replaceIfMuted;
17051709

17061710
@override
17071711
Widget build(BuildContext context) {
@@ -1712,6 +1716,10 @@ class AvatarImage extends StatelessWidget {
17121716
return const SizedBox.shrink();
17131717
}
17141718

1719+
if (replaceIfMuted && store.isUserMuted(userId)) {
1720+
return _AvatarPlaceholder(size: size);
1721+
}
1722+
17151723
final resolvedUrl = switch (user.avatarUrl) {
17161724
null => null, // TODO(#255): handle computing gravatars
17171725
var avatarUrl => store.tryResolveUrl(avatarUrl),
@@ -1732,6 +1740,32 @@ class AvatarImage extends StatelessWidget {
17321740
}
17331741
}
17341742

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

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ class ProfilePage extends StatelessWidget {
5656
borderRadius: 200 / 8,
5757
// Would look odd with this large image;
5858
// we'll show it by the user's name instead.
59-
showPresence: false)),
59+
showPresence: false,
60+
replaceIfMuted: false,
61+
)),
6062
const SizedBox(height: 16),
6163
Text.rich(
6264
TextSpan(children: [
@@ -65,7 +67,7 @@ class ProfilePage extends StatelessWidget {
6567
fontSize: nameStyle.fontSize!,
6668
textScaler: MediaQuery.textScalerOf(context),
6769
),
68-
// TODO write a test where the user is muted
70+
// TODO write a test where the user is muted; check this and avatar
6971
TextSpan(text: store.userDisplayName(userId, replaceIfMuted: false)),
7072
]),
7173
textAlign: TextAlign.center,

test/widgets/message_list_test.dart

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

1698+
final avatarFinder = find.byWidgetPredicate(
1699+
(widget) => widget is Avatar && widget.userId == message.senderId);
1700+
final mutedAvatarFinder = find.descendant(
1701+
of: avatarFinder,
1702+
matching: find.byIcon(ZulipIcons.person));
1703+
final nonmutedAvatarFinder = find.descendant(
1704+
of: avatarFinder,
1705+
matching: find.byType(RealmContentNetworkImage));
1706+
16981707
final senderName = store.senderDisplayName(message, replaceIfMuted: false);
16991708
assert(senderName != mutedLabel);
17001709
final senderNameFinder = find.widgetWithText(MessageWithPossibleSender,
17011710
senderName);
17021711

17031712
check(mutedLabelFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
17041713
check(senderNameFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
1714+
check(mutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
1715+
check(nonmutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
17051716
}
17061717

1707-
final user = eg.user(userId: 1, fullName: 'User');
1718+
final user = eg.user(userId: 1, fullName: 'User', avatarUrl: '/foo.png');
17081719
final message = eg.streamMessage(sender: user,
17091720
content: '<p>A message</p>', reactions: [eg.unicodeEmojiReaction]);
17101721

17111722
testWidgets('muted appearance', (tester) async {
1723+
prepareBoringImageHttpClient();
17121724
await setupMessageListPage(tester,
17131725
users: [user], mutedUserIds: [user.userId], messages: [message]);
17141726
checkMessage(message, expectIsMuted: true);
1727+
debugNetworkImageHttpClientProvider = null;
17151728
});
17161729

17171730
testWidgets('not-muted appearance', (tester) async {
1731+
prepareBoringImageHttpClient();
17181732
await setupMessageListPage(tester,
17191733
users: [user], mutedUserIds: [], messages: [message]);
17201734
checkMessage(message, expectIsMuted: false);
1735+
debugNetworkImageHttpClientProvider = null;
17211736
});
17221737
});
17231738

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)