Skip to content

Commit f5cde2b

Browse files
committed
msglist: Use [User.avatarUrl] instead of [Message.avatarUrl]
Now, as demonstrated in the new test, if the author of a message changes their avatar, we'll see the update in the message list as soon as we get the event. While we're at it, comment out the property on [Message] to guide future consumers to [User]. Related: zulip#135
1 parent a41b1db commit f5cde2b

File tree

5 files changed

+74
-13
lines changed

5 files changed

+74
-13
lines changed

lib/api/model/model.dart

+1-4
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ class Subscription {
248248
///
249249
/// https://zulip.com/api/get-messages#response
250250
sealed class Message {
251-
final String? avatarUrl;
251+
// final String? avatarUrl; // Use [User.avatarUrl] instead; will live-update
252252
final String client;
253253
final String content;
254254
final String contentType;
@@ -276,7 +276,6 @@ sealed class Message {
276276
final String? matchSubject;
277277

278278
Message({
279-
this.avatarUrl,
280279
required this.client,
281280
required this.content,
282281
required this.contentType,
@@ -315,7 +314,6 @@ class StreamMessage extends Message {
315314
final int streamId;
316315

317316
StreamMessage({
318-
super.avatarUrl,
319317
required super.client,
320318
required super.content,
321319
required super.contentType,
@@ -417,7 +415,6 @@ class DmMessage extends Message {
417415
Iterable<int> get allRecipientIds => displayRecipient.map((e) => e.id);
418416

419417
DmMessage({
420-
super.avatarUrl,
421418
required super.client,
422419
required super.content,
423420
required super.contentType,

lib/api/model/model.g.dart

-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/widgets/message_list.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,11 @@ class MessageWithSender extends StatelessWidget {
475475
@override
476476
Widget build(BuildContext context) {
477477
final store = PerAccountStoreWidget.of(context);
478+
final author = store.users[message.senderId]!;
478479

479-
final avatarUrl = message.avatarUrl == null // TODO get from user data
480+
final avatarUrl = author.avatarUrl == null
480481
? null // TODO handle computing gravatars
481-
: resolveUrl(message.avatarUrl!, store.account);
482+
: resolveUrl(author.avatarUrl!, store.account);
482483
final avatar = (avatarUrl == null)
483484
? const SizedBox.shrink()
484485
: RealmContentNetworkImage(avatarUrl, filterQuality: FilterQuality.medium);

test/widgets/content_checks.dart

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:zulip/widgets/content.dart';
3+
4+
extension RealmContentNetworkImageChecks on Subject<RealmContentNetworkImage> {
5+
Subject<String> get src => has((i) => i.src, 'src');
6+
// TODO others
7+
}

test/widgets/message_list_test.dart

+63-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
import 'dart:io';
2+
13
import 'package:checks/checks.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:zulip/api/model/events.dart';
47
import 'package:zulip/api/model/model.dart';
58
import 'package:zulip/api/route/messages.dart';
69
import 'package:zulip/model/narrow.dart';
10+
import 'package:zulip/widgets/content.dart';
711
import 'package:zulip/widgets/message_list.dart';
812
import 'package:zulip/widgets/sticky_header.dart';
913
import 'package:zulip/widgets/store.dart';
1014

11-
import '../api/fake_api.dart';
15+
import '../api/fake_api.dart' as fake_api;
16+
import '../test_images.dart';
1217
import '../example_data.dart' as eg;
1318
import '../model/binding.dart';
19+
import '../model/test_store.dart';
20+
import 'content_checks.dart';
1421

1522
Future<void> setupMessageListPage(WidgetTester tester, {
1623
required Narrow narrow,
@@ -22,11 +29,12 @@ Future<void> setupMessageListPage(WidgetTester tester, {
2229

2330
await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot());
2431
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
25-
final connection = store.connection as FakeApiConnection;
32+
final connection = store.connection as fake_api.FakeApiConnection;
2633

2734
// prepare message list data
35+
store.addUser(eg.selfUser);
2836
final List<StreamMessage> messages = List.generate(10, (index) {
29-
return eg.streamMessage(id: index);
37+
return eg.streamMessage(id: index, sender: eg.selfUser);
3038
});
3139
connection.prepare(json: GetMessagesResult(
3240
anchor: messages[0].id,
@@ -51,6 +59,58 @@ Future<void> setupMessageListPage(WidgetTester tester, {
5159
void main() {
5260
TestZulipBinding.ensureInitialized();
5361

62+
group('MessageWithSender', () {
63+
testWidgets('Updates avatar on RealmUserUpdateEvent', (tester) async {
64+
addTearDown(TestZulipBinding.instance.reset);
65+
66+
RealmContentNetworkImage? findAvatarImageWidget(WidgetTester tester) {
67+
final firstMessageWithSender = tester.widgetList(find.byType(MessageWithSender)).first;
68+
return tester.widgetList<RealmContentNetworkImage>(
69+
find.descendant(
70+
of: find.byWidget(firstMessageWithSender),
71+
matching: find.byType(RealmContentNetworkImage)),
72+
).singleOrNull;
73+
}
74+
75+
void checkResultForSender(User sender) {
76+
final avatarUrl = sender.avatarUrl;
77+
switch (avatarUrl) {
78+
case String(): {
79+
check(findAvatarImageWidget(tester)).isNotNull().src.equals(resolveUrl(avatarUrl, eg.selfAccount));
80+
}
81+
case null: {
82+
check(findAvatarImageWidget(tester)).isNull();
83+
}
84+
}
85+
}
86+
87+
Future<void> handleNewAvatarEventAndPump(WidgetTester tester, String avatarUrl) async {
88+
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
89+
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: eg.selfUser.userId, avatarUrl: avatarUrl));
90+
await tester.pump();
91+
}
92+
93+
final httpClient = FakeHttpClient();
94+
debugNetworkImageHttpClientProvider = () => httpClient;
95+
httpClient.request.response
96+
..statusCode = HttpStatus.ok
97+
..content = kSolidBlueAvatar;
98+
99+
await setupMessageListPage(tester, narrow: const AllMessagesNarrow());
100+
checkResultForSender(eg.selfUser);
101+
102+
await handleNewAvatarEventAndPump(tester, '/foo.png');
103+
// TODO only vary [avatarUrl], not other fields
104+
checkResultForSender(eg.user(userId: eg.selfUser.userId, avatarUrl: '/foo.png'));
105+
106+
await handleNewAvatarEventAndPump(tester, '/bar.jpg');
107+
// TODO only vary [avatarUrl], not other fields
108+
checkResultForSender(eg.user(userId: eg.selfUser.userId, avatarUrl: '/bar.jpg'));
109+
110+
debugNetworkImageHttpClientProvider = null;
111+
});
112+
});
113+
54114
group('ScrollToBottomButton interactions', () {
55115
ScrollController? findMessageListScrollController(WidgetTester tester) {
56116
final stickyHeaderListView = tester.widget<StickyHeaderListView>(find.byType(StickyHeaderListView));

0 commit comments

Comments
 (0)