Skip to content

Commit 0d03c8e

Browse files
committed
unread: Apply stream and topic muting to unread counts
1 parent 331d4ba commit 0d03c8e

File tree

5 files changed

+89
-15
lines changed

5 files changed

+89
-15
lines changed

lib/model/unreads.dart

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,34 +132,64 @@ class Unreads extends ChangeNotifier {
132132

133133
final int selfUserId;
134134

135-
// TODO(#346): account for muted topics and streams
136135
// TODO(#370): maintain this count incrementally, rather than recomputing from scratch
137136
int countInAllMessagesNarrow() {
138137
int c = 0;
139138
for (final messageIds in dms.values) {
140139
c = c + messageIds.length;
141140
}
142-
for (final topics in streams.values) {
143-
for (final messageIds in topics.values) {
144-
c = c + messageIds.length;
141+
for (final MapEntry(key: streamId, value: topics) in streams.entries) {
142+
for (final MapEntry(key: topic, value: messageIds) in topics.entries) {
143+
if (streamStore.isTopicVisible(streamId, topic)) {
144+
c = c + messageIds.length;
145+
}
146+
}
147+
}
148+
return c;
149+
}
150+
151+
/// The "strict" unread count for this stream,
152+
/// using [StreamStore.isTopicVisible].
153+
///
154+
/// If the stream is muted, this will count only topics that are
155+
/// actively unmuted.
156+
///
157+
/// For a count that's appropriate in UI contexts that are focused
158+
/// specifically on this stream, see [countInStreamNarrow].
159+
// TODO(#370): maintain this count incrementally, rather than recomputing from scratch
160+
int countInStream(int streamId) {
161+
final topics = streams[streamId];
162+
if (topics == null) return 0;
163+
int c = 0;
164+
for (final entry in topics.entries) {
165+
if (streamStore.isTopicVisible(streamId, entry.key)) {
166+
c = c + entry.value.length;
145167
}
146168
}
147169
return c;
148170
}
149171

150-
// TODO(#346): account for muted topics and streams
172+
/// The "broad" unread count for this stream,
173+
/// using [StreamStore.isTopicVisibleInStream].
174+
///
175+
/// This includes topics that have no visibility policy of their own,
176+
/// even if the stream itself is muted.
177+
///
178+
/// For a count that's appropriate in UI contexts that are not already
179+
/// focused on this stream, see [countInStream].
151180
// TODO(#370): maintain this count incrementally, rather than recomputing from scratch
152181
int countInStreamNarrow(int streamId) {
153182
final topics = streams[streamId];
154183
if (topics == null) return 0;
155184
int c = 0;
156-
for (final messageIds in topics.values) {
157-
c = c + messageIds.length;
185+
for (final entry in topics.entries) {
186+
if (streamStore.isTopicVisibleInStream(streamId, entry.key)) {
187+
c = c + entry.value.length;
188+
}
158189
}
159190
return c;
160191
}
161192

162-
// TODO(#346): account for muted topics and streams
163193
int countInTopicNarrow(int streamId, String topic) {
164194
final topics = streams[streamId];
165195
return topics?[topic]?.length ?? 0;

lib/widgets/subscription_list.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ class _SubscriptionList extends StatelessWidget {
176176
itemCount: subscriptions.length,
177177
itemBuilder: (BuildContext context, int index) {
178178
final subscription = subscriptions[index];
179-
final unreadCount = unreadsModel!.countInStreamNarrow(subscription.streamId);
179+
final unreadCount = unreadsModel!.countInStream(subscription.streamId);
180+
// TODO(#346): if stream muted, show a dot for unreads
180181
return SubscriptionItem(subscription: subscription, unreadCount: unreadCount);
181182
});
182183
}

test/model/unreads_test.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:zulip/model/store.dart';
99
import 'package:zulip/model/unreads.dart';
1010

1111
import '../example_data.dart' as eg;
12+
import 'test_store.dart';
1213
import 'unreads_checks.dart';
1314

1415
void main() {
@@ -152,30 +153,48 @@ void main() {
152153

153154
group('count helpers', () {
154155
test('countInAllMessagesNarrow', () {
155-
final stream1 = eg.stream();
156-
final stream2 = eg.stream();
156+
final stream1 = eg.stream(streamId: 1, name: 'stream 1');
157+
final stream2 = eg.stream(streamId: 2, name: 'stream 2');
158+
final stream3 = eg.stream(streamId: 3, name: 'stream 3');
157159
prepare();
160+
streamStore.addStreams([stream1, stream2, stream3]);
161+
streamStore.addSubscription(eg.subscription(stream1));
162+
streamStore.addSubscription(eg.subscription(stream2));
163+
streamStore.addSubscription(eg.subscription(stream3, isMuted: true));
164+
streamStore.addUserTopic(stream1, 'a', UserTopicVisibilityPolicy.muted);
158165
fillWithMessages([
159166
eg.streamMessage(stream: stream1, topic: 'a', flags: []),
160167
eg.streamMessage(stream: stream1, topic: 'b', flags: []),
161168
eg.streamMessage(stream: stream1, topic: 'b', flags: []),
162169
eg.streamMessage(stream: stream2, topic: 'c', flags: []),
170+
eg.streamMessage(stream: stream3, topic: 'd', flags: []),
163171
eg.dmMessage(from: eg.otherUser, to: [eg.selfUser], flags: []),
164172
eg.dmMessage(from: eg.thirdUser, to: [eg.selfUser], flags: []),
165173
]);
166-
check(model.countInAllMessagesNarrow()).equals(6);
174+
check(model.countInAllMessagesNarrow()).equals(5);
167175
});
168176

169-
test('countInStreamNarrow', () {
177+
test('countInStream/Narrow', () {
170178
final stream = eg.stream();
171179
prepare();
180+
streamStore.addStream(stream);
181+
streamStore.addSubscription(eg.subscription(stream));
182+
streamStore.addUserTopic(stream, 'a', UserTopicVisibilityPolicy.unmuted);
183+
streamStore.addUserTopic(stream, 'c', UserTopicVisibilityPolicy.muted);
172184
fillWithMessages([
173185
eg.streamMessage(stream: stream, topic: 'a', flags: []),
174186
eg.streamMessage(stream: stream, topic: 'a', flags: []),
175187
eg.streamMessage(stream: stream, topic: 'b', flags: []),
176188
eg.streamMessage(stream: stream, topic: 'b', flags: []),
177189
eg.streamMessage(stream: stream, topic: 'b', flags: []),
190+
eg.streamMessage(stream: stream, topic: 'c', flags: []),
178191
]);
192+
check(model.countInStream (stream.streamId)).equals(5);
193+
check(model.countInStreamNarrow(stream.streamId)).equals(5);
194+
195+
streamStore.handleEvent(SubscriptionUpdateEvent(id: 1, streamId: stream.streamId,
196+
property: SubscriptionProperty.isMuted, value: true));
197+
check(model.countInStream (stream.streamId)).equals(2);
179198
check(model.countInStreamNarrow(stream.streamId)).equals(5);
180199
});
181200

test/widgets/message_list_test.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ void main() {
4747
UnreadMessagesSnapshot? unreadMsgs,
4848
}) async {
4949
addTearDown(testBinding.reset);
50-
streams ??= subscriptions;
50+
streams ??= subscriptions ??= [eg.subscription(eg.stream())];
5151
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
5252
streams: streams, subscriptions: subscriptions, unreadMsgs: unreadMsgs));
5353
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
@@ -307,6 +307,7 @@ void main() {
307307
final stream = eg.stream(name: 'stream name');
308308
await setupMessageListPage(tester,
309309
narrow: const AllMessagesNarrow(),
310+
subscriptions: [],
310311
messages: [
311312
eg.streamMessage(stream: stream),
312313
]);
@@ -323,7 +324,7 @@ void main() {
323324
});
324325
await setupMessageListPage(tester,
325326
narrow: const AllMessagesNarrow(),
326-
streams: [streamAfter],
327+
subscriptions: [eg.subscription(streamAfter)],
327328
messages: [
328329
eg.streamMessage(stream: streamBefore),
329330
]);

test/widgets/subscription_list_test.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:zulip/widgets/store.dart';
77
import 'package:zulip/widgets/subscription_list.dart';
88
import 'package:zulip/widgets/unread_count_badge.dart';
99

10+
import '../flutter_checks.dart';
1011
import '../model/binding.dart';
1112
import '../example_data.dart' as eg;
1213

@@ -15,12 +16,14 @@ void main() {
1516

1617
Future<void> setupStreamListPage(WidgetTester tester, {
1718
required List<Subscription> subscriptions,
19+
List<UserTopicItem> userTopics = const [],
1820
UnreadMessagesSnapshot? unreadMsgs,
1921
}) async {
2022
addTearDown(testBinding.reset);
2123
final initialSnapshot = eg.initialSnapshot(
2224
subscriptions: subscriptions,
2325
streams: subscriptions.toList(),
26+
userTopics: userTopics,
2427
unreadMsgs: unreadMsgs,
2528
);
2629
await testBinding.globalStore.add(eg.selfAccount, initialSnapshot);
@@ -111,6 +114,26 @@ void main() {
111114
check(find.byType(UnreadCountBadge).evaluate()).length.equals(1);
112115
});
113116

117+
testWidgets('unread badge counts unmuted only', (tester) async {
118+
final stream = eg.stream();
119+
final unreadMsgs = eg.unreadMsgs(streams: [
120+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'a', unreadMessageIds: [1, 2]),
121+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'b', unreadMessageIds: [3]),
122+
]);
123+
await setupStreamListPage(tester,
124+
subscriptions: [eg.subscription(stream, isMuted: true)],
125+
userTopics: [UserTopicItem(
126+
streamId: stream.streamId,
127+
topicName: 'b',
128+
lastUpdated: 1234567890,
129+
visibilityPolicy: UserTopicVisibilityPolicy.unmuted,
130+
)],
131+
unreadMsgs: unreadMsgs);
132+
check(tester.widget<Text>(find.descendant(
133+
of: find.byType(UnreadCountBadge), matching: find.byType(Text))))
134+
.data.equals('1');
135+
});
136+
114137
testWidgets('unread badge does not show with no unreads', (tester) async {
115138
final stream = eg.stream();
116139
final unreadMsgs = eg.unreadMsgs(streams: []);

0 commit comments

Comments
 (0)