Skip to content

Commit a81e43a

Browse files
chrisbobbegnprice
authored andcommitted
model: Treat topics case-insensitively in topic-visibility
Worth noting some helpful feedback from Greg in review, about the additions to test/model/channel_test.dart: #1608 (comment) > I think my ideal version of these tests would have most of the new > checks in this file be separate test cases, rather than added to > existing tests. This also doesn't need a full matrix with the > other variables: for example, this check can be done with any one > of the non-`none` policies, rather than all three. > > These are fine, though. No need to rewrite them — we have > higher-priority things to do.
1 parent cf57281 commit a81e43a

File tree

3 files changed

+86
-9
lines changed

3 files changed

+86
-9
lines changed

lib/model/channel.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ mixin ChannelStore {
4343
///
4444
/// For policies directly applicable in the UI, see
4545
/// [isTopicVisibleInStream] and [isTopicVisible].
46+
///
47+
/// Topics are treated case-insensitively; see [TopicName.isSameAs].
4648
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic);
4749

4850
/// The raw data structure underlying [topicVisibilityPolicy].
@@ -173,13 +175,13 @@ class ChannelStoreImpl with ChannelStore {
173175
streams.putIfAbsent(stream.streamId, () => stream);
174176
}
175177

176-
final topicVisibility = <int, Map<TopicName, UserTopicVisibilityPolicy>>{};
178+
final topicVisibility = <int, TopicKeyedMap<UserTopicVisibilityPolicy>>{};
177179
for (final item in initialSnapshot.userTopics ?? const <UserTopicItem>[]) {
178180
if (_warnInvalidVisibilityPolicy(item.visibilityPolicy)) {
179181
// Not a value we expect. Keep it out of our data structures. // TODO(log)
180182
continue;
181183
}
182-
final forStream = topicVisibility.putIfAbsent(item.streamId, () => {});
184+
final forStream = topicVisibility.putIfAbsent(item.streamId, () => makeTopicKeyedMap());
183185
forStream[item.topicName] = item.visibilityPolicy;
184186
}
185187

@@ -206,9 +208,9 @@ class ChannelStoreImpl with ChannelStore {
206208
final Map<int, Subscription> subscriptions;
207209

208210
@override
209-
Map<int, Map<TopicName, UserTopicVisibilityPolicy>> get debugTopicVisibility => topicVisibility;
211+
Map<int, TopicKeyedMap<UserTopicVisibilityPolicy>> get debugTopicVisibility => topicVisibility;
210212

211-
final Map<int, Map<TopicName, UserTopicVisibilityPolicy>> topicVisibility;
213+
final Map<int, TopicKeyedMap<UserTopicVisibilityPolicy>> topicVisibility;
212214

213215
@override
214216
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic) {
@@ -366,7 +368,7 @@ class ChannelStoreImpl with ChannelStore {
366368
topicVisibility.remove(event.streamId);
367369
}
368370
} else {
369-
final forStream = topicVisibility.putIfAbsent(event.streamId, () => {});
371+
final forStream = topicVisibility.putIfAbsent(event.streamId, () => makeTopicKeyedMap());
370372
forStream[event.topicName] = visibilityPolicy;
371373
}
372374
}

test/model/channel_test.dart

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import 'package:checks/checks.dart';
32
import 'package:test/scaffolding.dart';
43
import 'package:zulip/api/model/events.dart';
@@ -163,6 +162,10 @@ void main() {
163162
await store.setUserTopic(stream1, 'topic', policy);
164163
check(store.topicVisibilityPolicy(stream1.streamId, eg.t('topic')))
165164
.equals(policy);
165+
166+
// Case-insensitive
167+
check(store.topicVisibilityPolicy(stream1.streamId, eg.t('ToPiC')))
168+
.equals(policy);
166169
}
167170
});
168171
});
@@ -198,6 +201,10 @@ void main() {
198201
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.muted);
199202
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('topic'))).isFalse();
200203
check(store.isTopicVisible (stream1.streamId, eg.t('topic'))).isFalse();
204+
205+
// Case-insensitive
206+
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('ToPiC'))).isFalse();
207+
check(store.isTopicVisible (stream1.streamId, eg.t('ToPiC'))).isFalse();
201208
});
202209

203210
test('with policy unmuted', () async {
@@ -207,6 +214,10 @@ void main() {
207214
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.unmuted);
208215
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('topic'))).isTrue();
209216
check(store.isTopicVisible (stream1.streamId, eg.t('topic'))).isTrue();
217+
218+
// Case-insensitive
219+
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('tOpIc'))).isTrue();
220+
check(store.isTopicVisible (stream1.streamId, eg.t('tOpIc'))).isTrue();
210221
});
211222

212223
test('with policy followed', () async {
@@ -216,20 +227,40 @@ void main() {
216227
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.followed);
217228
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('topic'))).isTrue();
218229
check(store.isTopicVisible (stream1.streamId, eg.t('topic'))).isTrue();
230+
231+
// Case-insensitive
232+
check(store.isTopicVisibleInStream(stream1.streamId, eg.t('TOPIC'))).isTrue();
233+
check(store.isTopicVisible (stream1.streamId, eg.t('TOPIC'))).isTrue();
219234
});
220235
});
221236

222237
group('willChangeIfTopicVisible/InStream', () {
223238
UserTopicEvent mkEvent(UserTopicVisibilityPolicy policy) =>
224239
eg.userTopicEvent(stream1.streamId, 'topic', policy);
225240

241+
// For testing case-insensitivity
242+
UserTopicEvent mkEventDifferentlyCased(UserTopicVisibilityPolicy policy) =>
243+
eg.userTopicEvent(stream1.streamId, 'ToPiC', policy);
244+
245+
assert(() {
246+
// (sanity check on mkEvent and mkEventDifferentlyCased)
247+
final event1 = mkEvent(UserTopicVisibilityPolicy.followed);
248+
final event2 = mkEventDifferentlyCased(UserTopicVisibilityPolicy.followed);
249+
return event1.topicName.isSameAs(event2.topicName)
250+
&& event1.topicName.apiName != event2.topicName.apiName;
251+
}());
252+
226253
void checkChanges(PerAccountStore store,
227254
UserTopicVisibilityPolicy newPolicy,
228255
UserTopicVisibilityEffect expectedInStream,
229256
UserTopicVisibilityEffect expectedOverall) {
230257
final event = mkEvent(newPolicy);
231258
check(store.willChangeIfTopicVisibleInStream(event)).equals(expectedInStream);
232259
check(store.willChangeIfTopicVisible (event)).equals(expectedOverall);
260+
261+
final event2 = mkEventDifferentlyCased(newPolicy);
262+
check(store.willChangeIfTopicVisibleInStream(event2)).equals(expectedInStream);
263+
check(store.willChangeIfTopicVisible (event2)).equals(expectedOverall);
233264
}
234265

235266
test('stream not muted, policy none -> followed, no change', () async {
@@ -366,6 +397,12 @@ void main() {
366397
compareTopicVisibility(store, [
367398
eg.userTopicItem(stream1, 'topic', UserTopicVisibilityPolicy.unmuted),
368399
]);
400+
401+
// case-insensitivity
402+
await store.setUserTopic(stream1, 'ToPiC', UserTopicVisibilityPolicy.followed);
403+
compareTopicVisibility(store, [
404+
eg.userTopicItem(stream1, 'topic', UserTopicVisibilityPolicy.followed),
405+
]);
369406
});
370407

371408
test('remove, with others in stream', () async {
@@ -381,15 +418,17 @@ void main() {
381418
test('remove, as last in stream', () async {
382419
final store = eg.store();
383420
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.muted);
384-
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.none);
421+
// case-insensitivity
422+
await store.setUserTopic(stream1, 'ToPiC', UserTopicVisibilityPolicy.none);
385423
compareTopicVisibility(store, [
386424
]);
387425
});
388426

389427
test('treat unknown enum value as removing', () async {
390428
final store = eg.store();
391429
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.muted);
392-
await store.setUserTopic(stream1, 'topic', UserTopicVisibilityPolicy.unknown);
430+
// case-insensitivity
431+
await store.setUserTopic(stream1, 'ToPiC', UserTopicVisibilityPolicy.unknown);
393432
compareTopicVisibility(store, [
394433
]);
395434
});
@@ -406,7 +445,8 @@ void main() {
406445
]));
407446
check(store.topicVisibilityPolicy(stream.streamId, eg.t('topic 1')))
408447
.equals(UserTopicVisibilityPolicy.muted);
409-
check(store.topicVisibilityPolicy(stream.streamId, eg.t('topic 2')))
448+
// case-insensitivity
449+
check(store.topicVisibilityPolicy(stream.streamId, eg.t('ToPiC 2')))
410450
.equals(UserTopicVisibilityPolicy.unmuted);
411451
check(store.topicVisibilityPolicy(stream.streamId, eg.t('topic 3')))
412452
.equals(UserTopicVisibilityPolicy.followed);

test/widgets/inbox_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:zulip/widgets/color.dart';
99
import 'package:zulip/widgets/home.dart';
1010
import 'package:zulip/widgets/icons.dart';
1111
import 'package:zulip/widgets/channel_colors.dart';
12+
import 'package:zulip/widgets/unread_count_badge.dart';
1213

1314
import '../example_data.dart' as eg;
1415
import '../flutter_checks.dart';
@@ -58,6 +59,7 @@ void main() {
5859
List<Subscription>? subscriptions,
5960
List<User>? users,
6061
required List<Message> unreadMessages,
62+
List<Message>? otherMessages,
6163
NavigatorObserver? navigatorObserver,
6264
}) async {
6365
addTearDown(testBinding.reset);
@@ -362,6 +364,39 @@ void main() {
362364
parent: findRowByLabel(tester, topic),
363365
icon: ZulipIcons.unmute)).isTrue();
364366
});
367+
368+
testWidgets('unmuted (topics treated case-insensitively)', (tester) async {
369+
// Case-insensitivity of both topic-visibility and unreads data
370+
// TODO(#1065) this belongs in test/model/ once the inbox page has
371+
// its own view-model
372+
373+
final message1 = eg.streamMessage(stream: channel, topic: 'aaa');
374+
final message2 = eg.streamMessage(stream: channel, topic: 'AaA', flags: [MessageFlag.read]);
375+
final message3 = eg.streamMessage(stream: channel, topic: 'aAa', flags: [MessageFlag.read]);
376+
await setupPage(tester,
377+
users: [eg.selfUser, eg.otherUser],
378+
streams: [channel],
379+
subscriptions: [eg.subscription(channel, isMuted: true)],
380+
unreadMessages: [message1]);
381+
await store.setUserTopic(channel, 'aaa', UserTopicVisibilityPolicy.unmuted);
382+
await tester.pump();
383+
384+
check(find.descendant(
385+
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
386+
matching: find.widgetWithText(UnreadCountBadge, '1'))).findsOne();
387+
388+
await store.handleEvent(eg.updateMessageFlagsRemoveEvent(MessageFlag.read, [message2]));
389+
await tester.pump();
390+
check(find.descendant(
391+
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
392+
matching: find.widgetWithText(UnreadCountBadge, '2'))).findsOne();
393+
394+
await store.handleEvent(eg.updateMessageFlagsRemoveEvent(MessageFlag.read, [message3]));
395+
await tester.pump();
396+
check(find.descendant(
397+
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
398+
matching: find.widgetWithText(UnreadCountBadge, '3'))).findsOne();
399+
});
365400
});
366401

367402
group('collapsing', () {

0 commit comments

Comments
 (0)