@@ -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,33 @@ 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
627+ // TODO(#1776) Follow new "Who can mention this group" setting instead
628+ .where ((userGroup) => ! userGroup.isSystemGroup)
629+ .toList ()
630+ ..sort (_userGroupComparator (store: store));
631+ }
632+
633+ static int Function (UserGroup , UserGroup ) _userGroupComparator ({
634+ required PerAccountStore store,
635+ }) {
636+ // See also [MentionAutocompleteQuery._rankUserGroupResult];
637+ // that ranking takes precedence over this.
638+
639+ return (userGroupA, userGroupB) =>
640+ compareGroupsByAlphabeticalOrder (userGroupA, userGroupB, store: store);
641+ }
642+
643+ static int compareGroupsByAlphabeticalOrder (UserGroup userGroupA, UserGroup userGroupB,
644+ {required PerAccountStore store}) {
645+ final groupAName = store.autocompleteViewManager.autocompleteDataCache
646+ .lowercaseNameForUserGroup (userGroupA);
647+ final groupBName = store.autocompleteViewManager.autocompleteDataCache
648+ .lowercaseNameForUserGroup (userGroupB);
649+ return groupAName.compareTo (groupBName); // TODO(i18n): add locale-aware sorting
650+ }
651+
614652 void computeWildcardMentionResults ({
615653 required List <MentionAutocompleteResult > results,
616654 required bool isComposingChannelMessage,
@@ -654,6 +692,11 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
654692 return null ;
655693 }
656694
695+ if (await filterCandidates (filter: _testUserGroup,
696+ candidates: sortedUserGroups, results: unsorted)) {
697+ return null ;
698+ }
699+
657700 return bucketSort (unsorted,
658701 (r) => r.rank, numBuckets: MentionAutocompleteQuery ._numResultRanks);
659702 }
@@ -662,6 +705,10 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
662705 return query.testUser (user, store);
663706 }
664707
708+ MentionAutocompleteResult ? _testUserGroup (MentionAutocompleteQuery query, UserGroup userGroup) {
709+ return query.testUserGroup (userGroup, store);
710+ }
711+
665712 @override
666713 void dispose () {
667714 store.autocompleteViewManager.unregisterMentionAutocomplete (this );
@@ -728,7 +775,8 @@ abstract class AutocompleteQuery {
728775 }
729776}
730777
731- /// The match quality of a [User.fullName] to a mention autocomplete query.
778+ /// The match quality of a [User.fullName] or [UserGroup.name]
779+ /// to a mention autocomplete query.
732780///
733781/// All matches are case-insensitive.
734782enum NameMatchQuality {
@@ -830,10 +878,24 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
830878 return lowercaseEmail.startsWith (_lowercase);
831879 }
832880
881+ MentionAutocompleteResult ? testUserGroup (UserGroup userGroup, PerAccountStore store) {
882+ final cache = store.autocompleteViewManager.autocompleteDataCache;
883+
884+ final nameMatchQuality = _matchName (
885+ normalizedName: cache.lowercaseNameForUserGroup (userGroup),
886+ normalizedNameWords: cache.lowercaseNameWordsForUserGroup (userGroup));
887+
888+ if (nameMatchQuality == null ) return null ;
889+
890+ return UserGroupMentionAutocompleteResult (
891+ groupId: userGroup.id,
892+ rank: _rankUserGroupResult (userGroup, nameMatchQuality: nameMatchQuality));
893+ }
894+
833895 /// A measure of a wildcard result's quality in the context of the query,
834896 /// from 0 (best) to one less than [_numResultRanks] .
835897 ///
836- /// See also [_rankUserResult] .
898+ /// See also [_rankUserResult] and [_rankUserGroupResult] .
837899 static const _rankWildcardResult = 0 ;
838900
839901 /// A measure of a user result's quality in the context of the query,
@@ -842,7 +904,7 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
842904 /// When [nameMatchQuality] is non-null (the name matches),
843905 /// callers should skip computing [matchesEmail] and pass null for that.
844906 ///
845- /// See also [_rankWildcardResult] .
907+ /// See also [_rankWildcardResult] and [_rankUserGroupResult] .
846908 static int _rankUserResult (User user, {
847909 required NameMatchQuality ? nameMatchQuality,
848910 required bool ? matchesEmail,
@@ -856,12 +918,26 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
856918 };
857919 }
858920 assert (matchesEmail == true );
859- return 4 ;
921+ return 7 ;
922+ }
923+
924+ /// A measure of a user-group result's quality in the context of the query,
925+ /// from 0 (best) to one less than [_numResultRanks] .
926+ ///
927+ /// See also [_rankWildcardResult] and [_rankUserResult] .
928+ static int _rankUserGroupResult (UserGroup userGroup, {
929+ required NameMatchQuality nameMatchQuality,
930+ }) {
931+ return switch (nameMatchQuality) {
932+ NameMatchQuality .exact => 4 ,
933+ NameMatchQuality .totalPrefix => 5 ,
934+ NameMatchQuality .wordPrefixes => 6 ,
935+ };
860936 }
861937
862938 /// The number of possible values returned by
863- /// [_rankWildcardResult] and [_rankUserResult] .
864- static const _numResultRanks = 5 ;
939+ /// [_rankWildcardResult] , [_rankUserResult] , and [_rankUserGroupResult] . .
940+ static const _numResultRanks = 8 ;
865941
866942 @override
867943 String toString () {
@@ -916,11 +992,30 @@ class AutocompleteDataCache {
916992 return _normalizedEmailsByUser[user.userId] ?? = user.deliveryEmail? .toLowerCase ();
917993 }
918994
995+ final Map <int , String > _lowercaseNamesByUserGroup = {};
996+
997+ /// The normalized `name` of [userGroup] .
998+ String lowercaseNameForUserGroup (UserGroup userGroup) {
999+ return _lowercaseNamesByUserGroup[userGroup.id] ?? = userGroup.name.toLowerCase ();
1000+ }
1001+
1002+ final Map <int , List <String >> _lowercaseNameWordsByUserGroup = {};
1003+
1004+ List <String > lowercaseNameWordsForUserGroup (UserGroup userGroup) {
1005+ return _lowercaseNameWordsByUserGroup[userGroup.id]
1006+ ?? = lowercaseNameForUserGroup (userGroup).split (' ' );
1007+ }
1008+
9191009 void invalidateUser (int userId) {
9201010 _normalizedNamesByUser.remove (userId);
9211011 _normalizedNameWordsByUser.remove (userId);
9221012 _normalizedEmailsByUser.remove (userId);
9231013 }
1014+
1015+ void invalidateUserGroup (int id) {
1016+ _lowercaseNamesByUserGroup.remove (id);
1017+ _lowercaseNameWordsByUserGroup.remove (id);
1018+ }
9241019}
9251020
9261021/// A result the user chose, or might choose, from an autocomplete interaction.
@@ -970,7 +1065,7 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
9701065 // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
9711066 //
9721067 // Behavior we have that web doesn't and might like to follow:
973- // - A "word-prefixes" match quality on user names:
1068+ // - A "word-prefixes" match quality on user and user-group names:
9741069 // see [NameMatchQuality.wordPrefixes], which we rank on.
9751070 //
9761071 // Behavior web has that seems undesired, which we don't plan to follow:
@@ -1015,7 +1110,15 @@ class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
10151110 final int rank;
10161111}
10171112
1018- // TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1113+ /// An autocomplete result for an @-mention of a user group.
1114+ class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
1115+ UserGroupMentionAutocompleteResult ({required this .groupId, required this .rank});
1116+
1117+ final int groupId;
1118+
1119+ @override
1120+ final int rank;
1121+ }
10191122
10201123/// An autocomplete interaction for choosing a topic for a message.
10211124class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
0 commit comments