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

autocomplete: Support @-wildcard in user-mention autocomplete #889

Merged

Conversation

sm-sayedi
Copy link
Collaborator

@sm-sayedi sm-sayedi commented Aug 16, 2024

Screenshot

Screen recording

Wildcard.mention.mp4

Fixes: #234

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 @sm-sayedi! A few high-level comments below.

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,8H4A2,2 0 0,0 2,10V14A2,2 0 0,0 4,16H5V20A1,1 0 0,0 6,21H8A1,1 0 0,0 9,20V16H12L17,20V4L12,8M15,15.6L13,14H4V10H13L15,8.4V15.6M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" /></svg>
Copy link
Member

Choose a reason for hiding this comment

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

What was the origin of this icon — is it from a particular place in Figma?

(I don't see a bullhorn.svg in Zulip web, which would be the other usual source.)

See git log --stat assets/icons/ for example previous commit messages.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That icon was from materialdesignicons.com. Updated the icon in this revision to match it with the web. The web uses a Font Awesome icon.

Copy link
Member

Choose a reason for hiding this comment

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

Cool. That sounds like a case of this issue, then:

Given Vlad's reply there, we'll probably want some other icon for this. I think the most efficient way to resolve that is to start a thread in #mobile and ask Vlad there what icon he'd like us to use.

He did recently finish a design that's related (I still need to update the relevant issue to reflect that):
https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3859-2936&t=z2va3CAInaEg1eWy-0
but looking there, it doesn't have an example for a wildcard, only for a group or an individual user. So go ahead and ask in chat, and include a link to that Figma node for context.

Comment on lines 202 to 205
/// The full name of the wildcard to be shown in autocomplete suggestions.
///
/// Ex: "all (Notify channel)" or "everyone (Notify recipients)".
final String fullDisplayName;
Copy link
Member

Choose a reason for hiding this comment

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

This seems more about our UI than about the Zulip server API. So let's find a home for it outside of lib/api/.

I think a good solution would look similar to MentionAutocompleteResult and its subclasses. Instead of a User being an object that knows how to behave as an @-mention autocomplete candidate, we'd have an object that describes an @-mention autocomplete candidate and refers to a User to do so.

Copy link
Member

Choose a reason for hiding this comment

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

In fact probably a good solution would be for these "autocomplete candidate" objects to be the same objects as the result objects.

I'll try doing a quick refactor in that direction.

Copy link
Member

Choose a reason for hiding this comment

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

(→ #889 (comment) below)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This seems more about our UI than about the Zulip server API. So let's find a home for it outside of lib/api/.

Now that the model class is moved outside of lib/api, is it okay for fullDisplayName to still live in the class?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah. Try to find the arrangement that seems cleanest (and once the PR is ready, reviewers may have suggestions), but between lib/model/ and lib/widgets/ either way is basically fine for where that lives.

Map<String, Wildcard> wildcardsForNarrow(Narrow narrow) => Map.fromEntries(
_wildcardsForNarrow(narrow).map((w) => MapEntry(w.name, w)));

List<Wildcard> _wildcardsForNarrow(Narrow narrow) {
Copy link
Member

Choose a reason for hiding this comment

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

This should live in an autocomplete-specific file — store.dart, and the PerAccountStore class definition, are fairly crowded places, so anything that can naturally go somewhere more specific should do so.

Comment on lines 352 to 353
return [
Wildcard(
Copy link
Member

Choose a reason for hiding this comment

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

Does this mirror the corresponding logic in zulip-mobile? (I haven't read this closely.) A link for comparison would be handy.

Copy link
Collaborator Author

@sm-sayedi sm-sayedi Aug 22, 2024

Choose a reason for hiding this comment

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

It doesn't follow the zulip-mobile in the exact same way regarding the code structure. Here's the zulip-mobile implementation: https://github.com/zulip/zulip-mobile/blob/main/src/autocomplete/WildcardMentionItem.js. Should I provide this link in the code itself?

Copy link
Member

Choose a reason for hiding this comment

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

OK. Let's try to make sure it gets the same results, even if the code ends up organized differently. (There should be test cases in zulip-mobile that will be helpful to translate over, to verify that.)

Then also include a link to that zulip-mobile implementation in either a code comment or commit message. For the link, make it a GitHub permalink:
#692 (comment)
#884 (comment)
That'll be helpful for reviewers, as a point of comparison.

@gnprice
Copy link
Member

gnprice commented Aug 19, 2024

In fact probably a good solution would be for these "autocomplete candidate" objects to be the same objects as the result objects.

I'll try doing a quick refactor in that direction.

OK, I started down that road (generating #895 along the way), and then realized it would mean we'd allocate one of those objects for every user, in MentionAutocompleteView._usersByRelevance and so MentionAutocompleteView.init. That doesn't sound great performance-wise when there are like 30k users in a realm.

Instead, let's handle the wildcard-vs-user distinction in the control flow. Take a look at #896, and build your changes atop that. Then the existing results for an @-mention are computed here:

  Future<List<MentionAutocompleteResult>?> computeResults() async {
    final results = <MentionAutocompleteResult>[];
    if (await filterCandidates(filter: _testUser,
          candidates: sortedUsers, results: results)) {
      return null;
    }
    return results;
  }

where that filterCandidates call does the main loop and accumulates results onto that results local. To add wildcard candidates, you can just add more code next to that, calling List.add on the same results local.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch from 76fd1ac to 1664a2d Compare August 22, 2024 20:36
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the previous review. It was really helpful. Pushed a new revision (still draft). PTAL.

@alya
Copy link
Collaborator

alya commented Aug 22, 2024

We're currently thinking we should use the three-people icon for wildcard mentions, rather than a loudspeaker. Assuming we move forward with it for the web app, we should make the same change here.

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.

Hmm, I missed that this had a request for my review, sorry ­@sm-sayedi — being a draft and with neither of the review labels, it looked like it was awaiting another revision first.

In general any time there's a PR where you're awaiting review for more than a week or two, please feel free to ping on the PR thread or in chat. We generally try to turn around reviews within a day or two, so a wait of over a week is likely a sign that something fell through the cracks.

Comments below. Since this PR is still a draft, I looked only at high-level aspects of the structure.

_isChannelWildcardIncluded = false;
final results = <MentionAutocompleteResult>[];
// give priority to wildcard mentions
if (await filterCandidates(filter: _testWildcard,
Copy link
Member

Choose a reason for hiding this comment

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

The fancy filterCandidates machinery isn't needed for the wildcards, because there's only a handful of them. (It's useful when there are potentially thousands of candidates.) So this can be simplified to a direct loop through the wildcard candidates.

That also lets _inChannelWildcardIncluded be a local variable, which simplifies reasoning about it.

… Then I started writing that the logic might be further clarified by unrolling the loop, akin to the logic in zulip-mobile:
https://github.com/zulip/zulip-mobile/blob/715d60a5e87fe37032bce58bd72edb99208e15be/src/autocomplete/WildcardMentionItem.js#L113-L138

But I see we discussed this previously above:
#889 (comment)
and that you have the corresponding logic living instead in the widgets code, in ComposeAutocomplete._wildcards. That structure may be fine too. Should have a link to the zulip-mobile version, though, as discussed there.

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 switched back to a similar logic of what zulip-mobile does. I think it fits our current code structure well.

final List<User> sortedUsers;

@override
Future<List<MentionAutocompleteResult>?> computeResults() async {
Copy link
Member

Choose a reason for hiding this comment

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

Moving this method to lower in the file is perfectly reasonable, but should happen as its own separate NFC prep commit. That way the substantive change stands out better.

Comment on lines 182 to 185
fullDisplayName: 'all (${isDmNarrow
? zulipLocalizations.notifyRecipients
: zulipLocalizations.notifyChannel(isChannelWildcardAvailable
? "channel" : "stream")})',
Copy link
Member

Choose a reason for hiding this comment

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

I think it will be cleaner to move this fullDisplayName logic to where the result is actually being displayed, at build time.

Copy link
Member

Choose a reason for hiding this comment

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

Also avoid string manipulation on translated strings. See the "general background" at the end of this section:
https://github.com/zulip/zulip-mobile/blob/main/docs/howto/translations.md#all-code-contributors-wiring-our-code-for-translations

Instead, put the two parts of this into separate widgets or TextSpans. That way they can also get different text styles.

case WildcardMentionAutocompleteResult(:var wildcardName):
avatar = const Icon(ZulipIcons.bullhorn, size: 29); // web uses 19px
print('wildcard name: $wildcardName');
label = _wildcards(context, store).singleWhere((w) => w.name == wildcardName).fullDisplayName;
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 avoid doing a search like this, and instead have the MentionAutocompleteResult value include whatever information this needs.

(If we did do a search, then like with store.users[userId] above, we should make it so that we're just looking it up in an existing data structure rather than recomputing the whole list again.)

In fact I think I'd like to refactor UserMentionAutocompleteResult so that it carries the actual User object, rather than just a user ID. That change is orthogonal to this PR, though.

@gnprice gnprice force-pushed the issue-234-wildcard-@-mention-autocomplete branch from 1664a2d to a24bddb Compare November 8, 2024 00:01
@gnprice
Copy link
Member

gnprice commented Nov 8, 2024

Also just pushed a rebase of the branch. (I did the rebase in order to better review it, since some things had changed in main; having done that, I figure I might as well share the result.)

The main change is the rebase across e91694f (#908), which causes some of the changes here to disappear as having already happened.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch 5 times, most recently from 078194e to b0a6651 Compare December 6, 2024 07:41
@sm-sayedi sm-sayedi marked this pull request as ready for review December 6, 2024 07:42
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the initial reviews. The PR is now ready for the maintainer review.

Note: The first two commits are actually necessary for displaying wildcard mentions based on a permission. I first created those commits and then started the CZO discussion , in which it was decided to leave the permission part for a future issue. I am not sure if we want to keep these two commits for now!

Comment on lines 284 to 282
return Text.rich(TextSpan(text: '${wildcard.name} ', children: [
TextSpan(text: description, style: TextStyle(fontSize: 12,
color: Colors.black.withValues(alpha: 0.8)))]));
Copy link
Collaborator Author

@sm-sayedi sm-sayedi Dec 6, 2024

Choose a reason for hiding this comment

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

Web uses 85% of the name (i.e., all) font size for the description (i.e., Notify channel). For us, 85% of 14 is almost 12.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch from b0a6651 to a0ab0ed Compare December 6, 2024 08:00
@sm-sayedi sm-sayedi added the maintainer review PR ready for review by Zulip maintainers label Dec 6, 2024
@sm-sayedi
Copy link
Collaborator Author

The CI is failing and it's related to icons. I am not sure why it is happening!

@gnprice
Copy link
Member

gnprice commented Dec 6, 2024

The error output is:

Running icons...

added 162 packages in 29s
Error: there were icon updates:
 M assets/icons/ZulipIcons.ttf

Seems like that font file isn't updated to match your other changes. The icons.dart file has instructions for updating for icons.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch 3 times, most recently from 9c80cb0 to e873ea4 Compare December 6, 2024 17:06
@sm-sayedi
Copy link
Collaborator Author

sm-sayedi commented Dec 6, 2024

Just transferred the CI failure discussion to CZO: https://chat.zulip.org/#narrow/channel/516-mobile-dev-help/topic/CI.20failure.20related.20to.20Zulip.20icons.20-.20.23F889/near/1994794.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch 2 times, most recently from 0af384f to 06918d3 Compare December 9, 2024 06:44
@sm-sayedi sm-sayedi requested a review from chrisbobbe January 26, 2025 06:54
@chrisbobbe
Copy link
Collaborator

Thanks, LGTM! Marking for Greg's review.

@chrisbobbe chrisbobbe assigned gnprice and unassigned chrisbobbe Jan 27, 2025
@chrisbobbe chrisbobbe requested a review from gnprice January 27, 2025 22:37
@chrisbobbe chrisbobbe added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Jan 27, 2025
@chrisbobbe
Copy link
Collaborator

While reviewing, I noticed a new bug, but it's caused by an upstream change, not the changes here; filed as #1310.

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 @sm-sayedi for carrying this through! And thanks @chrisbobbe for the previous reviews.

Generally this looks good. Some comments below. I haven't yet read the test changes, because I think a couple of these comments will lead to some simplifications there.

Comment on lines +123 to +124
/// The Zulip custom icon "three_person".
static const IconData three_person = IconData(0xf121, fontFamily: "Zulip Icons");
Copy link
Member

Choose a reason for hiding this comment

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

nit:

assets: Add "three_person.svg" icon for wildcard mentions

The prefix we've used for this is "icons:" — see git log --oneline assets/icons.

Comment on lines 69 to 87
// (hint text of compose input in a 1:1 DM)
final finder = find.widgetWithText(TextField, 'Message @${eg.otherUser.fullName}');
final finder = find.widgetWithText(TextField,
_composeInputHintTextFor(narrow, store: store));
Copy link
Member

Choose a reason for hiding this comment

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

This _composeInputHintTextFor helper is a fair bit of code, and seems brittle given that its details aren't something this test file is meant to be testing.

Instead, let's borrow what we have in compose_box_test.dart:

  final contentInputFinder = find.byWidgetPredicate(
    (widget) => widget is TextField && widget.controller is ComposeContentController);

It's true that's a bit less end-to-end (the user sees the hint text and doesn't see the controller class is called "ComposeContentController"), but it should be more stable. The user doesn't care what the exact hint text is — outside of the tests for the hint text itself, what we really mean here is "the box that looks to the user like the compose box". It's tough to express that crisply, so we can settle for the ComposeContentController thing.

At least ComposeContentController isn't really an implementation detail — there are several different features of the product that all mean we'll want some kind of custom input-controller logic for the message content input, and will want to expose that to other code in the app, so it's a pretty stable assumption that we'll have something playing more or less the role of ComposeContentController indefinitely.

Comment on lines 15 to 17
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() {
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent(ZulipLocalizations localizations) {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I don't love having the localizations live on the AutocompleteQuery. That feels like it leads to having to route it through places that shouldn't have to know about it, like this.

Let's instead handle it the same way we handle the store: the view-model (the AutocompleteView) keeps around a reference to it, and passes that reference when needed to methods on the query that need it.

Then the autocomplete subsystem can get access to the localizations in the first place the same way it does for the store: the ComposeAutocomplete.initViewModel method, which has a build context, gets it from the context and passes it along.

Copy link
Collaborator Author

@sm-sayedi sm-sayedi Jan 29, 2025

Choose a reason for hiding this comment

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

In the current revision, the base AutocompleteView class holds a reference to ZulipLocalizations. With that, all the subclasses (should) retrieve the instance through FooAutocompleteView.init. What if we only have the ZulipLocalizations property in MentionAutocompleteView, not in the base class? I think for now, having localizations only makes sense in MentionAutocompleteView not the other two subclasses. Then if in the future, it makes sense for the others too, we can refactor the code and move ZulipLocalizations localizations to the base class, just like in this revision.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think having the localizations live either on the base class or the subclass would be fine — whichever seems easier and/or produces cleaner code right now. I'm not sure other types of autocomplete will ever need localizations; if they do, then add you say, we can refactor then.

Copy link
Member

Choose a reason for hiding this comment

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

OK, and now having read this revision: I agree, moving it to the subclass MentionAutocompleteView will make things cleaner.

Comment on lines 641 to 645
results.addAll(computeWildcardMentionResults(isComposingChannelMessage:
narrow is ChannelNarrow || narrow is TopicNarrow));

if (await filterCandidates(filter: _testUser,
candidates: sortedUsers, results: results)) {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of making a fresh list and passing it to addAll, have the callee accumulate results on results directly:

Suggested change
results.addAll(computeWildcardMentionResults(isComposingChannelMessage:
narrow is ChannelNarrow || narrow is TopicNarrow));
if (await filterCandidates(filter: _testUser,
candidates: sortedUsers, results: results)) {
computeWildcardMentionResults(results: results,
isComposingChannelMessage: narrow is ChannelNarrow || narrow is TopicNarrow);
if (await filterCandidates(filter: _testUser,
candidates: sortedUsers, results: results)) {

That's a more efficient style since it involves less allocation and copying. Not a big deal for this particular helper method — there's at most a couple of these wildcard-mention results — but it's the style that computeResults is already working in, so let's follow along. (And it's important that the existing logic in computeResults works that way, because there may be a lot of individual-user results.)

Comment on lines 611 to 616
if (query.testWildcardOption(WildcardMentionOption.all)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: WildcardMentionOption.all));
} else if (query.testWildcardOption(WildcardMentionOption.everyone)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: WildcardMentionOption.everyone));
Copy link
Member

Choose a reason for hiding this comment

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

This ends up with a kind of unfortunate amount of verbosity, and it feels like it'd be easy to miss the key details that matter in it.

Here's one way to tighten it up, with a local helper function (inside this method) tryOption:

Suggested change
if (query.testWildcardOption(WildcardMentionOption.all)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: WildcardMentionOption.all));
} else if (query.testWildcardOption(WildcardMentionOption.everyone)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: WildcardMentionOption.everyone));
all: {
if (tryOption(WildcardMentionOption.all)) break all;
if (tryOption(WildcardMentionOption.everyone)) break all;

So tryOption would do both the query.test… call, and the results.add when the answer is yes.

The break statements there are doing a similar job to an early return, but without needing to wrap this subgroup of the logic in a function just to get early returns.

// Deprecated in FL 247. Empirically, current servers (FL 330)
// still parse "@**stream**" in messages though.
stream(canonicalString: 'stream'),
topic(canonicalString: 'topic'); // New in FL 224.
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
topic(canonicalString: 'topic'); // New in FL 224.
topic(canonicalString: 'topic'); // TODO(server-8): New in FL 224.

The TODO-server comment is how we make sure we can systematically clean out these historical comments in the future when they're no longer relevant. Basically all references to a Zulip feature level (as well as anything else that will become obsolete when we drop support for some old Zulip Server version) should have a TODO-server comment that will make sure we find it.

/// The string identifying this option (e.g. "all" as in "@**all**").
// If servers want to localize this, we can compute it
// dynamically from server data instead of hard-coding it.
final String canonicalString;
Copy link
Member

Choose a reason for hiding this comment

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

I don't 100% understand this comment (the // part), but I think it's speculating about a possible future change to the API. We can leave that out — if the API changes, we'll deal with it then.

Comment on lines 145 to 152
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
assert(isChannelWildcardAvailable || wildcardOption != WildcardMentionOption.channel);
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
assert(isTopicWildcardAvailable || wildcardOption != WildcardMentionOption.topic);

final name = wildcardOption == WildcardMentionOption.stream && isChannelWildcardAvailable
? WildcardMentionOption.channel.canonicalString
: wildcardOption.canonicalString;
Copy link
Member

Choose a reason for hiding this comment

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

All these equality comparisons to different enum values feel like they'd be clearer as one switch statement that lists all the values.

Could start with String name = wildcardOption.canonicalString; and then have the switch mutate it in one case, to avoid repeating the common version and have the exception stand out.

]));
}

Widget wildcardLabel(WildcardMentionOption wildcard, {
Copy link
Member

Choose a reason for hiding this comment

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

nit: put this method before build; and mark it private

};
return Text.rich(TextSpan(text: '${wildcard.canonicalString} ', children: [
TextSpan(text: description, style: TextStyle(fontSize: 12,
color: Colors.black.withValues(alpha: 0.8)))]));
Copy link
Member

Choose a reason for hiding this comment

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

Does this color work equally well in dark theme?

Let's move it to DesignVariables so the theme behavior is explicit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for pointing it out. It didn't look good in the dark theme. Replaced it with the following, and now it looks good:

color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.8)

Does it still need to be defined in DesignVariables?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that's fine.

In principle an expression like that should still go into DesignVariables, because it still might vary by theme — for example, if in the future we add "high contrast" themes, they might want to keep the alpha at 1, or set it to 90% or something.

But that isn't a feature we have on the horizon. And these design details will get replaced soon anyway with #995 / #913. So with this solution which works for the existing themes, it's not worth the overhead to add to DesignVariables.

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch from 4e59600 to afaa43d Compare January 29, 2025 14:18
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the review. Changes pushed, with two comments, one just above this comment and another in here. PTAL.

all(canonicalString: 'all'),
everyone(canonicalString: 'everyone'),
channel(canonicalString: 'channel'),
// Deprecated in FL 247. Empirically, current servers (FL 330)
Copy link
Member

Choose a reason for hiding this comment

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

needs a TODO-server comment (similar to #889 (comment) )

}
}

final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
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 isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(server-8)

The spelling is important for these to serve their purpose of being findable via grep 🙂

(there's one other like this)

Comment on lines 171 to 175
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() => controller.autocompleteIntent();
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent(
BuildContext context) => controller.autocompleteIntent();
Copy link
Member

Choose a reason for hiding this comment

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

These changes are no longer needed, since the query and intent no longer carry the localizations.

Comment on lines 15 to 17
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() {
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent(ZulipLocalizations localizations) {
Copy link
Member

Choose a reason for hiding this comment

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

OK, and now having read this revision: I agree, moving it to the subclass MentionAutocompleteView will make things cleaner.

@gnprice
Copy link
Member

gnprice commented Jan 29, 2025

Thanks for the revision! I've read through the whole lib/ changes again, and just a few comments above. I'll take a look at the tests next.

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.

OK, and read through the tests too. Comments below.

.throws<AssertionError>();
});
});

group('computeWildcardMentionResults', () {
Copy link
Member

Choose a reason for hiding this comment

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

nit: this can be moved out of the enclosing group, I think — it's not really about "sorting" these results, but deciding which results appear

Here's what the current set of full test names looks like:

$ flutter test test/model/autocomplete_test.dart --name computeWi -r expanded
00:00 +0: loading /home/greg/z/flutter/test/model/autocomplete_test.dart
00:00 +0: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "" query in ChannelNarrow narrow wildcards are [WildcardMentionOption.all, WildcardMentionOption.topic]
00:00 +1: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "" query in TopicNarrow narrow wildcards are [WildcardMentionOption.all, WildcardMentionOption.topic]
00:00 +2: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "" query in DmNarrow narrow wildcards are [WildcardMentionOption.all]
00:00 +3: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "c" query in ChannelNarrow narrow wildcards are [WildcardMentionOption.channel, WildcardMentionOption.topic]
00:00 +4: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "ch" query in TopicNarrow narrow wildcards are [WildcardMentionOption.channel]
00:00 +5: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "str" query in ChannelNarrow narrow wildcards are [WildcardMentionOption.stream]
00:00 +6: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "e" query in TopicNarrow narrow wildcards are [WildcardMentionOption.everyone]
00:00 +7: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "everyone" query in ChannelNarrow narrow wildcards are [WildcardMentionOption.everyone]
00:00 +8: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "t" query in TopicNarrow narrow wildcards are [WildcardMentionOption.stream, WildcardMentionOption.topic]
00:00 +9: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "topic" query in ChannelNarrow narrow wildcards are [WildcardMentionOption.topic]
00:00 +10: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "topic etc" query in TopicNarrow narrow wildcards are []
00:00 +11: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "a" query in DmNarrow narrow wildcards are [WildcardMentionOption.all]
00:00 +12: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "every" query in DmNarrow narrow wildcards are [WildcardMentionOption.everyone]
00:00 +13: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "channel" query in DmNarrow narrow wildcards are []
00:00 +14: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "stream" query in DmNarrow narrow wildcards are []
00:00 +15: MentionAutocompleteView sorting mention results computeWildcardMentionResults for "topic" query in DmNarrow narrow wildcards are []
00:00 +16: MentionAutocompleteView sorting mention results computeWildcardMentionResults no wildcards for a silent mention
00:00 +17: MentionAutocompleteView sorting mention results computeWildcardMentionResults WildcardMentionOption.channel is available FL-247 onwards
00:00 +18: MentionAutocompleteView sorting mention results computeWildcardMentionResults WildcardMentionOption.channel is not available before FL-247
00:00 +19: MentionAutocompleteView sorting mention results computeWildcardMentionResults WildcardMentionOption.topic is available FL-224 onwards
00:00 +20: MentionAutocompleteView sorting mention results computeWildcardMentionResults WildcardMentionOption.topic is not available before FL-224
00:00 +21: All tests passed!

So that's quite a long prefix on them, which makes it nice to cut out.

];

for (final (String query, Narrow narrow, List<WildcardMentionOption> wildcardOptions) in testCases) {
test('for "$query" query in ${narrow.runtimeType} narrow wildcards are $wildcardOptions', () async {
Copy link
Member

Choose a reason for hiding this comment

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

nit: can tighten this (see list of test names in other comment just above):

Suggested change
test('for "$query" query in ${narrow.runtimeType} narrow wildcards are $wildcardOptions', () async {
test('query "$query" in ${narrow.runtimeType} -> $wildcardOptions', () async {

The "narrow" part is redundant because the names are "DmNarrow" etc. The "wildcards are" part is clear from the context that it's under "computeWildcardMentionResults".

Comment on lines 810 to 812
('ch', topicNarrow, [WildcardMentionOption.channel]),
('str', channelNarrow, [WildcardMentionOption.stream]),
('e', topicNarrow, [WildcardMentionOption.everyone]),
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I guess with the English names there's not actually a way to test the fact that .channel is preferred over .stream — the only letters they have in common are "a" and "e", and those queries also match the more-preferred .all and .everyone respectively.

Copy link
Collaborator Author

@sm-sayedi sm-sayedi Jan 30, 2025

Choose a reason for hiding this comment

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

Yeah, that's right! But in the new revision, it is exercised by the localized test cases at least!

}

test('no wildcards for a silent mention', () {
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow)).isEmpty();
Copy link
Member

Choose a reason for hiding this comment

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

nit: line too long: key information (the .isEmpty) past 80 columns:

Suggested change
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow)).isEmpty();
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow))
.isEmpty();

.throws<AssertionError>();
});
});

group('computeWildcardMentionResults', () {
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 have a couple of test cases exercising the fact that queries match against the localized names of "all", "channel", etc. After all, you've done significant work for the plumbing to make that possible — would a shame to let it get accidentally broken in the future because we lacked a test for it 🙂

Since you've included Arabic translations for the new strings, you could use those for those test cases.

// 3. Human vs. Bot users (human users come first).
// 4. Alphabetical order by name.
check(await getResults(topicNarrow, MentionAutocompleteQuery('')))
// 1. Wildcards.
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
// 1. Wildcards.
// 1. Wildcards before individual users.

This numbered list isn't really a list of categories from first to last in the results, but more a list of ranking criteria from highest to lowest priority in their effect on the order of results. (E.g. compare the "human vs. bot users" item.)

Comment on lines +912 to 839
final results1 = await getResults(topicNarrow, MentionAutocompleteQuery(''));
check(getWildcardOptionsFromResults(results1.take(2)))
.deepEquals([WildcardMentionOption.all, WildcardMentionOption.topic]);
check(getUsersFromResults(results1.skip(2)))
.deepEquals([1, 5, 4, 2, 7, 3, 6]);
Copy link
Member

Choose a reason for hiding this comment

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

Cool, this structure works.

FYI the more general solution for this sort of situation — the one we'd need to go for if this got a bit more complicated, for example if the two types of results were interleaved ­— is to use deepEquals on the original list and pass an expected list of Condition values, instead of values to do equality checks on. You can find some examples in test/notifications/display_test.dart, like this:

      check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
        conditionActiveNotif(data1, conversationKey1),
        conditionSummaryActiveNotif(expectedGroupKey),
        conditionActiveNotif(data2, conversationKey2),
      ]);

where conditionSummaryActiveNotif and conditionActiveNotif are helpers local to that test file.

Comment on lines 239 to 242
// 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 @chan');
await tester.enterText(composeInputFinder, 'hello @channel');
Copy link
Member

Choose a reason for hiding this comment

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

I think this and the remaining steps of the test can be left out here — they're basically redundant with the test case above, since the logic controlling this doesn't depend on the particular type of result being shown.

Comment on lines 221 to 223
checkWildcardShown(WildcardMentionOption.channel, store, expected: true);
checkWildcardShown(WildcardMentionOption.topic, store, expected: true);
checkWildcardShown(WildcardMentionOption.all, store, expected: false);
Copy link
Member

Choose a reason for hiding this comment

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

(By contrast this step of the test is important, because it lets us confirm that the options do get shown — and have the pieces that checkWildcardShown checks for — and that no errors get thrown when doing so.

IOW the enterText and pump above exercise the wildcard-specific logic that's been added to the _MentionAutocompleteItem widget, and these checks confirm that that logic successfully got run and produced the right result.)

@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch 2 times, most recently from 7ca409a to 826a5a2 Compare January 30, 2025 19:29
: localizations.wildcardMentionStreamDescription,
WildcardMentionOption.topic => localizations.wildcardMentionTopicDescription,
};
return Text.rich(TextSpan(text: '${wildcardOption.canonicalString} ', children: [
Copy link
Collaborator Author

@sm-sayedi sm-sayedi Jan 30, 2025

Choose a reason for hiding this comment

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

What about replacing wildcardOption.canonicalString with wildcardOption.localizedCanonicalString?! I think canonicalString would cause some confusion when the app is used in other languages. For example, when someone tries to choose @all or @everyone wildcards by writing its translation in the compose box, it will not be straightforward which word to write down in their language to match "all" or "everyone" because there may be different synonyms of these words! And the translated description of these two wildcards will also not hint at that word, because it always says: "Notify channel/stream".

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, could be good. What does web do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Web shows the English version of wildcards and only matches for the English strings!

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see, so it doesn't even let you search for them by the names in your language? I guess that must have been something we thought of for zulip-mobile.

This UX will be fine to merge — it's the same as in the existing mobile app in this respect, and that one difference from web (the ability to match your language's term) seems like clearly an improvement. I agree it seems suboptimal that there's no direct hint of what term in your language will match an option.

Copy link
Member

Choose a reason for hiding this comment

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

If you want to pursue making that UX change, the best place to discuss would be in a chat thread so more people can see it — either in #mobile, or in #feedback if you think it should apply to web too (and maybe web should gain the feature mobile has where you can search by your language's term in the first place).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess that must have been something we thought of for zulip-mobile.

Correct

@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the review. Pushed the changes. Please have a look.

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! Looks good, with just a few small comments below. There's also a UX question you asked above at #889 (comment) (but it won't block merge).

Comment on lines 7 to 9
static ZulipLocalizations zulipLocalizationsArabic =
lookupZulipLocalizations(ZulipLocalizations.supportedLocales
.firstWhere((locale) => locale.languageCode == 'ar'));
Copy link
Member

Choose a reason for hiding this comment

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

This is for use only in tests, so it should go in test code instead. (Probably just in the specific test file that's using it — other tests that use a particular translation might use other languages.)

Copy link
Member

Choose a reason for hiding this comment

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

(The existing field next to this one refers to whatever language the user has chosen — it's initialized here to English, but gets updated by ZulipApp as soon as the app starts up far enough to know the user's preference.)

Copy link
Member

Choose a reason for hiding this comment

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

I also noticed there were no docs in this file, so I just added some:
1731385 i18n [nfc]: Add doc on GlobalLocalizations.zulipLocalizations

Hopefully that makes the story clearer.

@@ -615,6 +602,59 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
return userAName.compareTo(userBName); // TODO(i18n): add locale-aware sorting
}

void computeWildcardMentionResults({
Copy link
Member

Choose a reason for hiding this comment

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

nit in commit message:

autocomplete: Support @-wildcard in user-mention autocomplete

The implementation logic is similar to the zulip-mobile implementation:
  https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c

017d786/src/autocomplete/WildcardMentionItem.js

There's a few other things going on in that file, so this link can be made more precise with a line-number range:
https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/autocomplete/WildcardMentionItem.js#L92-L140

Copy link
Member

Choose a reason for hiding this comment

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

Then there's a few ways we've made this differ from zulip-mobile, so let's briefly mention those:

  • suppress wildcards on silent mentions
  • match "stream" queries even on new servers (but emit @**channel**)
  • possibly something I'm forgetting

Comment on lines 428 to 429
required PerAccountStore store,
required Narrow narrow,
required MentionAutocompleteQuery query,
required Narrow narrow,
required ZulipLocalizations localizations,
Copy link
Member

Choose a reason for hiding this comment

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

nit: logically the store and the localizations are a lot alike — they're more or less global context that the whole app, or a large swath of it, is operating within — so let's keep them together. Then the narrow is somewhat more-specific context, describing the whole current page, and the query is more specific than that, so should follow in that order.

@@ -419,42 +421,29 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
required super.query,
required this.narrow,
required this.sortedUsers,
required this.localizations,
Copy link
Member

Choose a reason for hiding this comment

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

nit: similarly, put this above the narrow and sorted users

This will become handy for setting up a compose box in different types
of narrows where compose box is supported.
This will result in a more logical order of the class members.
The implementation logic is mostly similar to the zulip-mobile implementation:
  https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/autocomplete/WildcardMentionItem.js#L92-L141

It is different from the zulip-mobile in the following ways:
  - suppress wildcards on silent mentions
  - match "stream" queries even on new servers (but emit @**channel**)

Fixes: zulip#234
@sm-sayedi sm-sayedi force-pushed the issue-234-wildcard-@-mention-autocomplete branch from 826a5a2 to 449f326 Compare January 31, 2025 03:23
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice. Pushed a new revision.

@gnprice
Copy link
Member

gnprice commented Jan 31, 2025

Thanks again for all your work on this! All looks great now — merging.

@gnprice gnprice merged commit 449f326 into zulip:main Jan 31, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

autocomplete: Support wildcard @-mentions
4 participants