Skip to content

Commit cc4dc20

Browse files
committed
subscription_list: Show a dot for unreads if channel is muted
Fixes: zulip#712
1 parent 2393a19 commit cc4dc20

File tree

5 files changed

+105
-5
lines changed

5 files changed

+105
-5
lines changed

lib/model/unreads.dart

+13
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ class Unreads extends ChangeNotifier {
206206
}
207207
}
208208

209+
/// Checks if stream contains strictly muted unreads,
210+
/// using [StreamStore.isTopicVisible].
211+
bool hasMutedInStream(int streamId) {
212+
final topics = streams[streamId];
213+
if (topics == null) return false;
214+
for (final entry in topics.entries) {
215+
if (!streamStore.isTopicVisible(streamId, entry.key)) {
216+
if (entry.value.isNotEmpty) return true;
217+
}
218+
}
219+
return false;
220+
}
221+
209222
void handleMessageEvent(MessageEvent event) {
210223
final message = event.message;
211224
if (message.flags.contains(MessageFlag.read)) {

lib/widgets/subscription_list.dart

+16-5
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,13 @@ class _SubscriptionList extends StatelessWidget {
188188
itemBuilder: (BuildContext context, int index) {
189189
final subscription = subscriptions[index];
190190
final unreadCount = unreadsModel!.countInStream(subscription.streamId);
191-
// TODO(#712): if stream muted, show a dot for unreads
192-
return SubscriptionItem(subscription: subscription, unreadCount: unreadCount);
191+
final hasUnmutedUnreads = unreadCount > 0;
192+
// There is no need to check for muted unreads if there unmuted ones
193+
final hasMutedUnreads = !hasUnmutedUnreads && unreadsModel!.hasMutedInStream(subscription.streamId);
194+
return SubscriptionItem(subscription: subscription,
195+
unreadCount: unreadCount,
196+
hasMutedUnreads: hasMutedUnreads,
197+
hasUnmutedUnreads: hasUnmutedUnreads);
193198
});
194199
}
195200
}
@@ -200,15 +205,18 @@ class SubscriptionItem extends StatelessWidget {
200205
super.key,
201206
required this.subscription,
202207
required this.unreadCount,
208+
required this.hasMutedUnreads,
209+
required this.hasUnmutedUnreads,
203210
});
204211

205212
final Subscription subscription;
206213
final int unreadCount;
214+
final bool hasMutedUnreads;
215+
final bool hasUnmutedUnreads;
207216

208217
@override
209218
Widget build(BuildContext context) {
210219
final swatch = colorSwatchFor(context, subscription);
211-
final hasUnreads = (unreadCount > 0);
212220
final opacity = subscription.isMuted ? 0.55 : 1.0;
213221
return Material(
214222
// TODO(#95) need dark-theme color
@@ -243,11 +251,11 @@ class SubscriptionItem extends StatelessWidget {
243251
// TODO(#95) need dark-theme color
244252
color: Color(0xFF262626),
245253
).merge(weightVariableTextStyle(context,
246-
wght: hasUnreads ? 600 : null)),
254+
wght: hasUnmutedUnreads ? 600 : null)),
247255
maxLines: 1,
248256
overflow: TextOverflow.ellipsis,
249257
subscription.name)))),
250-
if (unreadCount > 0) ...[
258+
if (hasUnmutedUnreads) ...[
251259
const SizedBox(width: 12),
252260
// TODO(#747) show @-mention indicator when it applies
253261
Opacity(
@@ -256,6 +264,9 @@ class SubscriptionItem extends StatelessWidget {
256264
count: unreadCount,
257265
backgroundColor: swatch,
258266
bold: true)),
267+
] else if (hasMutedUnreads) ...[
268+
const SizedBox(width: 12),
269+
const MutedUnreadBadge(),
259270
],
260271
const SizedBox(width: 16),
261272
])));

lib/widgets/unread_count_badge.dart

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import 'package:flutter/material.dart';
3+
import 'package:flutter_color_models/flutter_color_models.dart';
34

45
import 'stream_colors.dart';
56
import 'text.dart';
@@ -65,3 +66,22 @@ class UnreadCountBadge extends StatelessWidget {
6566
count.toString())));
6667
}
6768
}
69+
70+
class MutedUnreadBadge extends StatelessWidget {
71+
const MutedUnreadBadge({
72+
super.key,
73+
});
74+
75+
@override
76+
Widget build(BuildContext context) {
77+
return Opacity(
78+
opacity: 0.5,
79+
child: Container(
80+
width: 8,
81+
height: 8,
82+
margin: const EdgeInsetsDirectional.only(end: 3),
83+
decoration: const BoxDecoration(
84+
color: HslColor(0, 0, 80),
85+
shape: BoxShape.circle)));
86+
}
87+
}

test/model/unreads_test.dart

+25
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,31 @@ void main() {
217217
});
218218
});
219219

220+
group('muted helpers', () {
221+
test('hasMutedInStream', () async {
222+
final stream = eg.stream();
223+
prepare();
224+
await streamStore.addStream(stream);
225+
await streamStore.addSubscription(eg.subscription(stream));
226+
await streamStore.addUserTopic(stream, 'a', UserTopicVisibilityPolicy.unmuted);
227+
await streamStore.addUserTopic(stream, 'c', UserTopicVisibilityPolicy.unmuted);
228+
fillWithMessages([
229+
eg.streamMessage(stream: stream, topic: 'a', flags: []),
230+
eg.streamMessage(stream: stream, topic: 'a', flags: []),
231+
eg.streamMessage(stream: stream, topic: 'b', flags: []),
232+
eg.streamMessage(stream: stream, topic: 'b', flags: []),
233+
eg.streamMessage(stream: stream, topic: 'b', flags: []),
234+
eg.streamMessage(stream: stream, topic: 'c', flags: []),
235+
]);
236+
check(model.hasMutedInStream(stream.streamId)).equals(false);
237+
238+
await streamStore.handleEvent(SubscriptionUpdateEvent(id: 1,
239+
streamId: stream.streamId,
240+
property: SubscriptionProperty.isMuted, value: true));
241+
check(model.hasMutedInStream(stream.streamId)).equals(true);
242+
});
243+
});
244+
220245
group('handleMessageEvent', () {
221246
for (final (isUnread, isStream, isDirectMentioned, isWildcardMentioned) in [
222247
(true, true, true, true ),

test/widgets/subscription_list_test.dart

+31
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,37 @@ void main() {
186186
check(find.byType(UnreadCountBadge).evaluate()).length.equals(0);
187187
});
188188

189+
testWidgets('muted unread badge shows with muted unreads', (tester) async {
190+
final stream = eg.stream();
191+
final unreadMsgs = eg.unreadMsgs(streams: [
192+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'a', unreadMessageIds: [1, 2]),
193+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'b', unreadMessageIds: [3]),
194+
]);
195+
await setupStreamListPage(tester,
196+
subscriptions: [eg.subscription(stream, isMuted: true)],
197+
userTopics: [eg.userTopicItem(stream, 'b', UserTopicVisibilityPolicy.muted)],
198+
unreadMsgs: unreadMsgs);
199+
final finder = find.byWidgetPredicate((widget) => widget is MutedUnreadBadge);
200+
check(finder.evaluate().length).equals(1);
201+
});
202+
203+
testWidgets('muted unread badge does not show with any unmuted unreads', (tester) async {
204+
final stream = eg.stream();
205+
final unreadMsgs = eg.unreadMsgs(streams: [
206+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'a', unreadMessageIds: [1, 2]),
207+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'b', unreadMessageIds: [3]),
208+
]);
209+
await setupStreamListPage(tester,
210+
subscriptions: [eg.subscription(stream, isMuted: true)],
211+
userTopics: [
212+
eg.userTopicItem(stream, 'b', UserTopicVisibilityPolicy.muted),
213+
eg.userTopicItem(stream, 'a', UserTopicVisibilityPolicy.unmuted),
214+
],
215+
unreadMsgs: unreadMsgs);
216+
final finder = find.byWidgetPredicate((widget) => widget is MutedUnreadBadge);
217+
check(finder.evaluate().length).equals(0);
218+
});
219+
189220
testWidgets('color propagates to icon and badge', (tester) async {
190221
final stream = eg.stream();
191222
final unreadMsgs = eg.unreadMsgs(streams: [

0 commit comments

Comments
 (0)