diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 6dbdec8d13f..534ef69c325 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -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; @@ -276,7 +276,6 @@ sealed class Message { final String? matchSubject; Message({ - this.avatarUrl, required this.client, required this.content, required this.contentType, @@ -315,7 +314,6 @@ class StreamMessage extends Message { final int streamId; StreamMessage({ - super.avatarUrl, required super.client, required super.content, required super.contentType, @@ -417,7 +415,6 @@ class DmMessage extends Message { Iterable get allRecipientIds => displayRecipient.map((e) => e.id); DmMessage({ - super.avatarUrl, required super.client, required super.content, required super.contentType, diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index 23641115657..78c9ebd7049 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -180,7 +180,6 @@ Map _$SubscriptionToJson(Subscription instance) => StreamMessage _$StreamMessageFromJson(Map json) => StreamMessage( - avatarUrl: json['avatar_url'] as String?, client: json['client'] as String, content: json['content'] as String, contentType: json['content_type'] as String, @@ -203,7 +202,6 @@ StreamMessage _$StreamMessageFromJson(Map json) => Map _$StreamMessageToJson(StreamMessage instance) => { - 'avatar_url': instance.avatarUrl, 'client': instance.client, 'content': instance.content, 'content_type': instance.contentType, @@ -239,7 +237,6 @@ Map _$DmRecipientToJson(DmRecipient instance) => }; DmMessage _$DmMessageFromJson(Map json) => DmMessage( - avatarUrl: json['avatar_url'] as String?, client: json['client'] as String, content: json['content'] as String, contentType: json['content_type'] as String, @@ -261,7 +258,6 @@ DmMessage _$DmMessageFromJson(Map json) => DmMessage( ); Map _$DmMessageToJson(DmMessage instance) => { - 'avatar_url': instance.avatarUrl, 'client': instance.client, 'content': instance.content, 'content_type': instance.contentType, diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 416037b8690..31b3a972950 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -451,10 +451,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( diff --git a/test/widgets/content_checks.dart b/test/widgets/content_checks.dart new file mode 100644 index 00000000000..cf692538104 --- /dev/null +++ b/test/widgets/content_checks.dart @@ -0,0 +1,7 @@ +import 'package:checks/checks.dart'; +import 'package:zulip/widgets/content.dart'; + +extension RealmContentNetworkImageChecks on Subject { + Subject get src => has((i) => i.src, 'src'); + // TODO others +} diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index e0a77369a64..035e7acf594 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -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 setupMessageListPage(WidgetTester tester, { required Narrow narrow, @@ -22,11 +29,12 @@ Future 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 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, @@ -51,6 +59,58 @@ Future 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( + 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 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(find.byType(StickyHeaderListView));