Skip to content

Commit 8bfc86b

Browse files
committed
api [nfc]: Add TopicName.processLikeServer
The point of this helper is to replicate what a topic sent from the client will become, after being processed by the server. This important when trying to create a local copy of a stream message, whose topic can get translated when it's delivered by the server.
1 parent 1e8f2c2 commit 8bfc86b

File tree

3 files changed

+79
-9
lines changed

3 files changed

+79
-9
lines changed

lib/api/model/model.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,15 @@ String? tryParseEmojiCodeToUnicode(String emojiCode) {
550550
}
551551
}
552552

553+
/// The topic servers understand to mean "there is no topic".
554+
///
555+
/// This should match
556+
/// https://github.com/zulip/zulip/blob/6.0/zerver/actions/message_edit.py#L940
557+
/// or similar logic at the latest `main`.
558+
// This is hardcoded in the server, and therefore untranslated; that's
559+
// zulip/zulip#3639.
560+
const String kNoTopicTopic = '(no topic)';
561+
553562
/// The name of a Zulip topic.
554563
// TODO(dart): Can we forbid calling Object members on this extension type?
555564
// (The lack of "implements Object" ought to do that, but doesn't.)
@@ -604,6 +613,51 @@ extension type const TopicName(String _value) {
604613
/// using [canonicalize].
605614
bool isSameAs(TopicName other) => canonicalize() == other.canonicalize();
606615

616+
/// Process this topic to match how it would appear on a message object from
617+
/// the server.
618+
///
619+
/// This assumes that the topic is constructed from a string without
620+
/// leading/trailing whitespace.
621+
///
622+
/// For a client that does not support empty topics, when FL>=334, the server
623+
/// converts empty topics to `store.realmEmptyTopicDisplayName`; when FL>=370,
624+
/// the server converts "(no topic)" to `store.realmEmptyTopicDisplayName`
625+
/// as well.
626+
///
627+
/// See API docs:
628+
/// https://zulip.com/api/send-message#parameter-topic
629+
TopicName processLikeServer({
630+
required int zulipFeatureLevel,
631+
required String? realmEmptyTopicDisplayName,
632+
}) {
633+
assert(_value.trim() == _value);
634+
// TODO(server-10) simplify this away
635+
if (zulipFeatureLevel < 334) {
636+
// From the API docs:
637+
// > Before Zulip 10.0 (feature level 334), empty string was not a valid
638+
// > topic name for channel messages.
639+
assert(_value.isNotEmpty);
640+
return this;
641+
}
642+
643+
// TODO(server-10) simplify this away
644+
if (zulipFeatureLevel < 370 && _value == kNoTopicTopic) {
645+
// From the API docs:
646+
// > Before Zulip 10.0 (feature level 370), "(no topic)" was not
647+
// > interpreted as an empty string.
648+
return TopicName(kNoTopicTopic);
649+
}
650+
651+
// TODO(#1250): This assumes that the 'empty_topic_name' client capability
652+
// is not declared. When we set 'empty_topic_name' to true,
653+
// make this return an empty topic if the value matches "(no topic)"
654+
// or realmEmptyTopicDisplayName.
655+
if (_value == kNoTopicTopic || _value.isEmpty) {
656+
return TopicName(realmEmptyTopicDisplayName!);
657+
}
658+
return TopicName(_value);
659+
}
660+
607661
TopicName.fromJson(this._value);
608662

609663
String toJson() => apiName;

lib/api/route/messages.dart

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,6 @@ const int kMaxTopicLengthCodePoints = 60;
169169
// https://zulip.com/api/send-message#parameter-content
170170
const int kMaxMessageLengthCodePoints = 10000;
171171

172-
/// The topic servers understand to mean "there is no topic".
173-
///
174-
/// This should match
175-
/// https://github.com/zulip/zulip/blob/6.0/zerver/actions/message_edit.py#L940
176-
/// or similar logic at the latest `main`.
177-
// This is hardcoded in the server, and therefore untranslated; that's
178-
// zulip/zulip#3639.
179-
const String kNoTopicTopic = '(no topic)';
180-
181172
/// https://zulip.com/api/send-message
182173
Future<SendMessageResult> sendMessage(
183174
ApiConnection connection, {

test/api/model/model_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,31 @@ void main() {
161161

162162
doCheck(eg.t('✔ a'), eg.t('✔ b'), false);
163163
});
164+
165+
test('processLikeServer', () {
166+
final emptyTopicDisplayName = eg.defaultRealmEmptyTopicDisplayName;
167+
void doCheck(TopicName topicA, TopicName expected, int zulipFeatureLevel) {
168+
check(topicA.processLikeServer(
169+
zulipFeatureLevel: zulipFeatureLevel,
170+
realmEmptyTopicDisplayName: emptyTopicDisplayName),
171+
).equals(expected);
172+
}
173+
174+
check(() => eg.t('').processLikeServer(
175+
zulipFeatureLevel: 333,
176+
realmEmptyTopicDisplayName: emptyTopicDisplayName),
177+
).throws<void>();
178+
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 333);
179+
doCheck(eg.t(emptyTopicDisplayName), eg.t(emptyTopicDisplayName), 333);
180+
doCheck(eg.t('other topic'), eg.t('other topic'), 333);
181+
182+
doCheck(eg.t(''), eg.t(emptyTopicDisplayName), 334);
183+
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 334);
184+
doCheck(eg.t(emptyTopicDisplayName), eg.t(emptyTopicDisplayName), 334);
185+
doCheck(eg.t('other topic'), eg.t('other topic'), 334);
186+
187+
doCheck(eg.t('(no topic)'), eg.t(emptyTopicDisplayName), 370);
188+
});
164189
});
165190

166191
group('DmMessage', () {

0 commit comments

Comments
 (0)