Skip to content

Prep for user-group autocomplete #1745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 54 additions & 22 deletions lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../api/model/model.dart';
import '../api/route/channels.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../widgets/compose_box.dart';
import 'algorithms.dart';
import 'compose.dart';
import 'emoji.dart';
import 'narrow.dart';
Expand Down Expand Up @@ -610,11 +611,10 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
if (query.silent) return;

bool tryOption(WildcardMentionOption option) {
if (query.testWildcardOption(option, localizations: localizations)) {
results.add(WildcardMentionAutocompleteResult(wildcardOption: option));
return true;
}
return false;
final result = query.testWildcardOption(option, localizations: localizations);
if (result == null) return false;
results.add(result);
return true;
}

// Only one of the (all, everyone, channel, stream) channel wildcards are
Expand All @@ -637,23 +637,22 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,

@override
Future<List<MentionAutocompleteResult>?> computeResults() async {
final results = <MentionAutocompleteResult>[];
final unsorted = <MentionAutocompleteResult>[];
// Give priority to wildcard mentions.
computeWildcardMentionResults(results: results,
computeWildcardMentionResults(results: unsorted,
isComposingChannelMessage: narrow is ChannelNarrow || narrow is TopicNarrow);

if (await filterCandidates(filter: _testUser,
candidates: sortedUsers, results: results)) {
candidates: sortedUsers, results: unsorted)) {
return null;
}
return results;

return bucketSort(unsorted,
(r) => r.rank, numBuckets: MentionAutocompleteQuery._numResultRanks);
}

MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) {
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache, store)) {
return UserMentionAutocompleteResult(userId: user.userId);
}
return null;
return query.testUser(user, store.autocompleteViewManager.autocompleteDataCache, store);
}

@override
Expand Down Expand Up @@ -748,25 +747,47 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
store: store, localizations: localizations, narrow: narrow, query: this);
}

bool testWildcardOption(WildcardMentionOption wildcardOption, {
WildcardMentionAutocompleteResult? testWildcardOption(WildcardMentionOption wildcardOption, {
required ZulipLocalizations localizations}) {
// TODO(#237): match insensitively to diacritics
return wildcardOption.canonicalString.contains(_lowercase)
final matches = wildcardOption.canonicalString.contains(_lowercase)
|| wildcardOption.localizedCanonicalString(localizations).contains(_lowercase);
if (!matches) return null;
return WildcardMentionAutocompleteResult(
wildcardOption: wildcardOption, rank: _rankWildcardResult);
}

bool testUser(User user, AutocompleteDataCache cache, UserStore store) {
if (!user.isActive) return false;
if (store.isUserMuted(user.userId)) return false;
MentionAutocompleteResult? testUser(User user, AutocompleteDataCache cache, UserStore store) {
if (!user.isActive) return null;
if (store.isUserMuted(user.userId)) return null;

// TODO(#236) test email too, not just name
return _testName(user, cache);
if (!_testName(user, cache)) return null;

return UserMentionAutocompleteResult(
userId: user.userId, rank: _rankUserResult);
}

bool _testName(User user, AutocompleteDataCache cache) {
return _testContainsQueryWords(cache.nameWordsForUser(user));
}

/// A measure of a wildcard result's quality in the context of the query,
/// from 0 (best) to one less than [_numResultRanks].
///
/// See also [_rankUserResult].
static const _rankWildcardResult = 0;
Comment on lines +775 to +779
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is a verb phrase (well, it could be extra elliptical and mean "the rank for a wildcard result", but I want to read it as a verb phrase: "rank a wildcard result"), so it's a bit odd for it to be a constant value.

Maybe go ahead and make it a function that returns a constant? That'll better demonstrate the future structure you intend anyway.


/// A measure of a user result's quality in the context of the query,
/// from 0 (best) to one less than [_numResultRanks].
///
/// See also [_rankWildcardResult].
static const _rankUserResult = 1;

/// The number of possible values returned by
/// [_rankWildcardResult] and [_rankUserResult].
static const _numResultRanks = 2;

@override
String toString() {
return '${objectRuntimeType(this, 'MentionAutocompleteQuery')}(raw: $raw, silent: $silent})';
Expand Down Expand Up @@ -853,20 +874,31 @@ class EmojiAutocompleteResult extends ComposeAutocompleteResult {
/// This is abstract because there are several kinds of result
/// that can all be offered in the same @-mention autocomplete interaction:
/// a user, a wildcard, or a user group.
sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {}
sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
/// A measure of the result's quality in the context of the query.
///
/// Used internally by [MentionAutocompleteView] for ranking the results.
int get rank;
}

/// An autocomplete result for an @-mention of an individual user.
class UserMentionAutocompleteResult extends MentionAutocompleteResult {
UserMentionAutocompleteResult({required this.userId});
UserMentionAutocompleteResult({required this.userId, required this.rank});

final int userId;

@override
final int rank;
}

/// An autocomplete result for an @-mention of all the users in a conversation.
class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
WildcardMentionAutocompleteResult({required this.wildcardOption});
WildcardMentionAutocompleteResult({required this.wildcardOption, required this.rank});

final WildcardMentionOption wildcardOption;

@override
final int rank;
}

// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
Expand Down
4 changes: 3 additions & 1 deletion test/model/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,9 @@ void main() {
void doCheck(String rawQuery, User user, bool expected) {
final result = MentionAutocompleteQuery(rawQuery)
.testUser(user, AutocompleteDataCache(), store);
expected ? check(result).isTrue() : check(result).isFalse();
expected
? check(result).isA<UserMentionAutocompleteResult>()
: check(result).isNull();
}

test('user is always excluded when not active regardless of other criteria', () {
Expand Down