-
Notifications
You must be signed in to change notification settings - Fork 258
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
autocomplete: Support @-wildcard in user-mention autocomplete #889
Conversation
There was a problem hiding this 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.
assets/icons/bullhorn.svg
Outdated
@@ -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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/api/model/model.dart
Outdated
/// The full name of the wildcard to be shown in autocomplete suggestions. | ||
/// | ||
/// Ex: "all (Notify channel)" or "everyone (Notify recipients)". | ||
final String fullDisplayName; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(→ #889 (comment) below)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/model/store.dart
Outdated
Map<String, Wildcard> wildcardsForNarrow(Narrow narrow) => Map.fromEntries( | ||
_wildcardsForNarrow(narrow).map((w) => MapEntry(w.name, w))); | ||
|
||
List<Wildcard> _wildcardsForNarrow(Narrow narrow) { |
There was a problem hiding this comment.
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.
lib/model/store.dart
Outdated
return [ | ||
Wildcard( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 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 |
76fd1ac
to
1664a2d
Compare
Thanks @gnprice for the previous review. It was really helpful. Pushed a new revision (still draft). PTAL. |
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. |
There was a problem hiding this 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.
lib/model/autocomplete.dart
Outdated
_isChannelWildcardIncluded = false; | ||
final results = <MentionAutocompleteResult>[]; | ||
// give priority to wildcard mentions | ||
if (await filterCandidates(filter: _testWildcard, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
lib/widgets/autocomplete.dart
Outdated
fullDisplayName: 'all (${isDmNarrow | ||
? zulipLocalizations.notifyRecipients | ||
: zulipLocalizations.notifyChannel(isChannelWildcardAvailable | ||
? "channel" : "stream")})', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 TextSpan
s. That way they can also get different text styles.
lib/widgets/autocomplete.dart
Outdated
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; |
There was a problem hiding this comment.
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.
1664a2d
to
a24bddb
Compare
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. |
078194e
to
b0a6651
Compare
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! |
lib/widgets/autocomplete.dart
Outdated
return Text.rich(TextSpan(text: '${wildcard.name} ', children: [ | ||
TextSpan(text: description, style: TextStyle(fontSize: 12, | ||
color: Colors.black.withValues(alpha: 0.8)))])); |
There was a problem hiding this comment.
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.
b0a6651
to
a0ab0ed
Compare
The CI is failing and it's related to icons. I am not sure why it is happening! |
The error output is:
Seems like that font file isn't updated to match your other changes. The icons.dart file has instructions for updating for icons. |
9c80cb0
to
e873ea4
Compare
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. |
0af384f
to
06918d3
Compare
Thanks, LGTM! Marking for Greg's review. |
While reviewing, I noticed a new bug, but it's caused by an upstream change, not the changes here; filed as #1310. |
There was a problem hiding this 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.
/// The Zulip custom icon "three_person". | ||
static const IconData three_person = IconData(0xf121, fontFamily: "Zulip Icons"); |
There was a problem hiding this comment.
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
.
test/widgets/autocomplete_test.dart
Outdated
// (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)); |
There was a problem hiding this comment.
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.
lib/model/autocomplete.dart
Outdated
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() { | ||
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent(ZulipLocalizations localizations) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/model/autocomplete.dart
Outdated
results.addAll(computeWildcardMentionResults(isComposingChannelMessage: | ||
narrow is ChannelNarrow || narrow is TopicNarrow)); | ||
|
||
if (await filterCandidates(filter: _testUser, | ||
candidates: sortedUsers, results: results)) { |
There was a problem hiding this comment.
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:
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.)
lib/model/autocomplete.dart
Outdated
if (query.testWildcardOption(WildcardMentionOption.all)) { | ||
wildcardMentions.add(WildcardMentionAutocompleteResult( | ||
wildcard: WildcardMentionOption.all)); | ||
} else if (query.testWildcardOption(WildcardMentionOption.everyone)) { | ||
wildcardMentions.add(WildcardMentionAutocompleteResult( | ||
wildcard: WildcardMentionOption.everyone)); |
There was a problem hiding this comment.
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
:
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.
lib/model/compose.dart
Outdated
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
lib/model/compose.dart
Outdated
/// 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; |
There was a problem hiding this comment.
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.
lib/model/compose.dart
Outdated
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; |
There was a problem hiding this comment.
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.
lib/widgets/autocomplete.dart
Outdated
])); | ||
} | ||
|
||
Widget wildcardLabel(WildcardMentionOption wildcard, { |
There was a problem hiding this comment.
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
lib/widgets/autocomplete.dart
Outdated
}; | ||
return Text.rich(TextSpan(text: '${wildcard.canonicalString} ', children: [ | ||
TextSpan(text: description, style: TextStyle(fontSize: 12, | ||
color: Colors.black.withValues(alpha: 0.8)))])); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
4e59600
to
afaa43d
Compare
lib/model/compose.dart
Outdated
all(canonicalString: 'all'), | ||
everyone(canonicalString: 'everyone'), | ||
channel(canonicalString: 'channel'), | ||
// Deprecated in FL 247. Empirically, current servers (FL 330) |
There was a problem hiding this comment.
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) )
lib/model/autocomplete.dart
Outdated
} | ||
} | ||
|
||
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
lib/widgets/autocomplete.dart
Outdated
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() => controller.autocompleteIntent(); | ||
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent( | ||
BuildContext context) => controller.autocompleteIntent(); |
There was a problem hiding this comment.
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.
lib/model/autocomplete.dart
Outdated
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() { | ||
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent(ZulipLocalizations localizations) { |
There was a problem hiding this comment.
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.
Thanks for the revision! I've read through the whole |
There was a problem hiding this 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.
test/model/autocomplete_test.dart
Outdated
.throws<AssertionError>(); | ||
}); | ||
}); | ||
|
||
group('computeWildcardMentionResults', () { |
There was a problem hiding this comment.
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.
test/model/autocomplete_test.dart
Outdated
]; | ||
|
||
for (final (String query, Narrow narrow, List<WildcardMentionOption> wildcardOptions) in testCases) { | ||
test('for "$query" query in ${narrow.runtimeType} narrow wildcards are $wildcardOptions', () async { |
There was a problem hiding this comment.
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):
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".
test/model/autocomplete_test.dart
Outdated
('ch', topicNarrow, [WildcardMentionOption.channel]), | ||
('str', channelNarrow, [WildcardMentionOption.stream]), | ||
('e', topicNarrow, [WildcardMentionOption.everyone]), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/model/autocomplete_test.dart
Outdated
} | ||
|
||
test('no wildcards for a silent mention', () { | ||
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow)).isEmpty(); |
There was a problem hiding this comment.
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:
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow)).isEmpty(); | |
check(getWildcardOptionsFor('', isSilent: true, narrow: channelNarrow)) | |
.isEmpty(); |
test/model/autocomplete_test.dart
Outdated
.throws<AssertionError>(); | ||
}); | ||
}); | ||
|
||
group('computeWildcardMentionResults', () { |
There was a problem hiding this comment.
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.
test/model/autocomplete_test.dart
Outdated
// 3. Human vs. Bot users (human users come first). | ||
// 4. Alphabetical order by name. | ||
check(await getResults(topicNarrow, MentionAutocompleteQuery(''))) | ||
// 1. Wildcards. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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.)
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]); |
There was a problem hiding this comment.
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.
test/widgets/autocomplete_test.dart
Outdated
// 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'); |
There was a problem hiding this comment.
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.
test/widgets/autocomplete_test.dart
Outdated
checkWildcardShown(WildcardMentionOption.channel, store, expected: true); | ||
checkWildcardShown(WildcardMentionOption.topic, store, expected: true); | ||
checkWildcardShown(WildcardMentionOption.all, store, expected: false); |
There was a problem hiding this comment.
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.)
7ca409a
to
826a5a2
Compare
: localizations.wildcardMentionStreamDescription, | ||
WildcardMentionOption.topic => localizations.wildcardMentionTopicDescription, | ||
}; | ||
return Text.rich(TextSpan(text: '${wildcardOption.canonicalString} ', children: [ |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
Thanks @gnprice for the review. Pushed the changes. Please have a look. |
There was a problem hiding this 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).
lib/model/localizations.dart
Outdated
static ZulipLocalizations zulipLocalizationsArabic = | ||
lookupZulipLocalizations(ZulipLocalizations.supportedLocales | ||
.firstWhere((locale) => locale.languageCode == 'ar')); |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
lib/model/autocomplete.dart
Outdated
required PerAccountStore store, | ||
required Narrow narrow, | ||
required MentionAutocompleteQuery query, | ||
required Narrow narrow, | ||
required ZulipLocalizations localizations, |
There was a problem hiding this comment.
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.
lib/model/autocomplete.dart
Outdated
@@ -419,42 +421,29 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery, | |||
required super.query, | |||
required this.narrow, | |||
required this.sortedUsers, | |||
required this.localizations, |
There was a problem hiding this comment.
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
The icon is taken from the Zulip Mobile Figma design file: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=32-10328&t=Us73OrqbhvtHtSoY-4
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
826a5a2
to
449f326
Compare
Thanks @gnprice. Pushed a new revision. |
Thanks again for all your work on this! All looks great now — merging. |
Screenshot
Screen recording
Wildcard.mention.mp4
Fixes: #234