Skip to content

Commit 96bfe26

Browse files
chrisbobbesm-sayedi
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 fe9f5be commit 96bfe26

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
@@ -1659,18 +1659,23 @@ class Avatar extends StatelessWidget {
16591659
required this.userId,
16601660
required this.size,
16611661
required this.borderRadius,
1662+
this.replaceIfMuted = true,
16621663
});
16631664

16641665
final int userId;
16651666
final double size;
16661667
final double borderRadius;
1668+
final bool replaceIfMuted;
16671669

16681670
@override
16691671
Widget build(BuildContext context) {
16701672
return AvatarShape(
16711673
size: size,
16721674
borderRadius: borderRadius,
1673-
child: AvatarImage(userId: userId, size: size));
1675+
child: AvatarImage(
1676+
userId: userId,
1677+
size: size,
1678+
replaceIfMuted: replaceIfMuted));
16741679
}
16751680
}
16761681

@@ -1684,10 +1689,12 @@ class AvatarImage extends StatelessWidget {
16841689
super.key,
16851690
required this.userId,
16861691
required this.size,
1692+
this.replaceIfMuted = true,
16871693
});
16881694

16891695
final int userId;
16901696
final double size;
1697+
final bool replaceIfMuted;
16911698

16921699
@override
16931700
Widget build(BuildContext context) {
@@ -1698,6 +1705,10 @@ class AvatarImage extends StatelessWidget {
16981705
return const SizedBox.shrink();
16991706
}
17001707

1708+
if (replaceIfMuted && store.isUserMuted(userId)) {
1709+
return _AvatarPlaceholder(size: size);
1710+
}
1711+
17011712
final resolvedUrl = switch (user.avatarUrl) {
17021713
null => null, // TODO(#255): handle computing gravatars
17031714
var avatarUrl => store.tryResolveUrl(avatarUrl),
@@ -1718,6 +1729,32 @@ class AvatarImage extends StatelessWidget {
17181729
}
17191730
}
17201731

1732+
/// A placeholder avatar for muted users.
1733+
///
1734+
/// Wrap this with [AvatarShape].
1735+
// TODO(#1558) use this as a fallback in more places (?) and update dartdoc.
1736+
class _AvatarPlaceholder extends StatelessWidget {
1737+
const _AvatarPlaceholder({required this.size});
1738+
1739+
/// The size of the placeholder box.
1740+
///
1741+
/// This should match the `size` passed to the wrapping [AvatarShape].
1742+
/// The placeholder's icon will be scaled proportionally to this.
1743+
final double size;
1744+
1745+
@override
1746+
Widget build(BuildContext context) {
1747+
final designVariables = DesignVariables.of(context);
1748+
return DecoratedBox(
1749+
decoration: BoxDecoration(color: designVariables.avatarPlaceholderBg),
1750+
child: Icon(ZulipIcons.person,
1751+
// Where the avatar placeholder appears in the Figma,
1752+
// this is how the icon is sized proportionally to its box.
1753+
size: size * 20 / 32,
1754+
color: designVariables.avatarPlaceholderIcon));
1755+
}
1756+
}
1757+
17211758
/// A rounded square shape, to wrap an [AvatarImage] or similar.
17221759
class AvatarShape extends StatelessWidget {
17231760
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
@@ -1591,29 +1591,44 @@ void main() {
15911591
final mutedLabelFinder = find.widgetWithText(MessageWithPossibleSender,
15921592
mutedLabel);
15931593

1594+
final avatarFinder = find.byWidgetPredicate(
1595+
(widget) => widget is Avatar && widget.userId == message.senderId);
1596+
final mutedAvatarFinder = find.descendant(
1597+
of: avatarFinder,
1598+
matching: find.byIcon(ZulipIcons.person));
1599+
final nonmutedAvatarFinder = find.descendant(
1600+
of: avatarFinder,
1601+
matching: find.byType(RealmContentNetworkImage));
1602+
15941603
final senderName = store.senderDisplayName(message, replaceIfMuted: false);
15951604
assert(senderName != mutedLabel);
15961605
final senderNameFinder = find.widgetWithText(MessageWithPossibleSender,
15971606
senderName);
15981607

15991608
check(mutedLabelFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
16001609
check(senderNameFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
1610+
check(mutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 1 : 0);
1611+
check(nonmutedAvatarFinder.evaluate().length).equals(expectIsMuted ? 0 : 1);
16011612
}
16021613

1603-
final user = eg.user(userId: 1, fullName: 'User');
1614+
final user = eg.user(userId: 1, fullName: 'User', avatarUrl: '/foo.png');
16041615
final message = eg.streamMessage(sender: user,
16051616
content: '<p>A message</p>', reactions: [eg.unicodeEmojiReaction]);
16061617

16071618
testWidgets('muted appearance', (tester) async {
1619+
prepareBoringImageHttpClient();
16081620
await setupMessageListPage(tester,
16091621
users: [user], mutedUserIds: [user.userId], messages: [message]);
16101622
checkMessage(message, expectIsMuted: true);
1623+
debugNetworkImageHttpClientProvider = null;
16111624
});
16121625

16131626
testWidgets('not-muted appearance', (tester) async {
1627+
prepareBoringImageHttpClient();
16141628
await setupMessageListPage(tester,
16151629
users: [user], mutedUserIds: [], messages: [message]);
16161630
checkMessage(message, expectIsMuted: false);
1631+
debugNetworkImageHttpClientProvider = null;
16171632
});
16181633
});
16191634
});

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)