Skip to content

@-mention autocomplete UI #207

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

Merged
merged 9 commits into from
Jul 20, 2023
Merged

Conversation

chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe commented Jun 28, 2023

(edited)

I'll follow up with specific issues this doesn't yet address, but here's a 1.0 of the @-mention autocomplete feature! 🎉

Fixes: #49
Fixes: #129

@chrisbobbe chrisbobbe added the a-compose Compose box, autocomplete, attaching files/images label Jun 28, 2023
@chrisbobbe chrisbobbe requested a review from gnprice June 28, 2023 21:47
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for building this! The code all looks good, with small comments below.

As you mentioned in the office, the UI should get avatars too. (Though if that's complicated for some reason, it'd be fine to leave as a TODO.)

It'd be good to have some tests for this — doesn't need many, given the thorough unit tests on the several areas of underlying logic, but it'd be good to have a few to check that the UI still shows up and still does its thing when the user taps on it.

Comment on lines 207 to 284
class _ContentInputState extends State<_ContentInput> {
MentionAutocompleteView? _mentionAutocompleteView; // TODO different autocomplete view types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this version, _ContentInputState no longer carries any state, and so could be folded back into _ContentInput as a stateless widget.

(Not sure whether that'll remain true in a future version.)

shrinkWrap: true,
itemCount: _resultsToDisplay.length,
itemBuilder: (BuildContext context, int index) {
final option = _resultsToDisplay.elementAt(index);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final option = _resultsToDisplay.elementAt(index);
final option = _resultsToDisplay[index];

For lists they're equivalent; but elementAt is present on Iterable, so seeing it makes it look like we're potentially working with a general Iterable (where it might be slow).

Comment on lines 153 to 155
child: Builder(
builder: (BuildContext context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Builder doing something subtle I'm missing, or can it be simplified out?

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nope, comes from copy-pasting from Flutter's Material Autocomplete then probably removing some stuff.

Comment on lines 155 to 157
return Container(
padding: const EdgeInsets.all(16.0),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
return Container(
padding: const EdgeInsets.all(16.0),
return Padding(
padding: const EdgeInsets.all(16.0),

builder: (BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
child: Text(label ?? 'unknown'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
child: Text(label ?? 'unknown'));
child: Text(label));

and String label; above

padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: _resultsToDisplay.length,
itemBuilder: (BuildContext context, int index) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably be a good spot to split this code up 🙂, as it's a bit of a long widget-expression — so like itemBuilder: _buildItem.

@chrisbobbe chrisbobbe force-pushed the pr-autocomplete-ui branch 4 times, most recently from deeba3d to 23df6ae Compare July 8, 2023 01:05
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed, now with a commit that bumps the Flutter floor to get flutter/flutter#129802.

@chrisbobbe chrisbobbe marked this pull request as ready for review July 8, 2023 01:06
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Generally this looks great; small comments below.

One commit-message nit:

autocomplete: Add basic UI for mention autocompletes

I'd say "@-mention autocompletes", or (a bit more specific) "user-mention autocompletes". I think you're intending "mention" here to mean an @-mention, a mention with a leading @; but I would call the stream-mention syntax #**general** a "mention" too, as well as topic mentions (like stream mentions but with > inside).


Let's also file follow-up issues for a couple of UX items:

  • Options should show the users' avatars.

  • Options should be sorted by name, rather than user ID.

    (Or better yet something more relevant than name: some kind of relevance to search plus relevance to the context, like people subscribed to the stream or that have talked recently in the narrow. But sorting by user ID seems like it's just going to feel weird and arbitrary to people.)

Comment on lines 153 to 154
/// An example empty result, for use in tests.
GetMessagesResult.empty() :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this test data in the test code.

For now since it has just one call site, it can be a private helper in that test file. If we find ourselves needing it in a variety of test files, we'll have a better idea of the right way to generalize it after we see those.

Comment on lines 154 to 157
GetMessagesResult.empty() :
// this `anchor` was observed empirically;
// also, it's not representative of all requests.
anchor = 10000000000000000,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting nit:

Suggested change
GetMessagesResult.empty() :
// this `anchor` was observed empirically;
// also, it's not representative of all requests.
anchor = 10000000000000000,
GetMessagesResult.empty()
: // this `anchor` was observed empirically;
// also, it's not representative of all requests.
anchor = 10000000000000000,

(Otherwise that line ending in a colon looks like it's some very different syntax ­— a label? a fragment of Python or YAML? I actually thought at first it was some other language, and had to reread the filename and the context.)

Comment on lines 56 to 57
group('ComposeAutocomplete', () {
testWidgets('', (WidgetTester tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty name looks a bit odd.

If there's just one test in the group, and you don't feel there's an informative name to give it, probably better to just drop the group and say testWidgets('ComposeAutocomplete',.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I left that to come back to but forgot to come back to it :)

Comment on lines 63 to 74
// BUG (RawAutocomplete): Options list can lag behind the
// MentionAutocompleteView. RawAutocomplete updates the list with a
// listener on the TextEditingValue, not on the MentionAutocompleteView.
// So, this extra edit, which we should delete when the bug is fixed.
await tester.enterText(composeInputFinder, 'hello @user ');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's file an issue for this, so we have something this can link to (which in turn makes the reference here findable by grep). Can be a quick issue with not many more words than this comment; we can always expand it later.

Also let's stick to TODO, rather than sometimes writing BUG. A lot of TODO comments are bugs, and many others might be arguably bugs. If we had both, then when trying to find references to a given issue I don't think it'd be possible to be able to consistently predict which was used, and one would have to search for both.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await tester.enterText(composeInputFinder, 'hello @user t');
await tester.pumpAndSettle(); // async computation; options appear
check(tester.widgetList(find.text('User One'))).isEmpty();
tester.widget(find.text('User Two'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this line is awkward because it looks like it isn't doing anything, but it has an important point which is to check that this widget is present. (I know I've written some similar tester.widget lines.) I initially glossed over this line's checking role and figured it was part of setting up the next set of checks.

I think the typical flutter_test idiom would be expect(find.whatever(), findsOneWidget);. I guess we should find or write some comparable idiom for package:checks.

Anyway, for the moment, perhaps a quick comment to say this is checking that this option appears, or that these three lines are checking that there are these two options appearing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #232 so we don't lose track of this.

Comment on lines +73 to +86
check(tester.widget<TextField>(composeInputFinder).controller!.text)
.contains(mention(user3, users: store.users));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also check that the options have disappeared.

Comment on lines 36 to 37
_viewModel ??= (MentionAutocompleteView.init(store: store, narrow: widget.narrow)
..addListener(_viewModelChanged));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_viewModel ??= (MentionAutocompleteView.init(store: store, narrow: widget.narrow)
..addListener(_viewModelChanged));
_viewModel ??= MentionAutocompleteView.init(store: store, narrow: widget.narrow)
..addListener(_viewModelChanged);

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that pair of parens, I was hoping to make it clear that the listener is only added once; that it doesn't behave like

      (_viewModel ??= MentionAutocompleteView.init(store: store, narrow: widget.narrow))
        ..addListener(_viewModelChanged);

which I think would add a new listener on every keystroke (!) when there's a newAutocompleteIntent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.

I think the way to parse this statement as a reader is to treat ??= as if it were = — all the assignment operators have the same precedence (which in fact is the lowest, or loosest-binding, precedence of all operators). And then one routinely writes things like

  foo = bar()
    ..baz()
    ..quux();

and those mean foo gets the value of that whole right-hand side. So that tells you that this works the same way.

@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

Let's also file follow-up issues for a couple of UX items:

  • Options should show the users' avatars.

  • Options should be sorted by name, rather than user ID.

    (Or better yet something more relevant than name: some kind of relevance to search plus relevance to the context, like people subscribed to the stream or that have talked recently in the narrow. But sorting by user ID seems like it's just going to feel weird and arbitrary to people.)

Filed:

Also:

which I hope to resolve soon as a followup.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! LGTM, with just nits remaining; see below and also #207 (comment) .

Comment on lines 89 to 97
// TODO(#226): Remove one of these edits when this bug is fixed.
await tester.enterText(composeInputFinder, 'hello @user ');
await tester.enterText(composeInputFinder, 'hello @user t');

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these lines intended? It doesn't seem like they're affecting the test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these because I realized that the part that comes after wasn't doing quite what I'd intended:

      // 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();

I wanted to check that removing an AutocompleteIntent (e.g. by changing the text from 'hello @user two' to ' ') causes the options to disappear. If there were a bug and that didn't happen, then without the added lines, the test would have still passed, because the options had disappeared as a result of tapping one of them.

I'll add comments to try to clarify: there are two things that can cause the options to disappear, and we want to check them both:

  • Finishing the autocomplete by tapping an option
  • Removing the autocomplete intent (like by clearing out the input)

@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

Reading [Iterable.takeWhile] more closely than I did when writing
this `mention` function in 6bd11b6, it turns out it's not what we
want here:

  /// Creates a lazy iterable of the leading elements satisfying [test].

Note that it says "*leading* elements".

[Iterable.where] also says it gives a lazy [Iterable], without the
"leading" restriction. Use that, and change the test so it would
fail if the bug were re-introduced.
This looks like something we just forgot to do.
It recently handed off its stateful logic to ComposeAutocomplete.
We're about to write a listener on a MentionAutocompleteView (the
view-model), and we won't want that one to get confused with this.
This widget is all about managing autocompletes, so there isn't
another kind of view model that this is likely to get confused with.
This *looks* like a likely-looking place where we might add this
logic, but perhaps it belongs somewhere else? We'll take a closer
look later.
The UI for now is kept simple; the optionsViewBuilder closely
follows the one in the Material library's `Autocomplete`, which
thinly wraps `RawAutocomplete`:
  https://api.flutter.dev/flutter/material/Autocomplete-class.html

Fixes: zulip#49
Fixes: zulip#129
@gnprice gnprice force-pushed the pr-autocomplete-ui branch from 6e2f3e1 to 234b65d Compare July 20, 2023 22:48
@gnprice
Copy link
Member

gnprice commented Jul 20, 2023

Thanks! All looks good; merging.

@gnprice gnprice merged commit 234b65d into zulip:main Jul 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-compose Compose box, autocomplete, attaching files/images
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support silent @-mention autocomplete @-mention typeahead/autocomplete
2 participants