Skip to content

Commit 5d32321

Browse files
committed
home: Show starred-message count in main menu, subject to user setting
Fixes-partly: zulip#1088
1 parent eed56a2 commit 5d32321

File tree

11 files changed

+125
-57
lines changed

11 files changed

+125
-57
lines changed

lib/widgets/home.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,9 @@ class _InboxButton extends _NavigationBarMenuButton {
480480
final store = PerAccountStoreWidget.of(context);
481481
final unreadCount = store.unreads.countInCombinedFeedNarrow();
482482
if (unreadCount == 0) return null;
483-
return UnreadCountBadge(
484-
style: UnreadCountBadgeStyle.mainMenu,
483+
return Counter(
484+
kind: CounterKind.unread,
485+
style: CounterStyle.mainMenu,
485486
count: unreadCount,
486487
channelIdForBackground: null,
487488
);
@@ -507,8 +508,9 @@ class _MentionsButton extends MenuButton {
507508
final store = PerAccountStoreWidget.of(context);
508509
final unreadCount = store.unreads.countInMentionsNarrow();
509510
if (unreadCount == 0) return null;
510-
return UnreadCountBadge(
511-
style: UnreadCountBadgeStyle.mainMenu,
511+
return Counter(
512+
kind: CounterKind.unread,
513+
style: CounterStyle.mainMenu,
512514
count: unreadCount,
513515
channelIdForBackground: null,
514516
);
@@ -532,6 +534,18 @@ class _StarredMessagesButton extends MenuButton {
532534
return zulipLocalizations.starredMessagesPageTitle;
533535
}
534536

537+
@override
538+
Widget? buildTrailing(BuildContext context) {
539+
final store = PerAccountStoreWidget.of(context);
540+
if (!store.userSettings.starredMessageCounts) return null;
541+
return Counter(
542+
kind: CounterKind.quantity,
543+
style: CounterStyle.mainMenu,
544+
count: store.starredMessages.length,
545+
channelIdForBackground: null,
546+
);
547+
}
548+
535549
@override
536550
void onPressed(BuildContext context) {
537551
Navigator.of(context).push(MessageListPage.buildRoute(
@@ -588,8 +602,9 @@ class _DirectMessagesButton extends _NavigationBarMenuButton {
588602
final store = PerAccountStoreWidget.of(context);
589603
final unreadCount = store.unreads.countInAllDms();
590604
if (unreadCount == 0) return null;
591-
return UnreadCountBadge(
592-
style: UnreadCountBadgeStyle.mainMenu,
605+
return Counter(
606+
kind: CounterKind.unread,
607+
style: CounterStyle.mainMenu,
593608
count: unreadCount,
594609
channelIdForBackground: null,
595610
);

lib/widgets/inbox.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ abstract class _HeaderItem extends StatelessWidget {
309309
const SizedBox(width: 12),
310310
if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign),
311311
Padding(padding: const EdgeInsetsDirectional.only(end: 16),
312-
child: UnreadCountBadge(
312+
child: Counter(
313+
// TODO(design) use CounterKind.quantity, following Figma
314+
kind: CounterKind.unread,
313315
channelIdForBackground: channelId,
314316
count: count)),
315317
])));
@@ -431,7 +433,10 @@ class _DmItem extends StatelessWidget {
431433
const SizedBox(width: 12),
432434
if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign),
433435
Padding(padding: const EdgeInsetsDirectional.only(end: 16),
434-
child: UnreadCountBadge(channelIdForBackground: null,
436+
child: Counter(
437+
// TODO(design) use CounterKind.quantity, following Figma
438+
kind: CounterKind.unread,
439+
channelIdForBackground: null,
435440
count: count)),
436441
]))));
437442
}
@@ -565,7 +570,9 @@ class _TopicItem extends StatelessWidget {
565570
// TODO(design) copies the "@" marker color; is there a better color?
566571
if (visibilityIcon != null) _IconMarker(icon: visibilityIcon),
567572
Padding(padding: const EdgeInsetsDirectional.only(end: 16),
568-
child: UnreadCountBadge(
573+
child: Counter(
574+
// TODO(design) use CounterKind.quantity, following Figma
575+
kind: CounterKind.unread,
569576
channelIdForBackground: streamId,
570577
count: count)),
571578
]))));

lib/widgets/recent_dm_conversations.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ class RecentDmConversationsItem extends StatelessWidget {
234234
const SizedBox(width: 12),
235235
unreadCount > 0
236236
? Padding(padding: const EdgeInsetsDirectional.only(end: 16),
237-
child: UnreadCountBadge(channelIdForBackground: null,
237+
child: Counter(
238+
kind: CounterKind.unread,
239+
channelIdForBackground: null,
238240
count: unreadCount))
239241
: const SizedBox(),
240242
]))));

lib/widgets/subscription_list.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,8 @@ class SubscriptionItem extends StatelessWidget {
336336
// TODO(#747) show @-mention indicator when it applies
337337
Opacity(
338338
opacity: opacity,
339-
child: UnreadCountBadge(
339+
child: Counter(
340+
kind: CounterKind.unread,
340341
count: unreadCount,
341342
channelIdForBackground: subscription.streamId)),
342343
] else if (showMutedUnreadBadge) ...[

lib/widgets/theme.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
184184
foreground: const Color(0xff000000),
185185
icon: const Color(0xff6159e1),
186186
iconSelected: const Color(0xff222222),
187+
labelCounterQuantity: const Color(0xff222222).withValues(alpha: 0.6),
187188
labelCounterUnread: const Color(0xff1a1a1a),
188189
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(),
189190
labelMenuButton: const Color(0xff222222),
@@ -285,6 +286,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
285286
foreground: const Color(0xffffffff),
286287
icon: const Color(0xff7977fe),
287288
iconSelected: Colors.white.withValues(alpha: 0.8),
289+
labelCounterQuantity: const Color(0xffffffff).withValues(alpha: 0.7),
288290
labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.95),
289291
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(),
290292
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
@@ -395,6 +397,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
395397
required this.fabShadow,
396398
required this.icon,
397399
required this.iconSelected,
400+
required this.labelCounterQuantity,
398401
required this.labelCounterUnread,
399402
required this.labelEdited,
400403
required this.labelMenuButton,
@@ -496,6 +499,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
496499
final Color foreground;
497500
final Color icon;
498501
final Color iconSelected;
502+
final Color labelCounterQuantity;
499503
final Color labelCounterUnread;
500504
final Color labelEdited;
501505
final Color labelMenuButton;
@@ -592,6 +596,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
592596
Color? foreground,
593597
Color? icon,
594598
Color? iconSelected,
599+
Color? labelCounterQuantity,
595600
Color? labelCounterUnread,
596601
Color? labelEdited,
597602
Color? labelMenuButton,
@@ -683,6 +688,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
683688
fabShadow: fabShadow ?? this.fabShadow,
684689
icon: icon ?? this.icon,
685690
iconSelected: iconSelected ?? this.iconSelected,
691+
labelCounterQuantity: labelCounterQuantity ?? this.labelCounterQuantity,
686692
labelCounterUnread: labelCounterUnread ?? this.labelCounterUnread,
687693
labelEdited: labelEdited ?? this.labelEdited,
688694
labelMenuButton: labelMenuButton ?? this.labelMenuButton,
@@ -781,6 +787,7 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
781787
fabShadow: Color.lerp(fabShadow, other.fabShadow, t)!,
782788
icon: Color.lerp(icon, other.icon, t)!,
783789
iconSelected: Color.lerp(iconSelected, other.iconSelected, t)!,
790+
labelCounterQuantity: Color.lerp(labelCounterQuantity, other.labelCounterQuantity, t)!,
784791
labelCounterUnread: Color.lerp(labelCounterUnread, other.labelCounterUnread, t)!,
785792
labelEdited: Color.lerp(labelEdited, other.labelEdited, t)!,
786793
labelMenuButton: Color.lerp(labelMenuButton, other.labelMenuButton, t)!,

lib/widgets/topic_list.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ class _TopicItem extends StatelessWidget {
303303
if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign),
304304
if (visibilityIcon != null) _IconMarker(icon: visibilityIcon),
305305
if (unreadCount > 0)
306-
UnreadCountBadge(
306+
Counter(
307+
kind: CounterKind.unread,
307308
count: unreadCount,
308309
channelIdForBackground: null),
309310
])),

lib/widgets/unread_count_badge.dart

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,29 @@ import 'store.dart';
44
import 'text.dart';
55
import 'theme.dart';
66

7-
/// A widget to display a given number of unreads in a conversation.
7+
/// A widget to display a given number (e.g. of unread messages or of users).
88
///
99
/// See Figma's "counter-menu" component, which this is based on:
1010
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-186671&m=dev
1111
/// It looks like that component was created for the main menu,
1212
/// then adapted for various other contexts, like the Inbox page.
13-
/// See [UnreadCountBadgeStyle].
14-
///
15-
/// Currently this widget supports only the component's "kind=unread" variant,
16-
/// not "kind=quantity".
17-
// TODO support the "kind=quantity" variant, update dartdoc
18-
class UnreadCountBadge extends StatelessWidget {
19-
const UnreadCountBadge({
13+
/// See [CounterStyle] and [CounterKind] for the possible variants.
14+
class Counter extends StatelessWidget {
15+
const Counter({
2016
super.key,
21-
this.style = UnreadCountBadgeStyle.other,
17+
this.style = CounterStyle.other,
18+
required this.kind,
2219
required this.count,
2320
required this.channelIdForBackground,
24-
});
21+
}) : assert(!(kind == CounterKind.quantity && channelIdForBackground != null));
2522

26-
final UnreadCountBadgeStyle style;
23+
final CounterStyle style;
24+
final CounterKind kind;
2725
final int count;
2826

2927
/// An optional [Subscription.streamId], for a channel-colorized background.
3028
///
31-
/// Useful when this badge represents messages in one specific channel.
29+
/// Useful when this counter represents unreads in one specific channel.
3230
///
3331
/// If null, the default neutral background will be used.
3432
// TODO remove; the Figma doesn't use this anymore.
@@ -48,40 +46,54 @@ class UnreadCountBadge extends StatelessWidget {
4846
final swatch = colorSwatchFor(context, subscription);
4947
backgroundColor = swatch.unreadCountBadgeBackground;
5048
} else {
51-
textColor = designVariables.labelCounterUnread;
49+
textColor = switch (kind) {
50+
CounterKind.unread => designVariables.labelCounterUnread,
51+
CounterKind.quantity => designVariables.labelCounterQuantity,
52+
};
5253
backgroundColor = designVariables.bgCounterUnread;
5354
}
5455

5556
final padding = switch (style) {
56-
UnreadCountBadgeStyle.mainMenu =>
57+
CounterStyle.mainMenu =>
5758
const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
58-
UnreadCountBadgeStyle.other =>
59+
CounterStyle.other =>
5960
const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
6061
};
6162

62-
final double wght = switch (style) {
63-
UnreadCountBadgeStyle.mainMenu => 600,
64-
UnreadCountBadgeStyle.other => 500,
63+
final double wght = switch ((style, kind)) {
64+
(CounterStyle.mainMenu, CounterKind.unread ) => 600,
65+
(CounterStyle.mainMenu, CounterKind.quantity) => 500,
66+
(CounterStyle.other, CounterKind.unread ) => 500,
67+
(CounterStyle.other, CounterKind.quantity) => 500,
6568
};
6669

67-
return DecoratedBox(
68-
decoration: BoxDecoration(
69-
borderRadius: BorderRadius.circular(5),
70-
color: backgroundColor,
71-
),
72-
child: Padding(
73-
padding: padding,
74-
child: Text(
75-
style: TextStyle(
76-
fontSize: 16,
77-
height: (16 / 16),
78-
color: textColor,
79-
).merge(weightVariableTextStyle(context, wght: wght)),
80-
count.toString())));
70+
Widget result = Padding(
71+
padding: padding,
72+
child: Text(
73+
style: TextStyle(
74+
fontSize: 16,
75+
height: (16 / 16),
76+
color: textColor,
77+
).merge(weightVariableTextStyle(context, wght: wght)),
78+
count.toString()));
79+
80+
switch (kind) {
81+
case CounterKind.unread:
82+
result = DecoratedBox(
83+
decoration: BoxDecoration(
84+
borderRadius: BorderRadius.circular(5),
85+
color: backgroundColor,
86+
),
87+
child: result);
88+
case CounterKind.quantity:
89+
// no decoration
90+
}
91+
92+
return result;
8193
}
8294
}
8395

84-
enum UnreadCountBadgeStyle {
96+
enum CounterStyle {
8597
/// The style to use in the main menu.
8698
///
8799
/// Figma:
@@ -98,6 +110,22 @@ enum UnreadCountBadgeStyle {
98110
other,
99111
}
100112

113+
enum CounterKind {
114+
/// The counter counts unread messages.
115+
///
116+
/// Figma:
117+
/// Main-menu style: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-185125&m=dev
118+
/// Other style: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6205-26001&m=dev
119+
unread,
120+
121+
/// The counter counts something else, like users or starred messages.
122+
///
123+
/// Figma:
124+
/// Main-menu style: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-186672&m=dev
125+
/// Other style: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6025-293468&m=dev
126+
quantity,
127+
}
128+
101129
class MutedUnreadBadge extends StatelessWidget {
102130
const MutedUnreadBadge({super.key});
103131

test/widgets/checks.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ extension PerAccountStoreWidgetChecks on Subject<PerAccountStoreWidget> {
9292
Subject<Widget> get child => has((x) => x.child, 'child');
9393
}
9494

95-
extension UnreadCountBadgeChecks on Subject<UnreadCountBadge> {
95+
extension UnreadCountBadgeChecks on Subject<Counter> {
9696
Subject<int> get count => has((b) => b.count, 'count');
9797
Subject<int?> get channelIdForBackground => has((b) => b.channelIdForBackground, 'channelIdForBackground');
9898
}

test/widgets/inbox_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ void main() {
222222
find.descendant(
223223
of: find.byWidget(findRowByLabel(tester, channel.name)!),
224224
matching: find.descendant(
225-
of: find.byType(UnreadCountBadge),
225+
of: find.byType(Counter),
226226
matching: find.text('1'))));
227227

228228
final expectedTextColor = DesignVariables.light.unreadCountBadgeTextForChannel;
@@ -406,19 +406,19 @@ void main() {
406406

407407
check(find.descendant(
408408
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
409-
matching: find.widgetWithText(UnreadCountBadge, '1'))).findsOne();
409+
matching: find.widgetWithText(Counter, '1'))).findsOne();
410410

411411
await store.handleEvent(eg.updateMessageFlagsRemoveEvent(MessageFlag.read, [message2]));
412412
await tester.pump();
413413
check(find.descendant(
414414
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
415-
matching: find.widgetWithText(UnreadCountBadge, '2'))).findsOne();
415+
matching: find.widgetWithText(Counter, '2'))).findsOne();
416416

417417
await store.handleEvent(eg.updateMessageFlagsRemoveEvent(MessageFlag.read, [message3]));
418418
await tester.pump();
419419
check(find.descendant(
420420
of: find.byWidget(findRowByLabel(tester, 'aaa')!),
421-
matching: find.widgetWithText(UnreadCountBadge, '3'))).findsOne();
421+
matching: find.widgetWithText(Counter, '3'))).findsOne();
422422
});
423423
});
424424

test/widgets/subscription_list_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ void main() {
186186
await setupStreamListPage(tester, subscriptions: [
187187
eg.subscription(stream),
188188
], unreadMsgs: unreadMsgs);
189-
check(find.byType(UnreadCountBadge).evaluate()).length.equals(1);
189+
check(find.byType(Counter).evaluate()).length.equals(1);
190190
check(find.byType(MutedUnreadBadge).evaluate().length).equals(0);
191191
});
192192

@@ -206,7 +206,7 @@ void main() {
206206
)],
207207
unreadMsgs: unreadMsgs);
208208
check(tester.widget<Text>(find.descendant(
209-
of: find.byType(UnreadCountBadge), matching: find.byType(Text))))
209+
of: find.byType(Counter), matching: find.byType(Text))))
210210
.data.equals('1');
211211
check(find.byType(MutedUnreadBadge).evaluate().length).equals(0);
212212
});
@@ -217,7 +217,7 @@ void main() {
217217
await setupStreamListPage(tester, subscriptions: [
218218
eg.subscription(stream),
219219
], unreadMsgs: unreadMsgs);
220-
check(find.byType(UnreadCountBadge).evaluate()).length.equals(0);
220+
check(find.byType(Counter).evaluate()).length.equals(0);
221221
check(find.byType(MutedUnreadBadge).evaluate().length).equals(0);
222222
});
223223

@@ -274,7 +274,7 @@ void main() {
274274
check(tester.widget<Icon>(find.byIcon(iconDataForStream(stream))).color)
275275
.isNotNull().isSameColorAs(swatch.iconOnPlainBackground);
276276

277-
final unreadCountBadgeRenderBox = tester.renderObject<RenderBox>(find.byType(UnreadCountBadge));
277+
final unreadCountBadgeRenderBox = tester.renderObject<RenderBox>(find.byType(Counter));
278278
check(unreadCountBadgeRenderBox).legacyMatcher(
279279
// `paints` isn't a [Matcher] so we wrap it with `equals`;
280280
// awkward but it works

0 commit comments

Comments
 (0)