@@ -249,6 +249,14 @@ class AutocompleteViewManager {
249249 autocompleteDataCache.invalidateUser (event.userId);
250250 }
251251
252+ void handleUserGroupRemoveEvent (UserGroupRemoveEvent event) {
253+ autocompleteDataCache.invalidateUserGroup (event.groupId);
254+ }
255+
256+ void handleUserGroupUpdateEvent (UserGroupUpdateEvent event) {
257+ autocompleteDataCache.invalidateUserGroup (event.groupId);
258+ }
259+
252260 /// Called when the app is reassembled during debugging, e.g. for hot reload.
253261 ///
254262 /// Calls [AutocompleteView.reassemble] for all that are registered.
@@ -423,6 +431,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
423431 required this .localizations,
424432 required this .narrow,
425433 required this .sortedUsers,
434+ required this .sortedUserGroups,
426435 });
427436
428437 factory MentionAutocompleteView .init ({
@@ -437,13 +446,15 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
437446 localizations: localizations,
438447 narrow: narrow,
439448 sortedUsers: _usersByRelevance (store: store, narrow: narrow),
449+ sortedUserGroups: _userGroupsByRelevance (store: store),
440450 );
441451 store.autocompleteViewManager.registerMentionAutocomplete (view);
442452 return view;
443453 }
444454
445455 final Narrow narrow;
446456 final List <User > sortedUsers;
457+ final List <UserGroup > sortedUserGroups;
447458 final ZulipLocalizations localizations;
448459
449460 static List <User > _usersByRelevance ({
@@ -611,6 +622,30 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
611622 return userAName.compareTo (userBName); // TODO(i18n): add locale-aware sorting
612623 }
613624
625+ static List <UserGroup > _userGroupsByRelevance ({required PerAccountStore store}) {
626+ return store.activeGroups.where ((userGroup) => ! userGroup.isSystemGroup).toList ()
627+ ..sort (_userGroupComparator (store: store));
628+ }
629+
630+ static int Function (UserGroup , UserGroup ) _userGroupComparator ({
631+ required PerAccountStore store,
632+ }) {
633+ // See also [MentionAutocompleteQuery._rankUserGroupResult];
634+ // that ranking takes precedence over this.
635+
636+ return (userGroupA, userGroupB) =>
637+ compareGroupsByAlphabeticalOrder (userGroupA, userGroupB, store: store);
638+ }
639+
640+ static int compareGroupsByAlphabeticalOrder (UserGroup userGroupA, UserGroup userGroupB,
641+ {required PerAccountStore store}) {
642+ final groupAName = store.autocompleteViewManager.autocompleteDataCache
643+ .lowercaseNameForUserGroup (userGroupA);
644+ final groupBName = store.autocompleteViewManager.autocompleteDataCache
645+ .lowercaseNameForUserGroup (userGroupB);
646+ return groupAName.compareTo (groupBName); // TODO(i18n): add locale-aware sorting
647+ }
648+
614649 void computeWildcardMentionResults ({
615650 required List <MentionAutocompleteResult > results,
616651 required bool isComposingChannelMessage,
@@ -654,6 +689,11 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
654689 return null ;
655690 }
656691
692+ if (await filterCandidates (filter: _testUserGroup,
693+ candidates: sortedUserGroups, results: unsorted)) {
694+ return null ;
695+ }
696+
657697 return bucketSort (unsorted,
658698 (r) => r.rank, numBuckets: MentionAutocompleteQuery ._numResultRanks);
659699 }
@@ -662,6 +702,10 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
662702 return query.testUser (user, store);
663703 }
664704
705+ MentionAutocompleteResult ? _testUserGroup (MentionAutocompleteQuery query, UserGroup userGroup) {
706+ return query.testUserGroup (userGroup, store);
707+ }
708+
665709 @override
666710 void dispose () {
667711 store.autocompleteViewManager.unregisterMentionAutocomplete (this );
@@ -728,7 +772,8 @@ abstract class AutocompleteQuery {
728772 }
729773}
730774
731- /// The match quality of a [User.fullName] to a mention autocomplete query.
775+ /// The match quality of a [User.fullName] or [UserGroup.name]
776+ /// to a mention autocomplete query.
732777///
733778/// All matches are case-insensitive.
734779enum NameMatchQuality {
@@ -836,10 +881,24 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
836881 return lowercaseEmail.startsWith (_lowercase);
837882 }
838883
884+ MentionAutocompleteResult ? testUserGroup (UserGroup userGroup, PerAccountStore store) {
885+ final cache = store.autocompleteViewManager.autocompleteDataCache;
886+
887+ final nameMatchQuality = _matchName (
888+ normalizedName: cache.lowercaseNameForUserGroup (userGroup),
889+ normalizedNameWords: cache.lowercaseNameWordsForUserGroup (userGroup));
890+
891+ if (nameMatchQuality == null ) return null ;
892+
893+ return UserGroupMentionAutocompleteResult (
894+ id: userGroup.id,
895+ rank: _rankUserGroupResult (userGroup, nameMatchQuality: nameMatchQuality));
896+ }
897+
839898 /// A measure of a wildcard result's quality in the context of the query,
840899 /// from 0 (best) to one less than [_numResultRanks] .
841900 ///
842- /// See also [_rankUserResult] .
901+ /// See also [_rankUserResult] and [_rankUserGroupResult] .
843902 static const _rankWildcardResult = 0 ;
844903
845904 /// A measure of a user result's quality in the context of the query,
@@ -848,7 +907,7 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
848907 /// When [nameMatchQuality] is non-null (the name matches),
849908 /// callers should skip computing [matchesEmail] and pass null for that.
850909 ///
851- /// See also [_rankWildcardResult] .
910+ /// See also [_rankWildcardResult] and [_rankUserGroupResult] .
852911 static int _rankUserResult (User user, {
853912 required NameMatchQuality ? nameMatchQuality,
854913 required bool ? matchesEmail,
@@ -862,12 +921,26 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
862921 };
863922 }
864923 assert (matchesEmail == true );
865- return 4 ;
924+ return 7 ;
925+ }
926+
927+ /// A measure of a user-group result's quality in the context of the query,
928+ /// from 0 (best) to one less than [_numResultRanks] .
929+ ///
930+ /// See also [_rankWildcardResult] and [_rankUserResult] .
931+ static int _rankUserGroupResult (UserGroup userGroup, {
932+ required NameMatchQuality nameMatchQuality,
933+ }) {
934+ return switch (nameMatchQuality) {
935+ NameMatchQuality .exact => 4 ,
936+ NameMatchQuality .totalPrefix => 5 ,
937+ NameMatchQuality .wordPrefixes => 6 ,
938+ };
866939 }
867940
868941 /// The number of possible values returned by
869942 /// [_rankWildcardResult] and [_rankUserResult] .
870- static const _numResultRanks = 5 ;
943+ static const _numResultRanks = 8 ;
871944
872945 @override
873946 String toString () {
@@ -922,11 +995,30 @@ class AutocompleteDataCache {
922995 return _normalizedEmailsByUser[user.userId] ?? = user.deliveryEmail? .toLowerCase ();
923996 }
924997
998+ final Map <int , String > _lowercaseNamesByUserGroup = {};
999+
1000+ /// The normalized `name` of [userGroup] .
1001+ String lowercaseNameForUserGroup (UserGroup userGroup) {
1002+ return _lowercaseNamesByUserGroup[userGroup.id] ?? = userGroup.name.toLowerCase ();
1003+ }
1004+
1005+ final Map <int , List <String >> _lowercaseNameWordsByUserGroup = {};
1006+
1007+ List <String > lowercaseNameWordsForUserGroup (UserGroup userGroup) {
1008+ return _lowercaseNameWordsByUserGroup[userGroup.id]
1009+ ?? = lowercaseNameForUserGroup (userGroup).split (' ' );
1010+ }
1011+
9251012 void invalidateUser (int userId) {
9261013 _normalizedNamesByUser.remove (userId);
9271014 _normalizedNameWordsByUser.remove (userId);
9281015 _normalizedEmailsByUser.remove (userId);
9291016 }
1017+
1018+ void invalidateUserGroup (int id) {
1019+ _lowercaseNamesByUserGroup.remove (id);
1020+ _lowercaseNameWordsByUserGroup.remove (id);
1021+ }
9301022}
9311023
9321024/// A result the user chose, or might choose, from an autocomplete interaction.
@@ -976,7 +1068,7 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
9761068 // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
9771069 //
9781070 // Behavior we have that web doesn't and might like to follow:
979- // - A "word-prefixes" match quality on user names:
1071+ // - A "word-prefixes" match quality on user and user-group names:
9801072 // see [NameMatchQuality.wordPrefixes], which we rank on.
9811073 //
9821074 // Behavior web has that seems undesired, which we don't plan to follow:
@@ -1021,7 +1113,15 @@ class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
10211113 final int rank;
10221114}
10231115
1024- // TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1116+ /// An autocomplete result for an @-mention of a user group.
1117+ class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1118+ UserGroupMentionAutocompleteResult ({required this .id, required this .rank});
1119+
1120+ final int id;
1121+
1122+ @override
1123+ final int rank;
1124+ }
10251125
10261126/// An autocomplete interaction for choosing a topic for a message.
10271127class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
0 commit comments