@@ -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 );
@@ -727,7 +771,8 @@ abstract class AutocompleteQuery {
727771 }
728772}
729773
730- /// The match quality of a [User.fullName] to a mention autocomplete query.
774+ /// The match quality of a [User.fullName] or [UserGroup.name]
775+ /// to a mention autocomplete query.
731776///
732777/// All matches are case-insensitive.
733778enum NameMatchQuality {
@@ -895,16 +940,30 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
895940 return EmailMatchQuality .localPartExactAndDomainPrefix;
896941 }
897942
943+ MentionAutocompleteResult ? testUserGroup (UserGroup userGroup, PerAccountStore store) {
944+ final cache = store.autocompleteViewManager.autocompleteDataCache;
945+
946+ final nameMatchQuality = _matchName (
947+ lowercaseName: cache.lowercaseNameForUserGroup (userGroup),
948+ lowercaseNameWords: cache.lowercaseNameWordsForUserGroup (userGroup));
949+
950+ if (nameMatchQuality == null ) return null ;
951+
952+ return UserGroupMentionAutocompleteResult (
953+ id: userGroup.id,
954+ rank: _rankUserGroupResult (userGroup, nameMatchQuality: nameMatchQuality));
955+ }
956+
898957 /// A measure of a wildcard result's quality in the context of the query,
899958 /// from 0 (best) to one less than [_numResultRanks] .
900959 ///
901- /// See also [_rankUserResult] .
960+ /// See also [_rankUserResult] and [_rankUserGroupResult] .
902961 static const _rankWildcardResult = 0 ;
903962
904963 /// A measure of a user result's quality in the context of the query,
905964 /// from 0 (best) to one less than [_numResultRanks] .
906965 ///
907- /// See also [_rankWildcardResult] .
966+ /// See also [_rankWildcardResult] and [_rankUserGroupResult] .
908967 static int _rankUserResult (User user, {
909968 required NameMatchQuality ? nameMatchQuality,
910969 required EmailMatchQuality ? emailMatchQuality,
@@ -918,15 +977,29 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
918977 }
919978 assert (emailMatchQuality != null );
920979 return switch (emailMatchQuality! ) {
921- EmailMatchQuality .localPartExactAndDomainPrefix => 4 ,
922- EmailMatchQuality .localPartExact => 5 ,
923- EmailMatchQuality .prefix => 6 ,
980+ EmailMatchQuality .localPartExactAndDomainPrefix => 7 ,
981+ EmailMatchQuality .localPartExact => 8 ,
982+ EmailMatchQuality .prefix => 9 ,
983+ };
984+ }
985+
986+ /// A measure of a user-group result's quality in the context of the query,
987+ /// from 0 (best) to one less than [_numResultRanks] .
988+ ///
989+ /// See also [_rankWildcardResult] and [_rankUserResult] .
990+ static int _rankUserGroupResult (UserGroup userGroup, {
991+ required NameMatchQuality nameMatchQuality,
992+ }) {
993+ return switch (nameMatchQuality) {
994+ NameMatchQuality .exact => 4 ,
995+ NameMatchQuality .totalPrefix => 5 ,
996+ NameMatchQuality .wordPrefixes => 6 ,
924997 };
925998 }
926999
9271000 /// The number of possible values returned by
9281001 /// [_rankWildcardResult] and [_rankUserResult] .
929- static const _numResultRanks = 7 ;
1002+ static const _numResultRanks = 10 ;
9301003
9311004 @override
9321005 String toString () {
@@ -1007,12 +1080,31 @@ class AutocompleteDataCache {
10071080 return (split[0 ], split[1 ]);
10081081 }
10091082
1083+ final Map <int , String > _lowercaseNamesByUserGroup = {};
1084+
1085+ /// The lowercase `name` of [userGroup] .
1086+ String lowercaseNameForUserGroup (UserGroup userGroup) {
1087+ return _lowercaseNamesByUserGroup[userGroup.id] ?? = userGroup.name.toLowerCase ();
1088+ }
1089+
1090+ final Map <int , List <String >> _lowercaseNameWordsByUserGroup = {};
1091+
1092+ List <String > lowercaseNameWordsForUserGroup (UserGroup userGroup) {
1093+ return _lowercaseNameWordsByUserGroup[userGroup.id]
1094+ ?? = lowercaseNameForUserGroup (userGroup).split (' ' );
1095+ }
1096+
10101097 void invalidateUser (int userId) {
10111098 _lowercaseNamesByUser.remove (userId);
10121099 _lowercaseNameWordsByUser.remove (userId);
10131100 _lowercaseEmailsByUser.remove (userId);
10141101 _lowercaseEmailPartsByUser.remove (userId);
10151102 }
1103+
1104+ void invalidateUserGroup (int id) {
1105+ _lowercaseNamesByUserGroup.remove (id);
1106+ _lowercaseNameWordsByUserGroup.remove (id);
1107+ }
10161108}
10171109
10181110/// A result the user chose, or might choose, from an autocomplete interaction.
@@ -1062,7 +1154,7 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
10621154 // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
10631155 //
10641156 // Behavior we have that web doesn't and might like to follow:
1065- // - A "word-prefixes" match quality on user names:
1157+ // - A "word-prefixes" match quality on user and user-group names:
10661158 // see [NameMatchQuality.wordPrefixes], which we rank on.
10671159 // - Two email match qualities when the query matches the "local part";
10681160 // see [EmailMatchQuality.localPartExactAndDomainPrefix] and
@@ -1110,7 +1202,15 @@ class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
11101202 final int rank;
11111203}
11121204
1113- // TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1205+ /// An autocomplete result for an @-mention of a user group.
1206+ class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1207+ UserGroupMentionAutocompleteResult ({required this .id, required this .rank});
1208+
1209+ final int id;
1210+
1211+ @override
1212+ final int rank;
1213+ }
11141214
11151215/// An autocomplete interaction for choosing a topic for a message.
11161216class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
0 commit comments