Skip to content

Commit

Permalink
autocomplete: Support @-wildcard in user-mention autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
sm-sayedi committed Dec 14, 2024
1 parent bdef21c commit ee45cde
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 54 deletions.
5 changes: 4 additions & 1 deletion assets/l10n/app_ar.arb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{

"notifyChannel": "إخطار القناة",
"notifyStream": "إخطار الدفق",
"notifyRecipients": "إخطار المستلمين",
"notifyTopic": "إخطار الموضوع"
}
16 changes: 16 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,22 @@
"@manyPeopleTyping": {
"description": "Text to display when there are multiple users typing."
},
"notifyChannel": "Notify channel",
"@notifyChannel": {
"description": "Description for \"@all\", \"@everyone\", \"@channel\", and \"@stream\" wildcard mentions in a channel or topic narrow."
},
"notifyStream": "Notify stream",
"@notifyStream": {
"description": "Description for \"@all\", \"@everyone\", and \"@stream\" wildcard mentions in a stream or topic narrow."
},
"notifyRecipients": "Notify recipients",
"@notifyRecipients": {
"description": "Description for \"@all\" and \"@everyone\" wildcard mentions in a DM narrow."
},
"notifyTopic": "Notify topic",
"@notifyTopic": {
"description": "Description for \"@topic\" wildcard mention in a channel or topic narrow."
},
"messageIsEditedLabel": "EDITED",
"@messageIsEditedLabel": {
"description": "Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"
Expand Down
24 changes: 24 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,30 @@ abstract class ZulipLocalizations {
/// **'Several people are typing…'**
String get manyPeopleTyping;

/// Description for "@all", "@everyone", "@channel", and "@stream" wildcard mentions in a channel or topic narrow.
///
/// In en, this message translates to:
/// **'Notify channel'**
String get notifyChannel;

/// Description for "@all", "@everyone", and "@stream" wildcard mentions in a stream or topic narrow.
///
/// In en, this message translates to:
/// **'Notify stream'**
String get notifyStream;

/// Description for "@all" and "@everyone" wildcard mentions in a DM narrow.
///
/// In en, this message translates to:
/// **'Notify recipients'**
String get notifyRecipients;

/// Description for "@topic" wildcard mention in a channel or topic narrow.
///
/// In en, this message translates to:
/// **'Notify topic'**
String get notifyTopic;

/// Label for an edited message. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)
///
/// In en, this message translates to:
Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Several people are typing…';

@override
String get notifyChannel => 'إخطار القناة';

@override
String get notifyStream => 'إخطار الدفق';

@override
String get notifyRecipients => 'إخطار المستلمين';

@override
String get notifyTopic => 'إخطار الموضوع';

@override
String get messageIsEditedLabel => 'EDITED';

Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Several people are typing…';

@override
String get notifyChannel => 'Notify channel';

@override
String get notifyStream => 'Notify stream';

@override
String get notifyRecipients => 'Notify recipients';

@override
String get notifyTopic => 'Notify topic';

@override
String get messageIsEditedLabel => 'EDITED';

Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_fr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Several people are typing…';

@override
String get notifyChannel => 'Notify channel';

@override
String get notifyStream => 'Notify stream';

@override
String get notifyRecipients => 'Notify recipients';

@override
String get notifyTopic => 'Notify topic';

@override
String get messageIsEditedLabel => 'EDITED';

Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Several people are typing…';

@override
String get notifyChannel => 'Notify channel';

@override
String get notifyStream => 'Notify stream';

@override
String get notifyRecipients => 'Notify recipients';

@override
String get notifyTopic => 'Notify topic';

@override
String get messageIsEditedLabel => 'EDITED';

Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Wielu ludzi coś pisze…';

@override
String get notifyChannel => 'Notify channel';

@override
String get notifyStream => 'Notify stream';

@override
String get notifyRecipients => 'Notify recipients';

@override
String get notifyTopic => 'Notify topic';

@override
String get messageIsEditedLabel => 'ZMIENIONO';

Expand Down
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get manyPeopleTyping => 'Several people are typing…';

@override
String get notifyChannel => 'Notify channel';

@override
String get notifyStream => 'Notify stream';

@override
String get notifyRecipients => 'Notify recipients';

@override
String get notifyTopic => 'Notify topic';

@override
String get messageIsEditedLabel => 'EDITED';

Expand Down
56 changes: 50 additions & 6 deletions lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,8 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,

factory MentionAutocompleteView.init({
required PerAccountStore store,
required Narrow narrow,
required MentionAutocompleteQuery query,
required Narrow narrow,
}) {
final view = MentionAutocompleteView._(
store: store,
Expand Down Expand Up @@ -492,8 +492,6 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
required String? topic,
required PerAccountStore store,
}) {
// TODO(#234): give preference to "all", "everyone" or "stream"

// TODO(#618): give preference to subscribed users first

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

List<WildcardMentionAutocompleteResult> get wildcardMentionResults {
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
final isChannelOrTopicNarrow = narrow is ChannelNarrow || narrow is TopicNarrow;

final wildcardMentions = <WildcardMentionAutocompleteResult>[];
// Only one of the (all, everyone, channel, stream) channel wildcards are
// shown.
if (query.testWildcard(Wildcard.all)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: Wildcard.all));
} else if (query.testWildcard(Wildcard.everyone)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: Wildcard.everyone));
} else if (isChannelOrTopicNarrow) {
if (query.testWildcard(Wildcard.channel) && isChannelWildcardAvailable) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: Wildcard.channel));
} else if (query.testWildcard(Wildcard.stream)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: Wildcard.stream));
}
}

final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
if (isChannelOrTopicNarrow
&& isTopicWildcardAvailable
&& query.testWildcard(Wildcard.topic)) {
wildcardMentions.add(WildcardMentionAutocompleteResult(
wildcard: Wildcard.topic));
}
return wildcardMentions;
}

@override
Future<List<MentionAutocompleteResult>?> computeResults() async {
final results = <MentionAutocompleteResult>[];
// Give priority to wildcard mentions.
results.addAll(wildcardMentionResults);

if (await filterCandidates(filter: _testUser,
candidates: sortedUsers, results: results)) {
return null;
Expand All @@ -625,6 +659,9 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
}
}

// The available user wildcard mention options.
enum Wildcard { all, everyone, channel, stream, topic }

/// A query the user has entered into some form of autocomplete.
///
/// Subclasses correspond to different types of autocomplete interaction
Expand Down Expand Up @@ -694,9 +731,12 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
return MentionAutocompleteView.init(store: store, narrow: narrow, query: this);
}

bool testWildcard(Wildcard wildcard) {
return wildcard.name.contains(raw.toLowerCase());
}

bool testUser(User user, AutocompleteDataCache cache) {
// TODO(#236) test email too, not just name

if (!user.isActive) return false;

return _testName(user, cache);
Expand Down Expand Up @@ -788,9 +828,13 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
final int userId;
}

// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
WildcardMentionAutocompleteResult({required this.wildcard});

final Wildcard wildcard;
}

// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {

/// An autocomplete interaction for choosing a topic for a message.
class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, TopicAutocompleteResult> {
Expand Down
24 changes: 20 additions & 4 deletions lib/model/compose.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math';

import '../api/model/model.dart';
import 'autocomplete.dart';
import 'internal_link.dart';
import 'narrow.dart';
import 'store.dart';
Expand Down Expand Up @@ -101,18 +102,33 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
return resultBuffer.toString();
}

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

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

/// An @wildcard-mention, like @**channel**.
String wildcardMention(Wildcard wildcard, {
required PerAccountStore store,
}) {
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
assert(isChannelWildcardAvailable || wildcard != Wildcard.channel);
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(sever-8)
assert(isTopicWildcardAvailable || wildcard != Wildcard.topic);

final name = wildcard == Wildcard.stream && isChannelWildcardAvailable
? Wildcard.channel.name
: wildcard.name;
return '@**$name**';
}

/// https://spec.commonmark.org/0.30/#inline-link
///
/// The "link text" is made by enclosing [visibleText] in square brackets.
Expand Down Expand Up @@ -145,7 +161,7 @@ String quoteAndReplyPlaceholder(PerAccountStore store, {
SendableNarrow.ofMessage(message, selfUserId: store.selfUserId),
nearMessageId: message.id);
// See note in [quoteAndReply] about asking `mention` to omit the |<id> part.
return '${mention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
return '${userMention(sender!, silent: true)} ${inlineLink('said', url)}: ' // TODO(i18n) ?
'*(loading message ${message.id})*\n'; // TODO(i18n) ?
}

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

0 comments on commit ee45cde

Please sign in to comment.