From ec07043d3ca9bd88d45568c5fe7b5e1ee52c56a1 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Mon, 25 Mar 2024 22:32:57 +0430 Subject: [PATCH 1/3] autocomplete [nfc]: Delete wildcard and user-group result subclasses The WildcardMentionAutocompleteResult and UserGroupMentionAutocompleteResult classes are not used, so better to remove them for now. --- lib/model/autocomplete.dart | 19 ++----------------- lib/widgets/autocomplete.dart | 8 -------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/lib/model/autocomplete.dart b/lib/model/autocomplete.dart index fda2cba197..f1351ef28e 100644 --- a/lib/model/autocomplete.dart +++ b/lib/model/autocomplete.dart @@ -342,21 +342,6 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult { final int userId; } -enum WildcardMentionType { - all, - everyone, - stream, -} - -class WildcardMentionAutocompleteResult extends MentionAutocompleteResult { - WildcardMentionAutocompleteResult({required this.type}); - - final WildcardMentionType type; -} +// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult { - -class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult { - UserGroupMentionAutocompleteResult({required this.userGroupId}); - - final int userGroupId; -} +// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult { diff --git a/lib/widgets/autocomplete.dart b/lib/widgets/autocomplete.dart index 773a741efe..ba5dea1b84 100644 --- a/lib/widgets/autocomplete.dart +++ b/lib/widgets/autocomplete.dart @@ -107,10 +107,6 @@ class _ComposeAutocompleteState extends State with PerAccou // TODO(i18n) language-appropriate space character; check active keyboard? // (maybe handle centrally in `widget.controller`) replacementString = '${mention(store.users[userId]!, silent: intent.query.silent, users: store.users)} '; - case WildcardMentionAutocompleteResult(): - replacementString = '[unimplemented]'; // TODO(#234) - case UserGroupMentionAutocompleteResult(): - replacementString = '[unimplemented]'; // TODO(#233) } widget.controller.value = intent.textEditingValue.replaced( @@ -128,10 +124,6 @@ class _ComposeAutocompleteState extends State with PerAccou case UserMentionAutocompleteResult(:var userId): // TODO(#227) avatar label = PerAccountStoreWidget.of(context).users[userId]!.fullName; - case WildcardMentionAutocompleteResult(): - label = '[unimplemented]'; // TODO(#234) - case UserGroupMentionAutocompleteResult(): - label = '[unimplemented]'; // TODO(#233) } return InkWell( onTap: () { From 4ab1a82ebf5f71ba27413fded606af075c871b50 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Wed, 27 Mar 2024 01:11:03 +0430 Subject: [PATCH 2/3] content test [nfc]: Pull `prepareBoringImageHttpClient` out to toplevel Moving `prepareBoringImageHttpClient` method to the toplevel of the file will make it reusable in other files. --- test/widgets/content_test.dart | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 00a7a1cfba..a6bf6b61d2 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -27,6 +27,21 @@ import 'dialog_checks.dart'; import 'message_list_checks.dart'; import 'page_checks.dart'; +/// Set [debugNetworkImageHttpClientProvider] to return a constant image. +/// +/// Returns the [FakeImageHttpClient] that handles the requests. +/// +/// The caller must set [debugNetworkImageHttpClientProvider] back to null +/// before the end of the test. +FakeImageHttpClient prepareBoringImageHttpClient() { + final httpClient = FakeImageHttpClient(); + debugNetworkImageHttpClientProvider = () => httpClient; + httpClient.request.response + ..statusCode = HttpStatus.ok + ..content = kSolidBlueAvatar; + return httpClient; +} + void main() { // For testing a new content feature: // @@ -64,21 +79,6 @@ void main() { }); } - /// Set [debugNetworkImageHttpClientProvider] to return a constant image. - /// - /// Returns the [FakeImageHttpClient] that handles the requests. - /// - /// The caller must set [debugNetworkImageHttpClientProvider] back to null - /// before the end of the test. - FakeImageHttpClient prepareBoringImageHttpClient() { - final httpClient = FakeImageHttpClient(); - debugNetworkImageHttpClientProvider = () => httpClient; - httpClient.request.response - ..statusCode = HttpStatus.ok - ..content = kSolidBlueAvatar; - return httpClient; - } - group('Heading', () { testWidgets('plain h6', (tester) async { await prepareContentBare(tester, From 919cf12e1bfaf772efc7fa204e95a20d0fef8d82 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Wed, 27 Mar 2024 01:22:01 +0430 Subject: [PATCH 3/3] autocomplete: Add user avatars to user-mention autocompletes Fixes: #227 --- lib/widgets/autocomplete.dart | 13 +++++-- test/widgets/autocomplete_test.dart | 54 ++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/widgets/autocomplete.dart b/lib/widgets/autocomplete.dart index ba5dea1b84..ece46a326f 100644 --- a/lib/widgets/autocomplete.dart +++ b/lib/widgets/autocomplete.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'content.dart'; import 'store.dart'; import '../model/autocomplete.dart'; import '../model/compose.dart'; @@ -119,10 +120,11 @@ class _ComposeAutocompleteState extends State with PerAccou Widget _buildItem(BuildContext _, int index) { final option = _resultsToDisplay[index]; + Widget avatar; String label; switch (option) { case UserMentionAutocompleteResult(:var userId): - // TODO(#227) avatar + avatar = Avatar(userId: userId, size: 32, borderRadius: 3); label = PerAccountStoreWidget.of(context).users[userId]!.fullName; } return InkWell( @@ -130,8 +132,13 @@ class _ComposeAutocompleteState extends State with PerAccou _onTapOption(option); }, child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(label))); + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + avatar, + const SizedBox(width: 8), + Text(label), + ]))); } @override diff --git a/test/widgets/autocomplete_test.dart b/test/widgets/autocomplete_test.dart index 6f540111ca..c5028bb0ff 100644 --- a/test/widgets/autocomplete_test.dart +++ b/test/widgets/autocomplete_test.dart @@ -6,6 +6,7 @@ import 'package:zulip/api/model/model.dart'; import 'package:zulip/api/route/messages.dart'; import 'package:zulip/model/compose.dart'; import 'package:zulip/model/narrow.dart'; +import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/store.dart'; @@ -13,11 +14,17 @@ import '../api/fake_api.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../model/test_store.dart'; +import 'content_test.dart'; /// Simulates loading a [MessageListPage] and tapping to focus the compose input. /// /// Also adds [users] to the [PerAccountStore], /// so they can show up in autocomplete. +/// +/// Also sets [debugNetworkImageHttpClientProvider] to return a constant image. +/// +/// The caller must set [debugNetworkImageHttpClientProvider] back to null +/// before the end of the test. Future setupToComposeInput(WidgetTester tester, { required List users, }) async { @@ -39,6 +46,8 @@ Future setupToComposeInput(WidgetTester tester, { messages: [message], ).toJson()); + prepareBoringImageHttpClient(); + await tester.pumpWidget( MaterialApp( localizationsDelegates: ZulipLocalizations.localizationsDelegates, @@ -65,10 +74,26 @@ void main() { TestZulipBinding.ensureInitialized(); group('ComposeAutocomplete', () { + + Finder findNetworkImage(String url) { + return find.byWidgetPredicate((widget) => switch(widget) { + Image(image: NetworkImage(url: var imageUrl)) when imageUrl == url + => true, + _ => false, + }); + } + + void checkUserShown(User user, PerAccountStore store, {required bool expected}) { + check(find.text(user.fullName).evaluate().length).equals(expected ? 1 : 0); + final avatarFinder = + findNetworkImage(store.tryResolveUrl(user.avatarUrl!).toString()); + check(avatarFinder.evaluate().length).equals(expected ? 1 : 0); + } + testWidgets('options appear, disappear, and change correctly', (WidgetTester tester) async { - final user1 = eg.user(userId: 1, fullName: 'User One'); - final user2 = eg.user(userId: 2, fullName: 'User Two'); - final user3 = eg.user(userId: 3, fullName: 'User Three'); + final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png'); + final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png'); + final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png'); final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]); final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); @@ -77,34 +102,37 @@ void main() { await tester.enterText(composeInputFinder, 'hello @user '); await tester.enterText(composeInputFinder, 'hello @user t'); await tester.pumpAndSettle(); // async computation; options appear + // "User Two" and "User Three" appear, but not "User One" - check(tester.widgetList(find.text('User One'))).isEmpty(); - tester.widget(find.text('User Two')); - tester.widget(find.text('User Three')); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: true); + checkUserShown(user3, store, expected: true); // Finishing autocomplete updates compose box; causes options to disappear await tester.tap(find.text('User Three')); await tester.pump(); check(tester.widget(composeInputFinder).controller!.text) .contains(mention(user3, users: store.users)); - check(tester.widgetList(find.text('User One'))).isEmpty(); - check(tester.widgetList(find.text('User Two'))).isEmpty(); - check(tester.widgetList(find.text('User Three'))).isEmpty(); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: false); + checkUserShown(user3, store, expected: false); // Then a new autocomplete intent brings up options again // TODO(#226): Remove this extra edit when this bug is fixed. await tester.enterText(composeInputFinder, 'hello @user tw'); await tester.enterText(composeInputFinder, 'hello @user two'); await tester.pumpAndSettle(); // async computation; options appear - tester.widget(find.text('User Two')); + checkUserShown(user2, store, expected: true); // Removing autocomplete intent causes options to disappear // TODO(#226): Remove one of these edits when this bug is fixed. await tester.enterText(composeInputFinder, ''); await tester.enterText(composeInputFinder, ' '); - check(tester.widgetList(find.text('User One'))).isEmpty(); - check(tester.widgetList(find.text('User Two'))).isEmpty(); - check(tester.widgetList(find.text('User Three'))).isEmpty(); + checkUserShown(user1, store, expected: false); + checkUserShown(user2, store, expected: false); + checkUserShown(user3, store, expected: false); + + debugNetworkImageHttpClientProvider = null; }); }); }