Skip to content

Commit 1664a2d

Browse files
committed
autocomplete: Support @-wildcard in user-mention autocomplete
Fixes: #234
1 parent 8b73cd4 commit 1664a2d

File tree

6 files changed

+202
-53
lines changed

6 files changed

+202
-53
lines changed

assets/l10n/app_en.arb

+35
Original file line numberDiff line numberDiff line change
@@ -533,5 +533,40 @@
533533
"manyPeopleTyping": "Several people are typing…",
534534
"@manyPeopleTyping": {
535535
"description": "Text to display when there are multiple users typing."
536+
},
537+
"all": "all",
538+
"@all": {
539+
"description": "Text for \"@all\" wildcard mention."
540+
},
541+
"everyone": "everyone",
542+
"@everyone": {
543+
"description": "Text for \"@everyone\" wildcard mention."
544+
},
545+
"channel": "channel",
546+
"@channel": {
547+
"description": "Text for \"@channel\" wildcard mention."
548+
},
549+
"stream": "stream",
550+
"@stream": {
551+
"description": "Text for \"@stream\" wildcard mention."
552+
},
553+
"topic": "topic",
554+
"@topic": {
555+
"description": "Text for \"@topic\" wildcard mention."
556+
},
557+
"notifyChannel": "Notify {value, select, channel{channel} other{stream}}",
558+
"@notifyChannel": {
559+
"description": "Description for \"@all\", \"@everyone\", \"@channel\", and \"@stream\" wildcard mentions in a channel or topic narrow.",
560+
"placeholders": {
561+
"value": {"type": "String"}
562+
}
563+
},
564+
"notifyRecipients": "Notify recipients",
565+
"@notifyRecipients": {
566+
"description": "Description for \"@all\" and \"@everyone\" wildcard mentions in a DM narrow."
567+
},
568+
"notifyTopic": "Notify topic",
569+
"@notifyTopic": {
570+
"description": "Description for \"@topic\" wildcard mention in a channel or topic narrow."
536571
}
537572
}

lib/model/autocomplete.dart

+85-30
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class AutocompleteViewManager {
187187
/// * On reassemble, call [reassemble].
188188
/// * When the object will no longer be used, call [dispose] to free
189189
/// resources on the [PerAccountStore].
190-
abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult, CandidateT> extends ChangeNotifier {
190+
abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult> extends ChangeNotifier {
191191
AutocompleteView({required this.store});
192192

193193
final PerAccountStore store;
@@ -284,46 +284,33 @@ abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extend
284284
}
285285
}
286286

287-
class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery, MentionAutocompleteResult, User> {
287+
class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery, MentionAutocompleteResult> {
288288
MentionAutocompleteView._({
289289
required super.store,
290290
required this.narrow,
291+
required this.wildcards,
291292
required this.sortedUsers,
292293
});
293294

294295
factory MentionAutocompleteView.init({
295296
required PerAccountStore store,
296297
required Narrow narrow,
298+
required List<Wildcard> wildcards,
297299
}) {
298300
final view = MentionAutocompleteView._(
299301
store: store,
300302
narrow: narrow,
303+
wildcards: wildcards,
301304
sortedUsers: _usersByRelevance(store: store, narrow: narrow),
302305
);
303306
store.autocompleteViewManager.registerMentionAutocomplete(view);
304307
return view;
305308
}
306309

307310
final Narrow narrow;
311+
final List<Wildcard> wildcards;
308312
final List<User> sortedUsers;
309313

310-
@override
311-
Future<List<MentionAutocompleteResult>?> computeResults() async {
312-
final results = <MentionAutocompleteResult>[];
313-
if (await filterCandidates(filter: _testUser,
314-
candidates: sortedUsers, results: results)) {
315-
return null;
316-
}
317-
return results;
318-
}
319-
320-
MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) {
321-
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache)) {
322-
return UserMentionAutocompleteResult(userId: user.userId);
323-
}
324-
return null;
325-
}
326-
327314
static List<User> _usersByRelevance({
328315
required PerAccountStore store,
329316
required Narrow narrow,
@@ -377,8 +364,6 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
377364
required String? topic,
378365
required PerAccountStore store,
379366
}) {
380-
// TODO(#234): give preference to "all", "everyone" or "stream"
381-
382367
// TODO(#618): give preference to subscribed users first
383368

384369
if (streamId != null) {
@@ -483,6 +468,42 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
483468
return userAName.compareTo(userBName); // TODO(i18n): add locale-aware sorting
484469
}
485470

471+
bool _isChannelWildcardIncluded = false;
472+
473+
@override
474+
Future<List<MentionAutocompleteResult>?> computeResults() async {
475+
_isChannelWildcardIncluded = false;
476+
final results = <MentionAutocompleteResult>[];
477+
// give priority to wildcard mentions
478+
if (await filterCandidates(filter: _testWildcard,
479+
candidates: wildcards, results: results)) {
480+
return null;
481+
}
482+
if (await filterCandidates(filter: _testUser,
483+
candidates: sortedUsers, results: results)) {
484+
return null;
485+
}
486+
return results;
487+
}
488+
489+
MentionAutocompleteResult? _testWildcard(MentionAutocompleteQuery query, Wildcard wildcard) {
490+
if (query.testWildcard(wildcard)) {
491+
if (wildcard.type == WildcardType.channel) {
492+
if (_isChannelWildcardIncluded) return null;
493+
_isChannelWildcardIncluded = true;
494+
}
495+
return WildcardMentionAutocompleteResult(wildcardName: wildcard.name);
496+
}
497+
return null;
498+
}
499+
500+
MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) {
501+
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache)) {
502+
return UserMentionAutocompleteResult(userId: user.userId);
503+
}
504+
return null;
505+
}
506+
486507
@override
487508
void dispose() {
488509
store.autocompleteViewManager.unregisterMentionAutocomplete(this);
@@ -493,6 +514,37 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
493514
}
494515
}
495516

517+
class Wildcard {
518+
Wildcard({
519+
required this.name,
520+
required this.value,
521+
required this.fullDisplayName,
522+
required this.type,
523+
});
524+
525+
/// The name of the wildcard to be shown as part of [fullDisplayName] in autocomplete suggestions.
526+
///
527+
/// Ex: "channel", "stream", "topic", ...
528+
final String name;
529+
530+
/// The value to be put at the compose box after choosing an option from autocomplete.
531+
///
532+
/// Same as the [name], except for "stream" it is "channel" in FL >= 247 (server-9).
533+
final String value; // TODO(sever-9): remove, instead use [name]
534+
535+
/// The full name of the wildcard to be shown in autocomplete suggestions.
536+
///
537+
/// Ex: "all (Notify channel)" or "everyone (Notify recipients)".
538+
final String fullDisplayName;
539+
540+
final WildcardType type;
541+
}
542+
543+
enum WildcardType {
544+
channel,
545+
topic, // TODO(sever-8)
546+
}
547+
496548
abstract class AutocompleteQuery {
497549
AutocompleteQuery(this.raw)
498550
: _lowercaseWords = raw.toLowerCase().split(' ');
@@ -529,15 +581,14 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
529581
/// Whether the user wants a silent mention (@_query, vs. @query).
530582
final bool silent;
531583

532-
bool testUser(User user, AutocompleteDataCache cache) {
533-
// TODO(#236) test email too, not just name
584+
bool testWildcard(Wildcard wildcard) {
585+
return wildcard.name.contains(raw.toLowerCase());
586+
}
534587

588+
bool testUser(User user, AutocompleteDataCache cache) {
535589
if (!user.isActive) return false;
536590

537-
return _testName(user, cache);
538-
}
539-
540-
bool _testName(User user, AutocompleteDataCache cache) {
591+
// TODO(#236) test email too, not just name
541592
return _testContainsQueryWords(cache.nameWordsForUser(user));
542593
}
543594

@@ -585,11 +636,15 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
585636
final int userId;
586637
}
587638

588-
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
639+
class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
640+
WildcardMentionAutocompleteResult({required this.wildcardName});
641+
642+
final String wildcardName;
643+
}
589644

590-
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
645+
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
591646

592-
class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, TopicAutocompleteResult, String> {
647+
class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, TopicAutocompleteResult> {
593648
TopicAutocompleteView._({required super.store, required this.streamId});
594649

595650
factory TopicAutocompleteView.init({required PerAccountStore store, required int streamId}) {

lib/model/compose.dart

+7-4
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,21 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
101101
return resultBuffer.toString();
102102
}
103103

104-
/// An @-mention, like @**Chris Bobbe|13313**.
104+
/// An @user-mention, like @**Chris Bobbe|13313**.
105105
///
106106
/// To omit the user ID part ("|13313") whenever the name part is unambiguous,
107107
/// pass a Map of all users we know about. This means accepting a linear scan
108108
/// through all users; avoid it in performance-sensitive codepaths.
109-
String mention(User user, {bool silent = false, Map<int, User>? users}) {
109+
String userMention(User user, {bool silent = false, Map<int, User>? users}) {
110110
bool includeUserId = users == null
111111
|| users.values.where((u) => u.fullName == user.fullName).take(2).length == 2;
112112

113113
return '@${silent ? '_' : ''}**${user.fullName}${includeUserId ? '|${user.userId}' : ''}**';
114114
}
115115

116+
/// An @wildcard-mention, like @**channel**.
117+
String wildcardMention(String wildcard) => '@**$wildcard**';
118+
116119
/// https://spec.commonmark.org/0.30/#inline-link
117120
///
118121
/// The "link text" is made by enclosing [visibleText] in square brackets.
@@ -145,7 +148,7 @@ String quoteAndReplyPlaceholder(PerAccountStore store, {
145148
SendableNarrow.ofMessage(message, selfUserId: store.selfUserId),
146149
nearMessageId: message.id);
147150
// See note in [quoteAndReply] about asking `mention` to omit the |<id> part.
148-
return '${mention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
151+
return '${userMention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
149152
'*(loading message ${message.id})*\n'; // TODO(i18n) ?
150153
}
151154

@@ -169,6 +172,6 @@ String quoteAndReply(PerAccountStore store, {
169172
// Could ask `mention` to omit the |<id> part unless the mention is ambiguous…
170173
// but that would mean a linear scan through all users, and the extra noise
171174
// won't much matter with the already probably-long message link in there too.
172-
return '${mention(sender!, silent: true)} ${inlineLink('said', url)}:\n' // TODO(i18n) ?
175+
return '${userMention(sender!, silent: true)} ${inlineLink('said', url)}:\n' // TODO(i18n) ?
173176
'${wrapWithBacktickFence(content: rawContent, infoString: 'quote')}';
174177
}

0 commit comments

Comments
 (0)