Skip to content

Commit ee45cde

Browse files
committed
autocomplete: Support @-wildcard in user-mention autocomplete
The implementation logic is similar to the zulip-mobile implementation: https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/autocomplete/WildcardMentionItem.js Fixes: zulip#234
1 parent bdef21c commit ee45cde

15 files changed

+446
-54
lines changed

assets/l10n/app_ar.arb

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
2+
"notifyChannel": "إخطار القناة",
3+
"notifyStream": "إخطار الدفق",
4+
"notifyRecipients": "إخطار المستلمين",
5+
"notifyTopic": "إخطار الموضوع"
36
}

assets/l10n/app_en.arb

+16
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,22 @@
641641
"@manyPeopleTyping": {
642642
"description": "Text to display when there are multiple users typing."
643643
},
644+
"notifyChannel": "Notify channel",
645+
"@notifyChannel": {
646+
"description": "Description for \"@all\", \"@everyone\", \"@channel\", and \"@stream\" wildcard mentions in a channel or topic narrow."
647+
},
648+
"notifyStream": "Notify stream",
649+
"@notifyStream": {
650+
"description": "Description for \"@all\", \"@everyone\", and \"@stream\" wildcard mentions in a stream or topic narrow."
651+
},
652+
"notifyRecipients": "Notify recipients",
653+
"@notifyRecipients": {
654+
"description": "Description for \"@all\" and \"@everyone\" wildcard mentions in a DM narrow."
655+
},
656+
"notifyTopic": "Notify topic",
657+
"@notifyTopic": {
658+
"description": "Description for \"@topic\" wildcard mention in a channel or topic narrow."
659+
},
644660
"messageIsEditedLabel": "EDITED",
645661
"@messageIsEditedLabel": {
646662
"description": "Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"

lib/generated/l10n/zulip_localizations.dart

+24
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,30 @@ abstract class ZulipLocalizations {
955955
/// **'Several people are typing…'**
956956
String get manyPeopleTyping;
957957

958+
/// Description for "@all", "@everyone", "@channel", and "@stream" wildcard mentions in a channel or topic narrow.
959+
///
960+
/// In en, this message translates to:
961+
/// **'Notify channel'**
962+
String get notifyChannel;
963+
964+
/// Description for "@all", "@everyone", and "@stream" wildcard mentions in a stream or topic narrow.
965+
///
966+
/// In en, this message translates to:
967+
/// **'Notify stream'**
968+
String get notifyStream;
969+
970+
/// Description for "@all" and "@everyone" wildcard mentions in a DM narrow.
971+
///
972+
/// In en, this message translates to:
973+
/// **'Notify recipients'**
974+
String get notifyRecipients;
975+
976+
/// Description for "@topic" wildcard mention in a channel or topic narrow.
977+
///
978+
/// In en, this message translates to:
979+
/// **'Notify topic'**
980+
String get notifyTopic;
981+
958982
/// Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)
959983
///
960984
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Several people are typing…';
510510

511+
@override
512+
String get notifyChannel => 'إخطار القناة';
513+
514+
@override
515+
String get notifyStream => 'إخطار الدفق';
516+
517+
@override
518+
String get notifyRecipients => 'إخطار المستلمين';
519+
520+
@override
521+
String get notifyTopic => 'إخطار الموضوع';
522+
511523
@override
512524
String get messageIsEditedLabel => 'EDITED';
513525

lib/generated/l10n/zulip_localizations_en.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Several people are typing…';
510510

511+
@override
512+
String get notifyChannel => 'Notify channel';
513+
514+
@override
515+
String get notifyStream => 'Notify stream';
516+
517+
@override
518+
String get notifyRecipients => 'Notify recipients';
519+
520+
@override
521+
String get notifyTopic => 'Notify topic';
522+
511523
@override
512524
String get messageIsEditedLabel => 'EDITED';
513525

lib/generated/l10n/zulip_localizations_fr.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Several people are typing…';
510510

511+
@override
512+
String get notifyChannel => 'Notify channel';
513+
514+
@override
515+
String get notifyStream => 'Notify stream';
516+
517+
@override
518+
String get notifyRecipients => 'Notify recipients';
519+
520+
@override
521+
String get notifyTopic => 'Notify topic';
522+
511523
@override
512524
String get messageIsEditedLabel => 'EDITED';
513525

lib/generated/l10n/zulip_localizations_ja.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Several people are typing…';
510510

511+
@override
512+
String get notifyChannel => 'Notify channel';
513+
514+
@override
515+
String get notifyStream => 'Notify stream';
516+
517+
@override
518+
String get notifyRecipients => 'Notify recipients';
519+
520+
@override
521+
String get notifyTopic => 'Notify topic';
522+
511523
@override
512524
String get messageIsEditedLabel => 'EDITED';
513525

lib/generated/l10n/zulip_localizations_pl.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Wielu ludzi coś pisze…';
510510

511+
@override
512+
String get notifyChannel => 'Notify channel';
513+
514+
@override
515+
String get notifyStream => 'Notify stream';
516+
517+
@override
518+
String get notifyRecipients => 'Notify recipients';
519+
520+
@override
521+
String get notifyTopic => 'Notify topic';
522+
511523
@override
512524
String get messageIsEditedLabel => 'ZMIENIONO';
513525

lib/generated/l10n/zulip_localizations_ru.dart

+12
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
508508
@override
509509
String get manyPeopleTyping => 'Several people are typing…';
510510

511+
@override
512+
String get notifyChannel => 'Notify channel';
513+
514+
@override
515+
String get notifyStream => 'Notify stream';
516+
517+
@override
518+
String get notifyRecipients => 'Notify recipients';
519+
520+
@override
521+
String get notifyTopic => 'Notify topic';
522+
511523
@override
512524
String get messageIsEditedLabel => 'EDITED';
513525

lib/model/autocomplete.dart

+50-6
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,8 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
423423

424424
factory MentionAutocompleteView.init({
425425
required PerAccountStore store,
426-
required Narrow narrow,
427426
required MentionAutocompleteQuery query,
427+
required Narrow narrow,
428428
}) {
429429
final view = MentionAutocompleteView._(
430430
store: store,
@@ -492,8 +492,6 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
492492
required String? topic,
493493
required PerAccountStore store,
494494
}) {
495-
// TODO(#234): give preference to "all", "everyone" or "stream"
496-
497495
// TODO(#618): give preference to subscribed users first
498496

499497
if (streamId != null) {
@@ -598,9 +596,45 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
598596
return userAName.compareTo(userBName); // TODO(i18n): add locale-aware sorting
599597
}
600598

599+
List<WildcardMentionAutocompleteResult> get wildcardMentionResults {
600+
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
601+
final isChannelOrTopicNarrow = narrow is ChannelNarrow || narrow is TopicNarrow;
602+
603+
final wildcardMentions = <WildcardMentionAutocompleteResult>[];
604+
// Only one of the (all, everyone, channel, stream) channel wildcards are
605+
// shown.
606+
if (query.testWildcard(Wildcard.all)) {
607+
wildcardMentions.add(WildcardMentionAutocompleteResult(
608+
wildcard: Wildcard.all));
609+
} else if (query.testWildcard(Wildcard.everyone)) {
610+
wildcardMentions.add(WildcardMentionAutocompleteResult(
611+
wildcard: Wildcard.everyone));
612+
} else if (isChannelOrTopicNarrow) {
613+
if (query.testWildcard(Wildcard.channel) && isChannelWildcardAvailable) {
614+
wildcardMentions.add(WildcardMentionAutocompleteResult(
615+
wildcard: Wildcard.channel));
616+
} else if (query.testWildcard(Wildcard.stream)) {
617+
wildcardMentions.add(WildcardMentionAutocompleteResult(
618+
wildcard: Wildcard.stream));
619+
}
620+
}
621+
622+
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
623+
if (isChannelOrTopicNarrow
624+
&& isTopicWildcardAvailable
625+
&& query.testWildcard(Wildcard.topic)) {
626+
wildcardMentions.add(WildcardMentionAutocompleteResult(
627+
wildcard: Wildcard.topic));
628+
}
629+
return wildcardMentions;
630+
}
631+
601632
@override
602633
Future<List<MentionAutocompleteResult>?> computeResults() async {
603634
final results = <MentionAutocompleteResult>[];
635+
// Give priority to wildcard mentions.
636+
results.addAll(wildcardMentionResults);
637+
604638
if (await filterCandidates(filter: _testUser,
605639
candidates: sortedUsers, results: results)) {
606640
return null;
@@ -625,6 +659,9 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
625659
}
626660
}
627661

662+
// The available user wildcard mention options.
663+
enum Wildcard { all, everyone, channel, stream, topic }
664+
628665
/// A query the user has entered into some form of autocomplete.
629666
///
630667
/// Subclasses correspond to different types of autocomplete interaction
@@ -694,9 +731,12 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
694731
return MentionAutocompleteView.init(store: store, narrow: narrow, query: this);
695732
}
696733

734+
bool testWildcard(Wildcard wildcard) {
735+
return wildcard.name.contains(raw.toLowerCase());
736+
}
737+
697738
bool testUser(User user, AutocompleteDataCache cache) {
698739
// TODO(#236) test email too, not just name
699-
700740
if (!user.isActive) return false;
701741

702742
return _testName(user, cache);
@@ -788,9 +828,13 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
788828
final int userId;
789829
}
790830

791-
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
831+
class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
832+
WildcardMentionAutocompleteResult({required this.wildcard});
833+
834+
final Wildcard wildcard;
835+
}
792836

793-
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
837+
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
794838

795839
/// An autocomplete interaction for choosing a topic for a message.
796840
class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, TopicAutocompleteResult> {

lib/model/compose.dart

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:math';
22

33
import '../api/model/model.dart';
4+
import 'autocomplete.dart';
45
import 'internal_link.dart';
56
import 'narrow.dart';
67
import 'store.dart';
@@ -101,18 +102,33 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
101102
return resultBuffer.toString();
102103
}
103104

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

113114
return '@${silent ? '_' : ''}**${user.fullName}${includeUserId ? '|${user.userId}' : ''}**';
114115
}
115116

117+
/// An @wildcard-mention, like @**channel**.
118+
String wildcardMention(Wildcard wildcard, {
119+
required PerAccountStore store,
120+
}) {
121+
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
122+
assert(isChannelWildcardAvailable || wildcard != Wildcard.channel);
123+
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
124+
assert(isTopicWildcardAvailable || wildcard != Wildcard.topic);
125+
126+
final name = wildcard == Wildcard.stream && isChannelWildcardAvailable
127+
? Wildcard.channel.name
128+
: wildcard.name;
129+
return '@**$name**';
130+
}
131+
116132
/// https://spec.commonmark.org/0.30/#inline-link
117133
///
118134
/// The "link text" is made by enclosing [visibleText] in square brackets.
@@ -145,7 +161,7 @@ String quoteAndReplyPlaceholder(PerAccountStore store, {
145161
SendableNarrow.ofMessage(message, selfUserId: store.selfUserId),
146162
nearMessageId: message.id);
147163
// See note in [quoteAndReply] about asking `mention` to omit the |<id> part.
148-
return '${mention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
164+
return '${userMention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
149165
'*(loading message ${message.id})*\n'; // TODO(i18n) ?
150166
}
151167

@@ -169,6 +185,6 @@ String quoteAndReply(PerAccountStore store, {
169185
// Could ask `mention` to omit the |<id> part unless the mention is ambiguous…
170186
// but that would mean a linear scan through all users, and the extra noise
171187
// 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) ?
188+
return '${userMention(sender!, silent: true)} ${inlineLink('said', url)}:\n' // TODO(i18n) ?
173189
'${wrapWithBacktickFence(content: rawContent, infoString: 'quote')}';
174190
}

0 commit comments

Comments
 (0)