-
Notifications
You must be signed in to change notification settings - Fork 238
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
base: main
Are you sure you want to change the base?
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
08789d6
to
ee45cde
Compare
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! Comments below.
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!
I think it's fine to leave those commits in. Checking that the new fields are added in the proper order isn't super trivial; we might as well keep the work we did for that. 🙂
lib/api/model/initial_snapshot.dart
Outdated
enum RealmWildcardMentionPolicy { | ||
anyone(apiValue: 1), | ||
members(apiValue: 2), | ||
fullMembers(apiValue: 3), | ||
orgAdmins(apiValue: 5), | ||
nobody(apiValue: 6), | ||
moderators(apiValue: 7); |
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.
How about:
anyone
>everyone
orgAdmins
>administrators
to be more similar to the "permission levels" list here: https://zulip.com/api/roles-and-permissions#permission-levels
lib/model/store.dart
Outdated
@@ -375,6 +377,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |||
Uri? tryResolveUrl(String reference) => _tryResolveUrl(realmUrl, reference); | |||
|
|||
String get zulipVersion => account.zulipVersion; | |||
final RealmWildcardMentionPolicy realmWildcardMentionPolicy; |
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 needs a // TODO(#668): update this realm setting
, like the setting below it.
test/widgets/autocomplete_test.dart
Outdated
case ChannelNarrow(:final streamId) || TopicNarrow(:final streamId): | ||
final stream = eg.stream(streamId: streamId); | ||
message = eg.streamMessage(stream: stream); | ||
await store.addStream(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.
If the narrow is a topic narrow, we should create message
with the narrow's topic, right?
@@ -33,7 +33,9 @@ import 'test_app.dart'; | |||
/// before the end of the test. | |||
Future<Finder> setupToComposeInput(WidgetTester tester, { | |||
List<User> users = const [], | |||
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.
autocomplete_test: Make `setupToComposeInput` accept `narrow` param
commit-message nit: autocomplete test:
for the prefix, like in other commits
test/widgets/autocomplete_test.dart
Outdated
final stream = eg.stream(streamId: streamId); | ||
message = eg.streamMessage(stream: stream); | ||
await store.addStream(stream); | ||
default: throw AssertionError(); // never happens because of the assert above |
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.
How about throw StateError('unexpected narrow type')
, without a comment? That way, even if the assert
line gets changed for some reason in the future, this line stays coherent. (Here and in another switch
below.)
lib/widgets/autocomplete.dart
Outdated
Wildcard.all => isDmNarrow | ||
? localizations.notifyRecipients | ||
: isChannelWildcardAvailable | ||
? localizations.notifyChannel | ||
: localizations.notifyStream, | ||
Wildcard.everyone => isDmNarrow | ||
? localizations.notifyRecipients | ||
: isChannelWildcardAvailable | ||
? localizations.notifyChannel | ||
: localizations.notifyStream, |
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.
Wildcard.all => isDmNarrow | |
? localizations.notifyRecipients | |
: isChannelWildcardAvailable | |
? localizations.notifyChannel | |
: localizations.notifyStream, | |
Wildcard.everyone => isDmNarrow | |
? localizations.notifyRecipients | |
: isChannelWildcardAvailable | |
? localizations.notifyChannel | |
: localizations.notifyStream, | |
Wildcard.all || Wildcard.everyone => isDmNarrow | |
? localizations.notifyRecipients | |
: isChannelWildcardAvailable | |
? localizations.notifyChannel | |
: localizations.notifyStream, |
test/model/autocomplete_test.dart
Outdated
@@ -811,20 +887,27 @@ void main() { | |||
RecentDmConversation(userIds: [1, 2], maxMessageId: 100), | |||
]); | |||
|
|||
// Check the ranking of the full list of users. | |||
// Check the ranking of the full list of user mentions. |
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.
// Check the ranking of the full list of user mentions. | |
// Check the ranking of the full list of mentions. |
(because wildcard mentions aren't user mentions, right?)
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. Channel and/or topic 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.
nit: How about:
// 1. Channel and/or topic wildcards. | |
// 1. Wildcards. |
There aren't other kinds of wildcard results that would appear in the list. But if there were, I think they'd probably go in this position too.
lib/model/autocomplete.dart
Outdated
@@ -615,6 +596,59 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery, | |||
return userAName.compareTo(userBName); // TODO(i18n): add locale-aware sorting | |||
} | |||
|
|||
List<WildcardMentionAutocompleteResult> get wildcardMentionResults { |
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 just checked the web app, and if the user wants a silent mention (they've typed "@_") then wildcard mentions are excluded. I guess that sounds right, since the wildcard options say "Notify channel" etc.; how about we follow web and do that?
test/model/autocomplete_test.dart
Outdated
@@ -438,7 +438,7 @@ void main() { | |||
}); | |||
}); | |||
|
|||
group('MentionAutocompleteView sorting users results', () { | |||
group('MentionAutocompleteView sorting user mention 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.
group('MentionAutocompleteView sorting user mention results', () { | |
group('MentionAutocompleteView sorting mention results', () { |
(since wildcard mentions aren't user mentions)
ee45cde
to
55c36c4
Compare
Thank you @chrisbobbe for the thorough review. Comments addressed, please have a look. |
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.
…` for a more logical order
The implementation logic is similar to the zulip-mobile implementation: https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/autocomplete/WildcardMentionItem.js Fixes: zulip#234
55c36c4
to
c21fe24
Compare
Screenshot
Screen recording
Wildcard.mention.mp4
Fixes: #234