@@ -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 {
@@ -830,10 +875,24 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
830875 return lowercaseEmail.startsWith (_lowercase);
831876 }
832877
878+ MentionAutocompleteResult ? testUserGroup (UserGroup userGroup, PerAccountStore store) {
879+ final cache = store.autocompleteViewManager.autocompleteDataCache;
880+
881+ final nameMatchQuality = _matchName (
882+ normalizedName: cache.lowercaseNameForUserGroup (userGroup),
883+ normalizedNameWords: cache.lowercaseNameWordsForUserGroup (userGroup));
884+
885+ if (nameMatchQuality == null ) return null ;
886+
887+ return UserGroupMentionAutocompleteResult (
888+ groupId: userGroup.id,
889+ rank: _rankUserGroupResult (userGroup, nameMatchQuality: nameMatchQuality));
890+ }
891+
833892 /// A measure of a wildcard result's quality in the context of the query,
834893 /// from 0 (best) to one less than [_numResultRanks] .
835894 ///
836- /// See also [_rankUserResult] .
895+ /// See also [_rankUserResult] and [_rankUserGroupResult] .
837896 static const _rankWildcardResult = 0 ;
838897
839898 /// A measure of a user result's quality in the context of the query,
@@ -842,7 +901,7 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
842901 /// When [nameMatchQuality] is non-null (the name matches),
843902 /// callers should skip computing [matchesEmail] and pass null for that.
844903 ///
845- /// See also [_rankWildcardResult] .
904+ /// See also [_rankWildcardResult] and [_rankUserGroupResult] .
846905 static int _rankUserResult (User user, {
847906 required NameMatchQuality ? nameMatchQuality,
848907 required bool ? matchesEmail,
@@ -856,12 +915,26 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
856915 };
857916 }
858917 assert (matchesEmail == true );
859- return 4 ;
918+ return 7 ;
919+ }
920+
921+ /// A measure of a user-group result's quality in the context of the query,
922+ /// from 0 (best) to one less than [_numResultRanks] .
923+ ///
924+ /// See also [_rankWildcardResult] and [_rankUserResult] .
925+ static int _rankUserGroupResult (UserGroup userGroup, {
926+ required NameMatchQuality nameMatchQuality,
927+ }) {
928+ return switch (nameMatchQuality) {
929+ NameMatchQuality .exact => 4 ,
930+ NameMatchQuality .totalPrefix => 5 ,
931+ NameMatchQuality .wordPrefixes => 6 ,
932+ };
860933 }
861934
862935 /// The number of possible values returned by
863- /// [_rankWildcardResult] and [_rankUserResult] .
864- static const _numResultRanks = 5 ;
936+ /// [_rankWildcardResult] , [_rankUserResult] , and [_rankUserGroupResult] . .
937+ static const _numResultRanks = 8 ;
865938
866939 @override
867940 String toString () {
@@ -916,11 +989,30 @@ class AutocompleteDataCache {
916989 return _normalizedEmailsByUser[user.userId] ?? = user.deliveryEmail? .toLowerCase ();
917990 }
918991
992+ final Map <int , String > _lowercaseNamesByUserGroup = {};
993+
994+ /// The normalized `name` of [userGroup] .
995+ String lowercaseNameForUserGroup (UserGroup userGroup) {
996+ return _lowercaseNamesByUserGroup[userGroup.id] ?? = userGroup.name.toLowerCase ();
997+ }
998+
999+ final Map <int , List <String >> _lowercaseNameWordsByUserGroup = {};
1000+
1001+ List <String > lowercaseNameWordsForUserGroup (UserGroup userGroup) {
1002+ return _lowercaseNameWordsByUserGroup[userGroup.id]
1003+ ?? = lowercaseNameForUserGroup (userGroup).split (' ' );
1004+ }
1005+
9191006 void invalidateUser (int userId) {
9201007 _normalizedNamesByUser.remove (userId);
9211008 _normalizedNameWordsByUser.remove (userId);
9221009 _normalizedEmailsByUser.remove (userId);
9231010 }
1011+
1012+ void invalidateUserGroup (int id) {
1013+ _lowercaseNamesByUserGroup.remove (id);
1014+ _lowercaseNameWordsByUserGroup.remove (id);
1015+ }
9241016}
9251017
9261018/// A result the user chose, or might choose, from an autocomplete interaction.
@@ -970,7 +1062,7 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
9701062 // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
9711063 //
9721064 // Behavior we have that web doesn't and might like to follow:
973- // - A "word-prefixes" match quality on user names:
1065+ // - A "word-prefixes" match quality on user and user-group names:
9741066 // see [NameMatchQuality.wordPrefixes], which we rank on.
9751067 //
9761068 // Behavior web has that seems undesired, which we don't plan to follow:
@@ -1015,7 +1107,15 @@ class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
10151107 final int rank;
10161108}
10171109
1018- // TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1110+ /// An autocomplete result for an @-mention of a user group.
1111+ class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1112+ UserGroupMentionAutocompleteResult ({required this .groupId, required this .rank});
1113+
1114+ final int groupId;
1115+
1116+ @override
1117+ final int rank;
1118+ }
10191119
10201120/// An autocomplete interaction for choosing a topic for a message.
10211121class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
0 commit comments