Skip to content

Commit 45accee

Browse files
committed
realm [nfc]: Move processTopicLikeServer to substore, from API code
This way, the method's implementation can look up zulipFeatureLevel and realmEmptyTopicDisplayName for itself, rather than have them looked up in advance by the caller and passed in. Then as a further consequence, the method only looks up realmEmptyTopicDisplayName if and when it determines it actually needs that information. That makes the lookup compatible with the check we have in the main realmEmptyTopicDisplayName getter to assert that it's only consulted when it should in fact be needed, on old servers. A hack in the message store, and a similar hack on the message list, therefore become unnecessary.
1 parent 4b56bba commit 45accee

File tree

7 files changed

+97
-110
lines changed

7 files changed

+97
-110
lines changed

lib/api/model/model.dart

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -837,53 +837,6 @@ extension type const TopicName(String _value) {
837837
/// using [canonicalize].
838838
bool isSameAs(TopicName other) => canonicalize() == other.canonicalize();
839839

840-
/// Process this topic to match how it would appear on a message object from
841-
/// the server.
842-
///
843-
/// This returns the [TopicName] the server would be predicted to include
844-
/// in a message object resulting from sending to this [TopicName]
845-
/// in a [sendMessage] request.
846-
///
847-
/// This [TopicName] is required to have no leading or trailing whitespace.
848-
///
849-
/// For a client that supports empty topics, when FL>=334, the server converts
850-
/// `store.realmEmptyTopicDisplayName` to an empty string; when FL>=370,
851-
/// the server converts "(no topic)" to an empty string as well.
852-
///
853-
/// See API docs:
854-
/// https://zulip.com/api/send-message#parameter-topic
855-
TopicName processLikeServer({
856-
required int zulipFeatureLevel,
857-
required String? realmEmptyTopicDisplayName,
858-
}) {
859-
assert(_value.trim() == _value);
860-
// TODO(server-10) simplify this away
861-
if (zulipFeatureLevel < 334) {
862-
// From the API docs:
863-
// > Before Zulip 10.0 (feature level 334), empty string was not a valid
864-
// > topic name for channel messages.
865-
assert(_value.isNotEmpty);
866-
return this;
867-
}
868-
869-
// TODO(server-10) simplify this away
870-
if (zulipFeatureLevel < 370 && _value == kNoTopicTopic) {
871-
// From the API docs:
872-
// > Before Zulip 10.0 (feature level 370), "(no topic)" was not
873-
// > interpreted as an empty string.
874-
return TopicName(kNoTopicTopic);
875-
}
876-
877-
if (_value == kNoTopicTopic || _value == realmEmptyTopicDisplayName) {
878-
// From the API docs:
879-
// > When "(no topic)" or the value of realm_empty_topic_display_name
880-
// > found in the POST /register response is used for [topic],
881-
// > it is interpreted as an empty string.
882-
return TopicName('');
883-
}
884-
return TopicName(_value);
885-
}
886-
887840
TopicName.fromJson(this._value);
888841

889842
String toJson() => apiName;

lib/model/message.dart

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../api/route/messages.dart';
1212
import '../log.dart';
1313
import 'binding.dart';
1414
import 'message_list.dart';
15+
import 'realm.dart';
1516
import 'store.dart';
1617

1718
const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809
@@ -102,20 +103,12 @@ class _EditMessageRequestStatus {
102103
final String newContent;
103104
}
104105

105-
class MessageStoreImpl extends PerAccountStoreBase with MessageStore, _OutboxMessageStore {
106-
MessageStoreImpl({required super.core, required String? realmEmptyTopicDisplayName})
107-
: _realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
108-
// There are no messages in InitialSnapshot, so we don't have
106+
class MessageStoreImpl extends HasRealmStore with MessageStore, _OutboxMessageStore {
107+
MessageStoreImpl({required super.realm})
108+
: // There are no messages in InitialSnapshot, so we don't have
109109
// a use case for initializing MessageStore with nonempty [messages].
110110
messages = {};
111111

112-
// This copy of the realm setting is here to bypass the feature-level check
113-
// on the usual getter for it. See discussion:
114-
// https://github.com/zulip/zulip-flutter/pull/1472#discussion_r2099069276
115-
// TODO move [TopicName.processLikeServer] to a substore, eliminating this
116-
@override
117-
final String? _realmEmptyTopicDisplayName; // TODO(#668): update this realm setting
118-
119112
@override
120113
final Map<int, Message> messages;
121114

@@ -771,9 +764,7 @@ class DmOutboxMessage extends OutboxMessage<DmConversation> {
771764
}
772765

773766
/// Manages the outbox messages portion of [MessageStore].
774-
mixin _OutboxMessageStore on PerAccountStoreBase {
775-
String? get _realmEmptyTopicDisplayName;
776-
767+
mixin _OutboxMessageStore on HasRealmStore {
777768
late final UnmodifiableMapView<int, OutboxMessage> outboxMessages =
778769
UnmodifiableMapView(_outboxMessages);
779770
final Map<int, OutboxMessage> _outboxMessages = {};
@@ -918,22 +909,21 @@ mixin _OutboxMessageStore on PerAccountStoreBase {
918909
}
919910

920911
TopicName _processTopicLikeServer(TopicName topic) {
921-
return topic.processLikeServer(
922-
// Processing this just once on creating the outbox message
923-
// allows an uncommon bug, because either of these values can change.
924-
// During the outbox message's life, a topic processed from
925-
// "(no topic)" could become stale/wrong when zulipFeatureLevel
926-
// changes; a topic processed from "general chat" could become
927-
// stale/wrong when realmEmptyTopicDisplayName changes.
928-
//
929-
// Shrug. The same effect is caused by an unavoidable race:
930-
// an admin could change the name of "general chat"
931-
// (i.e. the value of realmEmptyTopicDisplayName)
932-
// concurrently with the user making the send request,
933-
// so that the setting in effect by the time the request arrives
934-
// is different from the setting the client last heard about.
935-
zulipFeatureLevel: zulipFeatureLevel,
936-
realmEmptyTopicDisplayName: _realmEmptyTopicDisplayName);
912+
// Processing this just once on creating the outbox message
913+
// allows an uncommon bug, because either of the values
914+
// [zulipFeatureLevel] or [realmEmptyTopicDisplayName] can change.
915+
// During the outbox message's life, a topic processed from
916+
// "(no topic)" could become stale/wrong when zulipFeatureLevel
917+
// changes; a topic processed from "general chat" could become
918+
// stale/wrong when realmEmptyTopicDisplayName changes.
919+
//
920+
// Shrug. The same effect is caused by an unavoidable race:
921+
// an admin could change the name of "general chat"
922+
// (i.e. the value of realmEmptyTopicDisplayName)
923+
// concurrently with the user making the send request,
924+
// so that the setting in effect by the time the request arrives
925+
// is different from the setting the client last heard about.
926+
return processTopicLikeServer(topic);
937927
}
938928

939929
void _handleOutboxDebounce(int localMessageId) {

lib/model/realm.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,51 @@ mixin RealmStore on PerAccountStoreBase {
3535
List<CustomProfileField> get customProfileFields;
3636
/// For docs, please see [InitialSnapshot.emailAddressVisibility].
3737
EmailAddressVisibility? get emailAddressVisibility;
38+
39+
/// Process the given topic to match how it would appear
40+
/// on a message object from the server.
41+
///
42+
/// This returns the [TopicName] the server would be predicted to include
43+
/// in a message object resulting from sending to the given [TopicName]
44+
/// in a [sendMessage] request.
45+
///
46+
/// The [TopicName] is required to have no leading or trailing whitespace.
47+
///
48+
/// For a client that supports empty topics, when FL>=334, the server converts
49+
/// `store.realmEmptyTopicDisplayName` to an empty string; when FL>=370,
50+
/// the server converts "(no topic)" to an empty string as well.
51+
///
52+
/// See API docs:
53+
/// https://zulip.com/api/send-message#parameter-topic
54+
TopicName processTopicLikeServer(TopicName topic) {
55+
final apiName = topic.apiName;
56+
assert(apiName.trim() == apiName);
57+
// TODO(server-10) simplify this away
58+
if (zulipFeatureLevel < 334) {
59+
// From the API docs:
60+
// > Before Zulip 10.0 (feature level 334), empty string was not a valid
61+
// > topic name for channel messages.
62+
assert(apiName.isNotEmpty);
63+
return topic;
64+
}
65+
66+
// TODO(server-10) simplify this away
67+
if (zulipFeatureLevel < 370 && apiName == kNoTopicTopic) {
68+
// From the API docs:
69+
// > Before Zulip 10.0 (feature level 370), "(no topic)" was not
70+
// > interpreted as an empty string.
71+
return TopicName(kNoTopicTopic);
72+
}
73+
74+
if (apiName == kNoTopicTopic || apiName == realmEmptyTopicDisplayName) {
75+
// From the API docs:
76+
// > When "(no topic)" or the value of realm_empty_topic_display_name
77+
// > found in the POST /register response is used for [topic],
78+
// > it is interpreted as an empty string.
79+
return TopicName('');
80+
}
81+
return topic;
82+
}
3883
}
3984

4085
mixin ProxyRealmStore on RealmStore {

lib/model/store.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,12 @@ class PerAccountStore extends PerAccountStoreBase with
483483
selfUserId: account.userId,
484484
);
485485
final channels = ChannelStoreImpl(initialSnapshot: initialSnapshot);
486+
final realm = RealmStoreImpl(core: core, initialSnapshot: initialSnapshot);
486487
return PerAccountStore._(
487488
core: core,
488489
groups: UserGroupStoreImpl(core: core,
489490
groups: initialSnapshot.realmUserGroups),
490-
realm: RealmStoreImpl(core: core, initialSnapshot: initialSnapshot),
491+
realm: realm,
491492
emoji: EmojiStoreImpl(
492493
core: core, allRealmEmoji: initialSnapshot.realmEmoji),
493494
userSettings: initialSnapshot.userSettings,
@@ -509,8 +510,7 @@ class PerAccountStore extends PerAccountStoreBase with
509510
realmPresenceDisabled: initialSnapshot.realmPresenceDisabled,
510511
initial: initialSnapshot.presences),
511512
channels: channels,
512-
messages: MessageStoreImpl(core: core,
513-
realmEmptyTopicDisplayName: initialSnapshot.realmEmptyTopicDisplayName),
513+
messages: MessageStoreImpl(realm: realm),
514514
unreads: Unreads(
515515
initial: initialSnapshot.unreadMsgs,
516516
core: core,

lib/widgets/message_list.dart

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -778,11 +778,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
778778
if (narrow is TopicNarrow) {
779779
// Normalize topic name. See #1717.
780780
narrow = TopicNarrow(narrow.streamId,
781-
narrow.topic.processLikeServer(
782-
zulipFeatureLevel: store.zulipFeatureLevel,
783-
realmEmptyTopicDisplayName: store.zulipFeatureLevel > 334
784-
? store.realmEmptyTopicDisplayName
785-
: null),
781+
store.processTopicLikeServer(narrow.topic),
786782
with_: narrow.with_);
787783
if (narrow != widget.narrow) {
788784
SchedulerBinding.instance.scheduleFrameCallback((_) {

test/api/model/model_test.dart

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

228228
doCheck(eg.t('✔ a'), eg.t('✔ b'), false);
229229
});
230-
231-
test('processLikeServer', () {
232-
final emptyTopicDisplayName = eg.defaultRealmEmptyTopicDisplayName;
233-
void doCheck(TopicName topic, TopicName expected, int zulipFeatureLevel) {
234-
check(topic.processLikeServer(
235-
zulipFeatureLevel: zulipFeatureLevel,
236-
realmEmptyTopicDisplayName: emptyTopicDisplayName),
237-
).equals(expected);
238-
}
239-
240-
check(() => eg.t('').processLikeServer(
241-
zulipFeatureLevel: 333,
242-
realmEmptyTopicDisplayName: emptyTopicDisplayName),
243-
).throws<void>();
244-
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 333);
245-
doCheck(eg.t(emptyTopicDisplayName), eg.t(emptyTopicDisplayName), 333);
246-
doCheck(eg.t('other topic'), eg.t('other topic'), 333);
247-
248-
doCheck(eg.t(''), eg.t(''), 334);
249-
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 334);
250-
doCheck(eg.t(emptyTopicDisplayName), eg.t(''), 334);
251-
doCheck(eg.t('other topic'), eg.t('other topic'), 334);
252-
253-
doCheck(eg.t('(no topic)'), eg.t(''), 370);
254-
});
255230
});
256231

257232
group('DmMessage', () {

test/model/realm_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,32 @@ void main() {
4747
check(store.customProfileFields.map((f) => f.id)).deepEquals([2, 0, 1]);
4848
});
4949
});
50+
51+
test('processTopicLikeServer', () {
52+
final emptyTopicDisplayName = eg.defaultRealmEmptyTopicDisplayName;
53+
54+
TopicName process(TopicName topic, int zulipFeatureLevel) {
55+
final account = eg.selfAccount.copyWith(zulipFeatureLevel: zulipFeatureLevel);
56+
final store = eg.store(account: account, initialSnapshot: eg.initialSnapshot(
57+
zulipFeatureLevel: zulipFeatureLevel,
58+
realmEmptyTopicDisplayName: emptyTopicDisplayName));
59+
return store.processTopicLikeServer(topic);
60+
}
61+
62+
void doCheck(TopicName topic, TopicName expected, int zulipFeatureLevel) {
63+
check(process(topic, zulipFeatureLevel)).equals(expected);
64+
}
65+
66+
check(() => process(eg.t(''), 333)).throws<void>();
67+
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 333);
68+
doCheck(eg.t(emptyTopicDisplayName), eg.t(emptyTopicDisplayName), 333);
69+
doCheck(eg.t('other topic'), eg.t('other topic'), 333);
70+
71+
doCheck(eg.t(''), eg.t(''), 334);
72+
doCheck(eg.t('(no topic)'), eg.t('(no topic)'), 334);
73+
doCheck(eg.t(emptyTopicDisplayName), eg.t(''), 334);
74+
doCheck(eg.t('other topic'), eg.t('other topic'), 334);
75+
76+
doCheck(eg.t('(no topic)'), eg.t(''), 370);
77+
});
5078
}

0 commit comments

Comments
 (0)