Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

msglist: Use [User.avatarUrl] instead of [Message.avatarUrl] #246

Merged
merged 3 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
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(
Expand Down
9 changes: 7 additions & 2 deletions test/example_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ const String recentZulipVersion = '8.0';
const int recentZulipFeatureLevel = 185;
const int futureZulipFeatureLevel = 9999;

User user({int? userId, String? email, String? fullName}) {
User user({
int? userId,
String? email,
String? fullName,
String? avatarUrl,
}) {
return User(
userId: userId ?? 123, // TODO generate example IDs
deliveryEmailStaleDoNotUse: 'name@example.com',
Expand All @@ -25,7 +30,7 @@ User user({int? userId, String? email, String? fullName}) {
isBot: false,
role: 400,
timezone: 'UTC',
avatarUrl: null,
avatarUrl: avatarUrl,
avatarVersion: 0,
profileData: null,
);
Expand Down
68 changes: 68 additions & 0 deletions test/test_images.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

class FakeImageHttpClient extends Fake implements HttpClient {
final FakeImageHttpClientRequest request = FakeImageHttpClientRequest();

@override
Future<HttpClientRequest> getUrl(Uri url) async => request;
}

class FakeImageHttpClientRequest extends Fake implements HttpClientRequest {
final FakeImageHttpClientResponse response = FakeImageHttpClientResponse();

@override
final FakeImageHttpHeaders headers = FakeImageHttpHeaders();

@override
Future<HttpClientResponse> close() async => response;
}

class FakeImageHttpHeaders extends Fake implements HttpHeaders {
final Map<String, List<String>> values = {};

@override
void add(String name, Object value, {bool preserveHeaderCase = false}) {
(values[name] ??= []).add(value.toString());
}
}

class FakeImageHttpClientResponse extends Fake implements HttpClientResponse {
@override
int statusCode = HttpStatus.ok;

late List<int> content;

@override
int get contentLength => content.length;

@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;

@override
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream.value(content).listen(
onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
}
}

/// A 100x100 PNG image of solid Zulip blue, [kZulipBrandColor].
// Made from the following SVG:
// <svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" viewBox="0 0 1 1">
// <rect style="fill:#6492fe;fill-opacity:1" width="1" height="1" x="0" y="0" />
// </svg>
// with `inkscape tmp.svg -w 100 --export-png=tmp1.png`,
// `zopflipng tmp1.png tmp.png`,
// and `xxd -i tmp.png`.
const List<int> kSolidBlueAvatar = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64,
0x01, 0x03, 0x00, 0x00, 0x00, 0x4a, 0x2c, 0x07, 0x17, 0x00, 0x00, 0x00,
0x03, 0x50, 0x4c, 0x54, 0x45, 0x64, 0x92, 0xfe, 0xf1, 0xd6, 0x69, 0xa5,
0x00, 0x00, 0x00, 0x13, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0x63, 0xa0,
0x2b, 0x18, 0x05, 0xa3, 0x60, 0x14, 0x8c, 0x82, 0x51, 0x00, 0x00, 0x05,
0x78, 0x00, 0x01, 0x1e, 0xcd, 0x28, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
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
}
68 changes: 2 additions & 66 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io';

import 'package:checks/checks.dart';
Expand All @@ -13,6 +12,7 @@ import 'package:zulip/widgets/store.dart';

import '../example_data.dart' as eg;
import '../model/binding.dart';
import '../test_images.dart';
import 'dialog_checks.dart';

void main() {
Expand Down Expand Up @@ -126,7 +126,7 @@ void main() {
addTearDown(TestZulipBinding.instance.reset);
await globalStore.add(eg.selfAccount, eg.initialSnapshot());

final httpClient = _FakeHttpClient();
final httpClient = FakeImageHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
httpClient.request.response
..statusCode = HttpStatus.ok
Expand Down Expand Up @@ -162,67 +162,3 @@ void main() {
});
});
}

class _FakeHttpClient extends Fake implements HttpClient {
final _FakeHttpClientRequest request = _FakeHttpClientRequest();

@override
Future<HttpClientRequest> getUrl(Uri url) async => request;
}

class _FakeHttpClientRequest extends Fake implements HttpClientRequest {
final _FakeHttpClientResponse response = _FakeHttpClientResponse();

@override
final _FakeHttpHeaders headers = _FakeHttpHeaders();

@override
Future<HttpClientResponse> close() async => response;
}

class _FakeHttpHeaders extends Fake implements HttpHeaders {
final Map<String, List<String>> values = {};

@override
void add(String name, Object value, {bool preserveHeaderCase = false}) {
(values[name] ??= []).add(value.toString());
}
}

class _FakeHttpClientResponse extends Fake implements HttpClientResponse {
@override
int statusCode = HttpStatus.ok;

late List<int> content;

@override
int get contentLength => content.length;

@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;

@override
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream.value(content).listen(
onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
}
}

/// A 100x100 PNG image of solid Zulip blue, [kZulipBrandColor].
// Made from the following SVG:
// <svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" viewBox="0 0 1 1">
// <rect style="fill:#6492fe;fill-opacity:1" width="1" height="1" x="0" y="0" />
// </svg>
// with `inkscape tmp.svg -w 100 --export-png=tmp1.png`,
// `zopflipng tmp1.png tmp.png`,
// and `xxd -i tmp.png`.
const List<int> kSolidBlueAvatar = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64,
0x01, 0x03, 0x00, 0x00, 0x00, 0x4a, 0x2c, 0x07, 0x17, 0x00, 0x00, 0x00,
0x03, 0x50, 0x4c, 0x54, 0x45, 0x64, 0x92, 0xfe, 0xf1, 0xd6, 0x69, 0xa5,
0x00, 0x00, 0x00, 0x13, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0x63, 0xa0,
0x2b, 0x18, 0x05, 0xa3, 0x60, 0x14, 0x8c, 0x82, 0x51, 0x00, 0x00, 0x05,
0x78, 0x00, 0x01, 0x1e, 0xcd, 0x28, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
57 changes: 56 additions & 1 deletion 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 '../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 @@ -25,8 +32,9 @@ Future<void> setupMessageListPage(WidgetTester tester, {
final connection = store.connection as 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 Down Expand Up @@ -122,4 +130,51 @@ void main() {
check(scrollController.position.pixels).equals(0);
});
});

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

// TODO recognize avatar more reliably:
// https://github.com/zulip/zulip-flutter/pull/246#discussion_r1282516308
RealmContentNetworkImage? findAvatarImageWidget(WidgetTester tester) {
return tester.widgetList<RealmContentNetworkImage>(
find.descendant(
of: find.byType(MessageWithSender),
matching: find.byType(RealmContentNetworkImage))).firstOrNull;
}

void checkResultForSender(String? avatarUrl) {
if (avatarUrl == null) {
check(findAvatarImageWidget(tester)).isNull();
} else {
check(findAvatarImageWidget(tester)).isNotNull()
.src.equals(resolveUrl(avatarUrl, eg.selfAccount));
}
}

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 = FakeImageHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
httpClient.request.response
..statusCode = HttpStatus.ok
..content = kSolidBlueAvatar;

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

await handleNewAvatarEventAndPump(tester, '/foo.png');
checkResultForSender('/foo.png');

await handleNewAvatarEventAndPump(tester, '/bar.jpg');
checkResultForSender('/bar.jpg');

debugNetworkImageHttpClientProvider = null;
});
});
}