Skip to content

Commit

Permalink
msglist: Use [User.avatarUrl] instead of [Message.avatarUrl]
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chrisbobbe committed Aug 2, 2023
1 parent a41b1db commit f5cde2b
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 13 deletions.
5 changes: 1 addition & 4 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class Subscription {
///
/// https://zulip.com/api/get-messages#response
sealed class Message {
final String? avatarUrl;
// final String? avatarUrl; // Use [User.avatarUrl] instead; will live-update
final String client;
final String content;
final String contentType;
Expand Down Expand Up @@ -276,7 +276,6 @@ sealed class Message {
final String? matchSubject;

Message({
this.avatarUrl,
required this.client,
required this.content,
required this.contentType,
Expand Down Expand Up @@ -315,7 +314,6 @@ class StreamMessage extends Message {
final int streamId;

StreamMessage({
super.avatarUrl,
required super.client,
required super.content,
required super.contentType,
Expand Down Expand Up @@ -417,7 +415,6 @@ class DmMessage extends Message {
Iterable<int> get allRecipientIds => displayRecipient.map((e) => e.id);

DmMessage({
super.avatarUrl,
required super.client,
required super.content,
required super.contentType,
Expand Down
4 changes: 0 additions & 4 deletions lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,11 @@ class MessageWithSender extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final author = store.users[message.senderId]!;

final avatarUrl = message.avatarUrl == null // TODO get from user data
final avatarUrl = author.avatarUrl == null
? null // TODO handle computing gravatars
: resolveUrl(message.avatarUrl!, store.account);
: resolveUrl(author.avatarUrl!, store.account);
final avatar = (avatarUrl == null)
? const SizedBox.shrink()
: RealmContentNetworkImage(avatarUrl, filterQuality: FilterQuality.medium);
Expand Down
7 changes: 7 additions & 0 deletions test/widgets/content_checks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:checks/checks.dart';
import 'package:zulip/widgets/content.dart';

extension RealmContentNetworkImageChecks on Subject<RealmContentNetworkImage> {
Subject<String> get src => has((i) => i.src, 'src');
// TODO others
}
66 changes: 63 additions & 3 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import 'dart:io';

import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/api/model/events.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/api/route/messages.dart';
import 'package:zulip/model/narrow.dart';
import 'package:zulip/widgets/content.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/sticky_header.dart';
import 'package:zulip/widgets/store.dart';

import '../api/fake_api.dart';
import '../api/fake_api.dart' as fake_api;
import '../test_images.dart';
import '../example_data.dart' as eg;
import '../model/binding.dart';
import '../model/test_store.dart';
import 'content_checks.dart';

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

await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot());
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
final connection = store.connection as FakeApiConnection;
final connection = store.connection as fake_api.FakeApiConnection;

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

group('MessageWithSender', () {
testWidgets('Updates avatar on RealmUserUpdateEvent', (tester) async {
addTearDown(TestZulipBinding.instance.reset);

RealmContentNetworkImage? findAvatarImageWidget(WidgetTester tester) {
final firstMessageWithSender = tester.widgetList(find.byType(MessageWithSender)).first;
return tester.widgetList<RealmContentNetworkImage>(
find.descendant(
of: find.byWidget(firstMessageWithSender),
matching: find.byType(RealmContentNetworkImage)),
).singleOrNull;
}

void checkResultForSender(User sender) {
final avatarUrl = sender.avatarUrl;
switch (avatarUrl) {
case String(): {
check(findAvatarImageWidget(tester)).isNotNull().src.equals(resolveUrl(avatarUrl, eg.selfAccount));
}
case null: {
check(findAvatarImageWidget(tester)).isNull();
}
}
}

Future<void> handleNewAvatarEventAndPump(WidgetTester tester, String avatarUrl) async {
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: eg.selfUser.userId, avatarUrl: avatarUrl));
await tester.pump();
}

final httpClient = FakeHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
httpClient.request.response
..statusCode = HttpStatus.ok
..content = kSolidBlueAvatar;

await setupMessageListPage(tester, narrow: const AllMessagesNarrow());
checkResultForSender(eg.selfUser);

await handleNewAvatarEventAndPump(tester, '/foo.png');
// TODO only vary [avatarUrl], not other fields
checkResultForSender(eg.user(userId: eg.selfUser.userId, avatarUrl: '/foo.png'));

await handleNewAvatarEventAndPump(tester, '/bar.jpg');
// TODO only vary [avatarUrl], not other fields
checkResultForSender(eg.user(userId: eg.selfUser.userId, avatarUrl: '/bar.jpg'));

debugNetworkImageHttpClientProvider = null;
});
});

group('ScrollToBottomButton interactions', () {
ScrollController? findMessageListScrollController(WidgetTester tester) {
final stickyHeaderListView = tester.widget<StickyHeaderListView>(find.byType(StickyHeaderListView));
Expand Down

0 comments on commit f5cde2b

Please sign in to comment.