Skip to content

Commit 000cc84

Browse files
committed
api: Indicate support for handling empty topics
Look for `allow_empty_topic_name` and `empty_topic_name` under "Feature level 334" in the API Changelog to verify the affected routes: https://zulip.com/api/changelog To keep the API bindings thin, instead of setting `allow_empty_topic_name` for the callers, we require the callers to pass the appropriate values instead. Fixes: #1250
1 parent 4c198de commit 000cc84

File tree

11 files changed

+95
-4
lines changed

11 files changed

+95
-4
lines changed

lib/api/route/channels.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ part 'channels.g.dart';
77
/// https://zulip.com/api/get-stream-topics
88
Future<GetStreamTopicsResult> getStreamTopics(ApiConnection connection, {
99
required int streamId,
10+
required bool allowEmptyTopicName,
1011
}) {
11-
return connection.get('getStreamTopics', GetStreamTopicsResult.fromJson, 'users/me/$streamId/topics', {});
12+
assert(allowEmptyTopicName, '`allowEmptyTopicName` should only be true');
13+
return connection.get('getStreamTopics', GetStreamTopicsResult.fromJson, 'users/me/$streamId/topics', {
14+
'allow_empty_topic_name': allowEmptyTopicName,
15+
});
1216
}
1317

1418
@JsonSerializable(fieldRename: FieldRename.snake)

lib/api/route/events.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Future<InitialSnapshot> registerQueue(ApiConnection connection) {
1818
'user_avatar_url_field_optional': false, // TODO(#254): turn on
1919
'stream_typing_notifications': true,
2020
'user_settings_object': true,
21+
'empty_topic_name': true,
2122
},
2223
});
2324
}

lib/api/route/messages.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ part 'messages.g.dart';
1616
Future<Message?> getMessageCompat(ApiConnection connection, {
1717
required int messageId,
1818
bool? applyMarkdown,
19+
required bool allowEmptyTopicName,
1920
}) async {
2021
final useLegacyApi = connection.zulipFeatureLevel! < 120;
2122
if (useLegacyApi) {
@@ -25,6 +26,7 @@ Future<Message?> getMessageCompat(ApiConnection connection, {
2526
numBefore: 0,
2627
numAfter: 0,
2728
applyMarkdown: applyMarkdown,
29+
allowEmptyTopicName: allowEmptyTopicName,
2830

2931
// Hard-code this param to `true`, as the new single-message API
3032
// effectively does:
@@ -37,6 +39,7 @@ Future<Message?> getMessageCompat(ApiConnection connection, {
3739
final response = await getMessage(connection,
3840
messageId: messageId,
3941
applyMarkdown: applyMarkdown,
42+
allowEmptyTopicName: allowEmptyTopicName,
4043
);
4144
return response.message;
4245
} on ZulipApiException catch (e) {
@@ -57,10 +60,13 @@ Future<Message?> getMessageCompat(ApiConnection connection, {
5760
Future<GetMessageResult> getMessage(ApiConnection connection, {
5861
required int messageId,
5962
bool? applyMarkdown,
63+
required bool allowEmptyTopicName,
6064
}) {
65+
assert(allowEmptyTopicName, '`allowEmptyTopicName` should only be true');
6166
assert(connection.zulipFeatureLevel! >= 120);
6267
return connection.get('getMessage', GetMessageResult.fromJson, 'messages/$messageId', {
6368
if (applyMarkdown != null) 'apply_markdown': applyMarkdown,
69+
'allow_empty_topic_name': allowEmptyTopicName,
6470
});
6571
}
6672

@@ -89,8 +95,10 @@ Future<GetMessagesResult> getMessages(ApiConnection connection, {
8995
required int numAfter,
9096
bool? clientGravatar,
9197
bool? applyMarkdown,
98+
required bool allowEmptyTopicName,
9299
// bool? useFirstUnreadAnchor // omitted because deprecated
93100
}) {
101+
assert(allowEmptyTopicName, '`allowEmptyTopicName` should only be true');
94102
return connection.get('getMessages', GetMessagesResult.fromJson, 'messages', {
95103
'narrow': resolveApiNarrowForServer(narrow, connection.zulipFeatureLevel!),
96104
'anchor': RawParameter(anchor.toJson()),
@@ -99,6 +107,7 @@ Future<GetMessagesResult> getMessages(ApiConnection connection, {
99107
'num_after': numAfter,
100108
if (clientGravatar != null) 'client_gravatar': clientGravatar,
101109
if (applyMarkdown != null) 'apply_markdown': applyMarkdown,
110+
'allow_empty_topic_name': allowEmptyTopicName,
102111
});
103112
}
104113

lib/model/autocomplete.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,9 @@ class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, Top
904904
Future<void> _fetch() async {
905905
assert(!_isFetching);
906906
_isFetching = true;
907-
final result = await getStreamTopics(store.connection, streamId: streamId);
907+
final result = await getStreamTopics(store.connection, streamId: streamId,
908+
allowEmptyTopicName: true,
909+
);
908910
_topics = result.topics.map((e) => e.name);
909911
_isFetching = false;
910912
return _startSearch();

lib/model/message_list.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
495495
anchor: AnchorCode.newest,
496496
numBefore: kMessageListFetchBatchSize,
497497
numAfter: 0,
498+
allowEmptyTopicName: true,
498499
);
499500
if (this.generation > generation) return;
500501
_adjustNarrowForTopicPermalink(result.messages.firstOrNull);
@@ -567,6 +568,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
567568
includeAnchor: false,
568569
numBefore: kMessageListFetchBatchSize,
569570
numAfter: 0,
571+
allowEmptyTopicName: true,
570572
);
571573
} catch (e) {
572574
hasFetchError = true;

lib/widgets/actions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ abstract final class ZulipAction {
260260
fetchedMessage = await getMessageCompat(PerAccountStoreWidget.of(context).connection,
261261
messageId: messageId,
262262
applyMarkdown: false,
263+
allowEmptyTopicName: true,
263264
);
264265
if (fetchedMessage == null) {
265266
errorMessage = zulipLocalizations.errorMessageDoesNotSeemToExist;

test/api/route/messages_test.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ void main() {
2020
required bool expectLegacy,
2121
required int messageId,
2222
bool? applyMarkdown,
23+
required bool allowEmptyTopicName,
2324
}) async {
2425
final result = await getMessageCompat(connection,
2526
messageId: messageId,
2627
applyMarkdown: applyMarkdown,
28+
allowEmptyTopicName: allowEmptyTopicName,
2729
);
2830
if (expectLegacy) {
2931
check(connection.lastRequest).isA<http.Request>()
@@ -35,6 +37,7 @@ void main() {
3537
'num_before': '0',
3638
'num_after': '0',
3739
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
40+
'allow_empty_topic_name': allowEmptyTopicName.toString(),
3841
'client_gravatar': 'true',
3942
});
4043
} else {
@@ -43,6 +46,7 @@ void main() {
4346
..url.path.equals('/api/v1/messages/$messageId')
4447
..url.queryParameters.deepEquals({
4548
if (applyMarkdown != null) 'apply_markdown': applyMarkdown.toString(),
49+
'allow_empty_topic_name': allowEmptyTopicName.toString(),
4650
});
4751
}
4852
return result;
@@ -57,6 +61,7 @@ void main() {
5761
expectLegacy: false,
5862
messageId: message.id,
5963
applyMarkdown: true,
64+
allowEmptyTopicName: true,
6065
);
6166
check(result).isNotNull().jsonEquals(message);
6267
});
@@ -71,6 +76,7 @@ void main() {
7176
expectLegacy: false,
7277
messageId: message.id,
7378
applyMarkdown: true,
79+
allowEmptyTopicName: true,
7480
);
7581
check(result).isNull();
7682
});
@@ -92,6 +98,7 @@ void main() {
9298
expectLegacy: true,
9399
messageId: message.id,
94100
applyMarkdown: true,
101+
allowEmptyTopicName: true,
95102
);
96103
check(result).isNotNull().jsonEquals(message);
97104
});
@@ -113,6 +120,7 @@ void main() {
113120
expectLegacy: true,
114121
messageId: message.id,
115122
applyMarkdown: true,
123+
allowEmptyTopicName: true,
116124
);
117125
check(result).isNull();
118126
});
@@ -124,11 +132,13 @@ void main() {
124132
FakeApiConnection connection, {
125133
required int messageId,
126134
bool? applyMarkdown,
135+
required bool allowEmptyTopicName,
127136
required Map<String, String> expected,
128137
}) async {
129138
final result = await getMessage(connection,
130139
messageId: messageId,
131140
applyMarkdown: applyMarkdown,
141+
allowEmptyTopicName: allowEmptyTopicName,
132142
);
133143
check(connection.lastRequest).isA<http.Request>()
134144
..method.equals('GET')
@@ -145,7 +155,11 @@ void main() {
145155
await checkGetMessage(connection,
146156
messageId: 1,
147157
applyMarkdown: true,
148-
expected: {'apply_markdown': 'true'});
158+
allowEmptyTopicName: true,
159+
expected: {
160+
'apply_markdown': 'true',
161+
'allow_empty_topic_name': 'true',
162+
});
149163
});
150164
});
151165

@@ -155,7 +169,21 @@ void main() {
155169
await checkGetMessage(connection,
156170
messageId: 1,
157171
applyMarkdown: false,
158-
expected: {'apply_markdown': 'false'});
172+
allowEmptyTopicName: true,
173+
expected: {
174+
'apply_markdown': 'false',
175+
'allow_empty_topic_name': 'true',
176+
});
177+
});
178+
});
179+
180+
test('allow empty topic name', () {
181+
return FakeApiConnection.with_((connection) async {
182+
connection.prepare(json: fakeResult.toJson());
183+
await checkGetMessage(connection,
184+
messageId: 1,
185+
allowEmptyTopicName: true,
186+
expected: {'allow_empty_topic_name': 'true'});
159187
});
160188
});
161189

@@ -164,6 +192,7 @@ void main() {
164192
connection.prepare(json: fakeResult.toJson());
165193
check(() => getMessage(connection,
166194
messageId: 1,
195+
allowEmptyTopicName: true,
167196
)).throws<AssertionError>();
168197
});
169198
});
@@ -255,12 +284,14 @@ void main() {
255284
required int numAfter,
256285
bool? clientGravatar,
257286
bool? applyMarkdown,
287+
required bool allowEmptyTopicName,
258288
required Map<String, String> expected,
259289
}) async {
260290
final result = await getMessages(connection,
261291
narrow: narrow, anchor: anchor, includeAnchor: includeAnchor,
262292
numBefore: numBefore, numAfter: numAfter,
263293
clientGravatar: clientGravatar, applyMarkdown: applyMarkdown,
294+
allowEmptyTopicName: allowEmptyTopicName,
264295
);
265296
check(connection.lastRequest).isA<http.Request>()
266297
..method.equals('GET')
@@ -279,11 +310,13 @@ void main() {
279310
await checkGetMessages(connection,
280311
narrow: const CombinedFeedNarrow().apiEncode(),
281312
anchor: AnchorCode.newest, numBefore: 10, numAfter: 20,
313+
allowEmptyTopicName: true,
282314
expected: {
283315
'narrow': jsonEncode([]),
284316
'anchor': 'newest',
285317
'num_before': '10',
286318
'num_after': '20',
319+
'allow_empty_topic_name': 'true',
287320
});
288321
});
289322
});
@@ -294,13 +327,15 @@ void main() {
294327
await checkGetMessages(connection,
295328
narrow: [ApiNarrowDm([123, 234])],
296329
anchor: AnchorCode.newest, numBefore: 10, numAfter: 20,
330+
allowEmptyTopicName: true,
297331
expected: {
298332
'narrow': jsonEncode([
299333
{'operator': 'pm-with', 'operand': [123, 234]},
300334
]),
301335
'anchor': 'newest',
302336
'num_before': '10',
303337
'num_after': '20',
338+
'allow_empty_topic_name': 'true',
304339
});
305340
});
306341
});
@@ -312,11 +347,13 @@ void main() {
312347
narrow: const CombinedFeedNarrow().apiEncode(),
313348
anchor: const NumericAnchor(42),
314349
numBefore: 10, numAfter: 20,
350+
allowEmptyTopicName: true,
315351
expected: {
316352
'narrow': jsonEncode([]),
317353
'anchor': '42',
318354
'num_before': '10',
319355
'num_after': '20',
356+
'allow_empty_topic_name': 'true',
320357
});
321358
});
322359
});

test/model/autocomplete_test.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:convert';
33

44
import 'package:checks/checks.dart';
55
import 'package:flutter/widgets.dart';
6+
import 'package:http/http.dart' as http;
67
import 'package:test/scaffolding.dart';
78
import 'package:zulip/api/model/initial_snapshot.dart';
89
import 'package:zulip/api/model/model.dart';
@@ -19,6 +20,7 @@ import 'package:zulip/widgets/compose_box.dart';
1920
import '../api/fake_api.dart';
2021
import '../example_data.dart' as eg;
2122
import '../fake_async.dart';
23+
import '../stdlib_checks.dart';
2224
import 'test_store.dart';
2325
import 'autocomplete_checks.dart';
2426

@@ -1026,6 +1028,21 @@ void main() {
10261028
check(done).isTrue();
10271029
});
10281030

1031+
test('TopicAutocompleteView getStreamTopics request', () async {
1032+
final store = eg.store();
1033+
final connection = store.connection as FakeApiConnection;
1034+
1035+
connection.prepare(json: GetStreamTopicsResult(
1036+
topics: [eg.getStreamTopicsEntry(name: '')],
1037+
).toJson());
1038+
TopicAutocompleteView.init(store: store, streamId: 1000,
1039+
query: TopicAutocompleteQuery('foo'));
1040+
check(connection.lastRequest).isA<http.Request>()
1041+
..method.equals('GET')
1042+
..url.path.equals('/api/v1/users/me/1000/topics')
1043+
..url.queryParameters['allow_empty_topic_name'].equals('true');
1044+
});
1045+
10291046
group('TopicAutocompleteQuery.testTopic', () {
10301047
final store = eg.store();
10311048
void doCheck(String rawQuery, String topic, bool expected) {

test/model/message_list_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ void main() {
8282
bool? includeAnchor,
8383
required int numBefore,
8484
required int numAfter,
85+
required bool allowEmptyTopicName,
8586
}) {
8687
check(connection.lastRequest).isA<http.Request>()
8788
..method.equals('GET')
@@ -92,6 +93,7 @@ void main() {
9293
if (includeAnchor != null) 'include_anchor': includeAnchor.toString(),
9394
'num_before': numBefore.toString(),
9495
'num_after': numAfter.toString(),
96+
'allow_empty_topic_name': allowEmptyTopicName.toString(),
9597
});
9698
}
9799

@@ -126,6 +128,7 @@ void main() {
126128
anchor: 'newest',
127129
numBefore: kMessageListFetchBatchSize,
128130
numAfter: 0,
131+
allowEmptyTopicName: true,
129132
);
130133
}
131134

@@ -238,6 +241,7 @@ void main() {
238241
includeAnchor: false,
239242
numBefore: kMessageListFetchBatchSize,
240243
numAfter: 0,
244+
allowEmptyTopicName: true,
241245
);
242246
});
243247

test/widgets/action_sheet_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,18 @@ void main() {
11551155
await setupToMessageActionSheet(tester, message: message, narrow: const StarredMessagesNarrow());
11561156
check(findQuoteAndReplyButton(tester)).isNull();
11571157
});
1158+
1159+
testWidgets('handle empty topic', (tester) async {
1160+
final message = eg.streamMessage();
1161+
await setupToMessageActionSheet(tester,
1162+
message: message, narrow: TopicNarrow.ofMessage(message));
1163+
1164+
prepareRawContentResponseSuccess(message: message, rawContent: 'Hello world');
1165+
await tapQuoteAndReplyButton(tester);
1166+
check(connection.lastRequest).isA<http.Request>()
1167+
.url.queryParameters['allow_empty_topic_name'].equals('true');
1168+
await tester.pump(Duration.zero);
1169+
});
11581170
});
11591171

11601172
group('MarkAsUnread', () {

test/widgets/message_list_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ void main() {
331331
'anchor': AnchorCode.newest.toJson(),
332332
'num_before': kMessageListFetchBatchSize.toString(),
333333
'num_after': '0',
334+
'allow_empty_topic_name': 'true',
334335
});
335336
});
336337

@@ -363,6 +364,7 @@ void main() {
363364
'anchor': AnchorCode.newest.toJson(),
364365
'num_before': kMessageListFetchBatchSize.toString(),
365366
'num_after': '0',
367+
'allow_empty_topic_name': 'true',
366368
});
367369
});
368370
});

0 commit comments

Comments
 (0)