Skip to content

Commit 1910318

Browse files
committed
msglist: Apply stream and topic muting to MessageListView
This is the last step for implementing stream and topic muting, i.e. zulip#346. (Except for what's been split off to follow-up issues, notably zulip#421.) Fixes: zulip#346
1 parent cb4e0e1 commit 1910318

File tree

5 files changed

+198
-6
lines changed

5 files changed

+198
-6
lines changed

lib/model/message_list.dart

+30-3
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,31 @@ class MessageListView with ChangeNotifier, _MessageSequence {
307307
final PerAccountStore store;
308308
final Narrow narrow;
309309

310+
/// Whether [message] should actually appear in this message list,
311+
/// given that it does belong to the narrow.
312+
///
313+
/// This depends in particular on whether the message is muted in
314+
/// one way or another.
315+
bool _messageVisible(Message message) {
316+
switch (narrow) {
317+
case AllMessagesNarrow():
318+
return switch (message) {
319+
StreamMessage() =>
320+
store.isTopicVisibleAsReceived(message.streamId, message.subject),
321+
DmMessage() => true,
322+
};
323+
324+
case StreamNarrow(:final streamId):
325+
assert(message is StreamMessage && message.streamId == streamId);
326+
if (message is! StreamMessage) return false;
327+
return store.isTopicVisibleInStream(streamId, message.subject);
328+
329+
case TopicNarrow():
330+
case DmNarrow():
331+
return true;
332+
}
333+
}
334+
310335
/// Fetch messages, starting from scratch.
311336
Future<void> fetchInitial() async {
312337
// TODO(#80): fetch from anchor firstUnread, instead of newest
@@ -321,7 +346,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
321346
numAfter: 0,
322347
);
323348
for (final message in result.messages) {
324-
_addMessage(message);
349+
if (_messageVisible(message)) {
350+
_addMessage(message);
351+
}
325352
}
326353
_fetched = true;
327354
_haveOldest = result.foundOldest;
@@ -353,7 +380,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
353380
result.messages.removeLast();
354381
}
355382

356-
_insertAllMessages(0, result.messages);
383+
_insertAllMessages(0, result.messages.where(_messageVisible));
357384
_haveOldest = result.foundOldest;
358385
} finally {
359386
_fetchingOlder = false;
@@ -366,7 +393,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
366393
///
367394
/// Called in particular when we get a [MessageEvent].
368395
void maybeAddMessage(Message message) {
369-
if (!narrow.containsMessage(message)) {
396+
if (!narrow.containsMessage(message) || !_messageVisible(message)) {
370397
return;
371398
}
372399
if (!_fetched) {

lib/model/narrow.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ sealed class Narrow {
1111
/// This const constructor allows subclasses to have const constructors.
1212
const Narrow();
1313

14-
// TODO implement muting; will need containsMessage to take more params
15-
// This means stream muting, topic un/muting, and user muting.
14+
/// Whether this message satisfies the filters of this narrow.
15+
///
16+
/// This is true just when the server would be expected to include the message
17+
/// in a [getMessages] request for this narrow, given appropriate anchor etc.
18+
///
19+
/// This does not necessarily mean the message list would show this message
20+
/// when navigated to this narrow; in particular it does not address the
21+
/// question of whether the stream or topic, or the sending user, is muted.
1622
bool containsMessage(Message message);
1723

1824
/// This narrow, expressed as an [ApiNarrow].

lib/model/stream.dart

+2
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ class StreamStoreImpl with StreamStore {
219219
case SubscriptionProperty.color:
220220
subscription.color = event.value as int;
221221
case SubscriptionProperty.isMuted:
222+
// TODO(#421) update [MessageListView] if affected
222223
subscription.isMuted = event.value as bool;
223224
case SubscriptionProperty.inHomeView:
224225
subscription.isMuted = !(event.value as bool);
@@ -250,6 +251,7 @@ class StreamStoreImpl with StreamStore {
250251
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {
251252
visibilityPolicy = UserTopicVisibilityPolicy.none;
252253
}
254+
// TODO(#421) update [MessageListView] if affected
253255
if (visibilityPolicy == UserTopicVisibilityPolicy.none) {
254256
// This is the "zero value" for this type, which our data structure
255257
// represents by leaving the topic out entirely.

test/api/model/model_checks.dart

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extension StreamColorSwatchChecks on Subject<StreamColorSwatch> {
1616
}
1717

1818
extension MessageChecks on Subject<Message> {
19+
Subject<int> get id => has((e) => e.id, 'id');
1920
Subject<String> get content => has((e) => e.content, 'content');
2021
Subject<bool> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
2122
Subject<int?> get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp');

test/model/message_list_test.dart

+157-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import '../api/model/model_checks.dart';
1717
import '../example_data.dart' as eg;
1818
import '../stdlib_checks.dart';
1919
import 'content_checks.dart';
20+
import 'test_store.dart';
2021

2122
void main() async {
2223
// These variables are the common state operated on by each test.
2324
// Each test case calls [prepare] to initialize them.
25+
late Subscription subscription;
2426
late PerAccountStore store;
2527
late FakeApiConnection connection;
2628
late MessageListView model;
@@ -35,7 +37,10 @@ void main() async {
3537

3638
/// Initialize [model] and the rest of the test state.
3739
void prepare({Narrow narrow = const AllMessagesNarrow()}) {
38-
store = eg.store();
40+
final stream = eg.stream();
41+
subscription = eg.subscription(stream);
42+
store = eg.store()
43+
..addStream(stream)..addSubscription(subscription);
3944
connection = store.connection as FakeApiConnection;
4045
notifiedCount = 0;
4146
model = MessageListView.init(store: store, narrow: narrow)
@@ -548,6 +553,141 @@ void main() async {
548553
check(model.contents[0]).equalsNode(correctContent);
549554
});
550555

556+
group('stream/topic muting', () {
557+
test('in AllMessagesNarrow', () async {
558+
final stream1 = eg.stream(streamId: 1, name: 'stream 1');
559+
final stream2 = eg.stream(streamId: 2, name: 'stream 2');
560+
prepare(narrow: const AllMessagesNarrow());
561+
store.addStreams([stream1, stream2]);
562+
store.addSubscription(eg.subscription(stream1));
563+
store.addUserTopic(stream1, 'B', UserTopicVisibilityPolicy.muted);
564+
store.addSubscription(eg.subscription(stream2, isMuted: true));
565+
store.addUserTopic(stream2, 'C', UserTopicVisibilityPolicy.unmuted);
566+
567+
// Check filtering on fetchInitial…
568+
await prepareMessages(foundOldest: false, messages: [
569+
eg.streamMessage(id: 201, stream: stream1, topic: 'A'),
570+
eg.streamMessage(id: 202, stream: stream1, topic: 'B'),
571+
eg.streamMessage(id: 203, stream: stream2, topic: 'C'),
572+
eg.streamMessage(id: 204, stream: stream2, topic: 'D'),
573+
eg.dmMessage( id: 205, from: eg.otherUser, to: [eg.selfUser]),
574+
]);
575+
final expected = <int>[];
576+
check(model.messages.map((m) => m.id))
577+
.deepEquals(expected..addAll([201, 203, 205]));
578+
579+
// … and on fetchOlder…
580+
connection.prepare(json: olderResult(
581+
anchor: 201, foundOldest: true, messages: [
582+
eg.streamMessage(id: 101, stream: stream1, topic: 'A'),
583+
eg.streamMessage(id: 102, stream: stream1, topic: 'B'),
584+
eg.streamMessage(id: 103, stream: stream2, topic: 'C'),
585+
eg.streamMessage(id: 104, stream: stream2, topic: 'D'),
586+
eg.dmMessage( id: 105, from: eg.otherUser, to: [eg.selfUser]),
587+
]).toJson());
588+
await model.fetchOlder();
589+
checkNotified(count: 2);
590+
check(model.messages.map((m) => m.id))
591+
.deepEquals(expected..insertAll(0, [101, 103, 105]));
592+
593+
// … and on maybeAddMessage.
594+
model.maybeAddMessage(eg.streamMessage(id: 301, stream: stream1, topic: 'A'));
595+
checkNotifiedOnce();
596+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301));
597+
598+
model.maybeAddMessage(eg.streamMessage(id: 302, stream: stream1, topic: 'B'));
599+
checkNotNotified();
600+
check(model.messages.map((m) => m.id)).deepEquals(expected);
601+
602+
model.maybeAddMessage(eg.streamMessage(id: 303, stream: stream2, topic: 'C'));
603+
checkNotifiedOnce();
604+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(303));
605+
606+
model.maybeAddMessage(eg.streamMessage(id: 304, stream: stream2, topic: 'D'));
607+
checkNotNotified();
608+
check(model.messages.map((m) => m.id)).deepEquals(expected);
609+
610+
model.maybeAddMessage(eg.dmMessage(id: 305, from: eg.otherUser, to: [eg.selfUser]));
611+
checkNotifiedOnce();
612+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(305));
613+
});
614+
615+
test('in StreamNarrow', () async {
616+
final stream = eg.stream(streamId: 1, name: 'stream 1');
617+
prepare(narrow: StreamNarrow(stream.streamId));
618+
store.addStream(stream);
619+
store.addSubscription(eg.subscription(stream, isMuted: true));
620+
store.addUserTopic(stream, 'A', UserTopicVisibilityPolicy.unmuted);
621+
store.addUserTopic(stream, 'C', UserTopicVisibilityPolicy.muted);
622+
623+
// Check filtering on fetchInitial…
624+
await prepareMessages(foundOldest: false, messages: [
625+
eg.streamMessage(id: 201, stream: stream, topic: 'A'),
626+
eg.streamMessage(id: 202, stream: stream, topic: 'B'),
627+
eg.streamMessage(id: 203, stream: stream, topic: 'C'),
628+
]);
629+
final expected = <int>[];
630+
check(model.messages.map((m) => m.id))
631+
.deepEquals(expected..addAll([201, 202]));
632+
633+
// … and on fetchOlder…
634+
connection.prepare(json: olderResult(
635+
anchor: 201, foundOldest: true, messages: [
636+
eg.streamMessage(id: 101, stream: stream, topic: 'A'),
637+
eg.streamMessage(id: 102, stream: stream, topic: 'B'),
638+
eg.streamMessage(id: 103, stream: stream, topic: 'C'),
639+
]).toJson());
640+
await model.fetchOlder();
641+
checkNotified(count: 2);
642+
check(model.messages.map((m) => m.id))
643+
.deepEquals(expected..insertAll(0, [101, 102]));
644+
645+
// … and on maybeAddMessage.
646+
model.maybeAddMessage(eg.streamMessage(id: 301, stream: stream, topic: 'A'));
647+
checkNotifiedOnce();
648+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301));
649+
650+
model.maybeAddMessage(eg.streamMessage(id: 302, stream: stream, topic: 'B'));
651+
checkNotifiedOnce();
652+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(302));
653+
654+
model.maybeAddMessage(eg.streamMessage(id: 303, stream: stream, topic: 'C'));
655+
checkNotNotified();
656+
check(model.messages.map((m) => m.id)).deepEquals(expected);
657+
});
658+
659+
test('in TopicNarrow', () async {
660+
final stream = eg.stream(streamId: 1, name: 'stream 1');
661+
prepare(narrow: TopicNarrow(stream.streamId, 'A'));
662+
store.addStream(stream);
663+
store.addSubscription(eg.subscription(stream, isMuted: true));
664+
store.addUserTopic(stream, 'A', UserTopicVisibilityPolicy.muted);
665+
666+
// Check filtering on fetchInitial…
667+
await prepareMessages(foundOldest: false, messages: [
668+
eg.streamMessage(id: 201, stream: stream, topic: 'A'),
669+
]);
670+
final expected = <int>[];
671+
check(model.messages.map((m) => m.id))
672+
.deepEquals(expected..addAll([201]));
673+
674+
// … and on fetchOlder…
675+
connection.prepare(json: olderResult(
676+
anchor: 201, foundOldest: true, messages: [
677+
eg.streamMessage(id: 101, stream: stream, topic: 'A'),
678+
]).toJson());
679+
await model.fetchOlder();
680+
checkNotified(count: 2);
681+
check(model.messages.map((m) => m.id))
682+
.deepEquals(expected..insertAll(0, [101]));
683+
684+
// … and on maybeAddMessage.
685+
model.maybeAddMessage(eg.streamMessage(id: 301, stream: stream, topic: 'A'));
686+
checkNotifiedOnce();
687+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301));
688+
});
689+
});
690+
551691
test('recipient headers are maintained consistently', () async {
552692
// This tests the code that maintains the invariant that recipient headers
553693
// are present just where [canShareRecipientHeader] requires them.
@@ -772,6 +912,22 @@ void checkInvariants(MessageListView model) {
772912
check(model).fetchingOlder.isFalse();
773913
}
774914

915+
for (final message in model.messages) {
916+
check(model.narrow.containsMessage(message)).isTrue();
917+
918+
if (message is! StreamMessage) continue;
919+
switch (model.narrow) {
920+
case AllMessagesNarrow():
921+
check(model.store.isTopicVisibleAsReceived(message.streamId, message.subject))
922+
.isTrue();
923+
case StreamNarrow():
924+
check(model.store.isTopicVisibleInStream(message.streamId, message.subject))
925+
.isTrue();
926+
case TopicNarrow():
927+
case DmNarrow():
928+
}
929+
}
930+
775931
for (int i = 0; i < model.messages.length - 1; i++) {
776932
check(model.messages[i].id).isLessThan(model.messages[i+1].id);
777933
}

0 commit comments

Comments
 (0)