Skip to content

Commit 2fa4866

Browse files
committed
subscription_list: Show a dot for unreads if channel is muted
Fixes: zulip#712
1 parent a9fdb97 commit 2fa4866

File tree

5 files changed

+111
-5
lines changed

5 files changed

+111
-5
lines changed

lib/model/unreads.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ 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+
return streams[streamId]?.entries.any((entry) =>
213+
!streamStore.isTopicVisible(streamId, entry.key) &&
214+
entry.value.isNotEmpty) ?? false;
215+
}
216+
209217
void handleMessageEvent(MessageEvent event) {
210218
final message = event.message;
211219
if (message.flags.contains(MessageFlag.read)) {

lib/widgets/subscription_list.dart

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,12 @@ 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+
final hasAllUnreadsMuted = !hasUnmutedUnreads && unreadsModel!.hasMutedInStream(subscription.streamId);
193+
return SubscriptionItem(subscription: subscription,
194+
unreadCount: unreadCount,
195+
hasUnmutedUnreads: hasUnmutedUnreads,
196+
hasAllUnreadsMuted: hasAllUnreadsMuted);
193197
});
194198
}
195199
}
@@ -200,15 +204,18 @@ class SubscriptionItem extends StatelessWidget {
200204
super.key,
201205
required this.subscription,
202206
required this.unreadCount,
207+
required this.hasAllUnreadsMuted,
208+
required this.hasUnmutedUnreads,
203209
});
204210

205211
final Subscription subscription;
206212
final int unreadCount;
213+
final bool hasAllUnreadsMuted;
214+
final bool hasUnmutedUnreads;
207215

208216
@override
209217
Widget build(BuildContext context) {
210218
final swatch = colorSwatchFor(context, subscription);
211-
final hasUnreads = (unreadCount > 0);
212219
final opacity = subscription.isMuted ? 0.55 : 1.0;
213220
return Material(
214221
// TODO(#95) need dark-theme color
@@ -243,11 +250,11 @@ class SubscriptionItem extends StatelessWidget {
243250
// TODO(#95) need dark-theme color
244251
color: Color(0xFF262626),
245252
).merge(weightVariableTextStyle(context,
246-
wght: hasUnreads && !subscription.isMuted ? 600 : null)),
253+
wght: hasUnmutedUnreads && !subscription.isMuted ? 600 : null)),
247254
maxLines: 1,
248255
overflow: TextOverflow.ellipsis,
249256
subscription.name)))),
250-
if (unreadCount > 0) ...[
257+
if (hasUnmutedUnreads) ...[
251258
const SizedBox(width: 12),
252259
// TODO(#747) show @-mention indicator when it applies
253260
Opacity(
@@ -256,6 +263,9 @@ class SubscriptionItem extends StatelessWidget {
256263
count: unreadCount,
257264
backgroundColor: swatch,
258265
bold: true)),
266+
] else if (hasAllUnreadsMuted && subscription.isMuted) ...[
267+
const SizedBox(width: 12),
268+
const MutedUnreadBadge(),
259269
],
260270
const SizedBox(width: 16),
261271
])));

lib/widgets/unread_count_badge.dart

Lines changed: 20 additions & 0 deletions
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

Lines changed: 25 additions & 0 deletions
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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,49 @@ 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: 'b', unreadMessageIds: [3]),
193+
]);
194+
await setupStreamListPage(tester,
195+
subscriptions: [eg.subscription(stream, isMuted: true)],
196+
userTopics: [eg.userTopicItem(stream, 'b', UserTopicVisibilityPolicy.muted)],
197+
unreadMsgs: unreadMsgs);
198+
final finder = find.byWidgetPredicate((widget) => widget is MutedUnreadBadge);
199+
check(finder.evaluate().length).equals(1);
200+
});
201+
202+
testWidgets('muted unread badge does not show with any unmuted streams', (tester) async {
203+
final stream = eg.stream();
204+
final unreadMsgs = eg.unreadMsgs(streams: [
205+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'b', unreadMessageIds: [3]),
206+
]);
207+
await setupStreamListPage(tester,
208+
subscriptions: [eg.subscription(stream, isMuted: false)],
209+
userTopics: [eg.userTopicItem(stream, 'b', UserTopicVisibilityPolicy.muted)],
210+
unreadMsgs: unreadMsgs);
211+
final finder = find.byWidgetPredicate((widget) => widget is MutedUnreadBadge);
212+
check(finder.evaluate().length).equals(0);
213+
});
214+
215+
testWidgets('muted unread badge does not show with any unmuted unreads', (tester) async {
216+
final stream = eg.stream();
217+
final unreadMsgs = eg.unreadMsgs(streams: [
218+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'a', unreadMessageIds: [1, 2]),
219+
UnreadStreamSnapshot(streamId: stream.streamId, topic: 'b', unreadMessageIds: [3]),
220+
]);
221+
await setupStreamListPage(tester,
222+
subscriptions: [eg.subscription(stream, isMuted: true)],
223+
userTopics: [
224+
eg.userTopicItem(stream, 'b', UserTopicVisibilityPolicy.muted),
225+
eg.userTopicItem(stream, 'a', UserTopicVisibilityPolicy.unmuted),
226+
],
227+
unreadMsgs: unreadMsgs);
228+
final finder = find.byWidgetPredicate((widget) => widget is MutedUnreadBadge);
229+
check(finder.evaluate().length).equals(0);
230+
});
231+
189232
testWidgets('color propagates to icon and badge', (tester) async {
190233
final stream = eg.stream();
191234
final unreadMsgs = eg.unreadMsgs(streams: [

0 commit comments

Comments
 (0)