@@ -249,6 +249,14 @@ class AutocompleteViewManager {
249
249
autocompleteDataCache.invalidateUser (event.userId);
250
250
}
251
251
252
+ void handleUserGroupRemoveEvent (UserGroupRemoveEvent event) {
253
+ autocompleteDataCache.invalidateUserGroup (event.groupId);
254
+ }
255
+
256
+ void handleUserGroupUpdateEvent (UserGroupUpdateEvent event) {
257
+ autocompleteDataCache.invalidateUserGroup (event.groupId);
258
+ }
259
+
252
260
/// Called when the app is reassembled during debugging, e.g. for hot reload.
253
261
///
254
262
/// Calls [AutocompleteView.reassemble] for all that are registered.
@@ -423,6 +431,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
423
431
required this .localizations,
424
432
required this .narrow,
425
433
required this .sortedUsers,
434
+ required this .sortedUserGroups,
426
435
});
427
436
428
437
factory MentionAutocompleteView .init ({
@@ -437,13 +446,15 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
437
446
localizations: localizations,
438
447
narrow: narrow,
439
448
sortedUsers: _usersByRelevance (store: store, narrow: narrow),
449
+ sortedUserGroups: _userGroupsByRelevance (store: store),
440
450
);
441
451
store.autocompleteViewManager.registerMentionAutocomplete (view);
442
452
return view;
443
453
}
444
454
445
455
final Narrow narrow;
446
456
final List <User > sortedUsers;
457
+ final List <UserGroup > sortedUserGroups;
447
458
final ZulipLocalizations localizations;
448
459
449
460
static List <User > _usersByRelevance ({
@@ -611,6 +622,33 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
611
622
return userAName.compareTo (userBName); // TODO(i18n): add locale-aware sorting
612
623
}
613
624
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
+
614
652
void computeWildcardMentionResults ({
615
653
required List <MentionAutocompleteResult > results,
616
654
required bool isComposingChannelMessage,
@@ -654,6 +692,11 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
654
692
return null ;
655
693
}
656
694
695
+ if (await filterCandidates (filter: _testUserGroup,
696
+ candidates: sortedUserGroups, results: unsorted)) {
697
+ return null ;
698
+ }
699
+
657
700
return bucketSort (unsorted,
658
701
(r) => r.rank, numBuckets: MentionAutocompleteQuery ._numResultRanks);
659
702
}
@@ -662,6 +705,10 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
662
705
return query.testUser (user, store);
663
706
}
664
707
708
+ MentionAutocompleteResult ? _testUserGroup (MentionAutocompleteQuery query, UserGroup userGroup) {
709
+ return query.testUserGroup (userGroup, store);
710
+ }
711
+
665
712
@override
666
713
void dispose () {
667
714
store.autocompleteViewManager.unregisterMentionAutocomplete (this );
@@ -728,7 +775,8 @@ abstract class AutocompleteQuery {
728
775
}
729
776
}
730
777
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.
732
780
///
733
781
/// All matches are case-insensitive.
734
782
enum NameMatchQuality {
@@ -830,10 +878,24 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
830
878
return lowercaseEmail.startsWith (_lowercase);
831
879
}
832
880
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
+
833
895
/// A measure of a wildcard result's quality in the context of the query,
834
896
/// from 0 (best) to one less than [_numResultRanks] .
835
897
///
836
- /// See also [_rankUserResult] .
898
+ /// See also [_rankUserResult] and [_rankUserGroupResult] .
837
899
static const _rankWildcardResult = 0 ;
838
900
839
901
/// A measure of a user result's quality in the context of the query,
@@ -842,7 +904,7 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
842
904
/// When [nameMatchQuality] is non-null (the name matches),
843
905
/// callers should skip computing [matchesEmail] and pass null for that.
844
906
///
845
- /// See also [_rankWildcardResult] .
907
+ /// See also [_rankWildcardResult] and [_rankUserGroupResult] .
846
908
static int _rankUserResult (User user, {
847
909
required NameMatchQuality ? nameMatchQuality,
848
910
required bool ? matchesEmail,
@@ -856,12 +918,26 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
856
918
};
857
919
}
858
920
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
+ };
860
936
}
861
937
862
938
/// 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 ;
865
941
866
942
@override
867
943
String toString () {
@@ -916,11 +992,30 @@ class AutocompleteDataCache {
916
992
return _normalizedEmailsByUser[user.userId] ?? = user.deliveryEmail? .toLowerCase ();
917
993
}
918
994
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
+
919
1009
void invalidateUser (int userId) {
920
1010
_normalizedNamesByUser.remove (userId);
921
1011
_normalizedNameWordsByUser.remove (userId);
922
1012
_normalizedEmailsByUser.remove (userId);
923
1013
}
1014
+
1015
+ void invalidateUserGroup (int id) {
1016
+ _lowercaseNamesByUserGroup.remove (id);
1017
+ _lowercaseNameWordsByUserGroup.remove (id);
1018
+ }
924
1019
}
925
1020
926
1021
/// A result the user chose, or might choose, from an autocomplete interaction.
@@ -970,7 +1065,7 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
970
1065
// https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
971
1066
//
972
1067
// 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:
974
1069
// see [NameMatchQuality.wordPrefixes], which we rank on.
975
1070
//
976
1071
// Behavior web has that seems undesired, which we don't plan to follow:
@@ -1015,7 +1110,15 @@ class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
1015
1110
final int rank;
1016
1111
}
1017
1112
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
+ }
1019
1122
1020
1123
/// An autocomplete interaction for choosing a topic for a message.
1021
1124
class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
0 commit comments