From 05768ec1a23fcf4a429d9b7f5ce34212abdeeabf Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 2 Oct 2025 18:34:27 -0700 Subject: [PATCH 1/3] unread_count_badge: Remove `bold` variant; pin down to current Figma The "Channels" page and "Inbox" page both have updated designs in Figma, which we should follow. This commit matches the badges on the "Channels" page to that new design. There's more work to match the badges on the "Inbox" page, but now that work is defined crisply in a TODO: // TODO support the "kind=quantity" variant, update dartdoc Related: #1406 Related: #1527 --- lib/widgets/inbox.dart | 1 - lib/widgets/subscription_list.dart | 3 +-- lib/widgets/theme.dart | 4 ++-- lib/widgets/unread_count_badge.dart | 26 ++++++++++++++++---------- test/widgets/checks.dart | 1 - 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index 00887b17ff..ce7b200327 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -311,7 +311,6 @@ abstract class _HeaderItem extends StatelessWidget { Padding(padding: const EdgeInsetsDirectional.only(end: 16), child: UnreadCountBadge( channelIdForBackground: channelId, - bold: true, count: count)), ]))); } diff --git a/lib/widgets/subscription_list.dart b/lib/widgets/subscription_list.dart index 327ac5265a..03714c734a 100644 --- a/lib/widgets/subscription_list.dart +++ b/lib/widgets/subscription_list.dart @@ -338,8 +338,7 @@ class SubscriptionItem extends StatelessWidget { opacity: opacity, child: UnreadCountBadge( count: unreadCount, - channelIdForBackground: subscription.streamId, - bold: true)), + channelIdForBackground: subscription.streamId)), ] else if (showMutedUnreadBadge) ...[ const SizedBox(width: 12), // TODO(#747) show @-mention indicator when it applies diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index a82909bcae..18540139d0 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -184,7 +184,7 @@ class DesignVariables extends ThemeExtension { foreground: const Color(0xff000000), icon: const Color(0xff6159e1), iconSelected: const Color(0xff222222), - labelCounterUnread: const Color(0xff222222), + labelCounterUnread: const Color(0xff1a1a1a), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), labelMenuButton: const Color(0xff222222), labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5), @@ -285,7 +285,7 @@ class DesignVariables extends ThemeExtension { foreground: const Color(0xffffffff), icon: const Color(0xff7977fe), iconSelected: Colors.white.withValues(alpha: 0.8), - labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7), + labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.95), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(), labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85), labelSearchPrompt: const Color(0xffffffff).withValues(alpha: 0.5), diff --git a/lib/widgets/unread_count_badge.dart b/lib/widgets/unread_count_badge.dart index b14bd6972d..88d30d2fad 100644 --- a/lib/widgets/unread_count_badge.dart +++ b/lib/widgets/unread_count_badge.dart @@ -6,24 +6,32 @@ import 'theme.dart'; /// A widget to display a given number of unreads in a conversation. /// -/// Implements the design for these in Figma: -/// +/// See Figma's "counter-menu" component, which this is based on: +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-186671&m=dev +/// It looks like that component was created for the main menu, +/// then adapted for various other contexts, like the Inbox page. +/// +/// Currently this widget supports only those other contexts (not the main menu) +/// and only the component's "kind=unread" variant (not "kind=quantity"). +/// For example, the "Channels" page: +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6205-26001&m=dev +// TODO support the main-menu context, update dartdoc +// TODO support the "kind=quantity" variant, update dartdoc class UnreadCountBadge extends StatelessWidget { const UnreadCountBadge({ super.key, required this.count, required this.channelIdForBackground, - this.bold = false, }); final int count; - final bool bold; /// An optional [Subscription.streamId], for a channel-colorized background. /// /// Useful when this badge represents messages in one specific channel. /// /// If null, the default neutral background will be used. + // TODO remove; the Figma doesn't use this anymore. final int? channelIdForBackground; @override @@ -46,19 +54,17 @@ class UnreadCountBadge extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(3), + borderRadius: BorderRadius.circular(5), color: backgroundColor, ), child: Padding( - padding: const EdgeInsets.fromLTRB(4, 0, 4, 1), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: Text( style: TextStyle( fontSize: 16, - height: (18 / 16), - fontFeatures: const [FontFeature.enable('smcp')], // small caps + height: (16 / 16), color: textColor, - ).merge(weightVariableTextStyle(context, - wght: bold ? 600 : null)), + ).merge(weightVariableTextStyle(context, wght: 500)), count.toString()))); } } diff --git a/test/widgets/checks.dart b/test/widgets/checks.dart index d3a2761ad1..ef3f9b634f 100644 --- a/test/widgets/checks.dart +++ b/test/widgets/checks.dart @@ -94,7 +94,6 @@ extension PerAccountStoreWidgetChecks on Subject { extension UnreadCountBadgeChecks on Subject { Subject get count => has((b) => b.count, 'count'); - Subject get bold => has((b) => b.bold, 'bold'); Subject get channelIdForBackground => has((b) => b.channelIdForBackground, 'channelIdForBackground'); } From ba8d2966c41049e0596fc1aae03cb934c5fd34a1 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 2 Oct 2025 18:56:51 -0700 Subject: [PATCH 2/3] topic_list: Make rows at least 40px tall, for big-enough touch target --- lib/widgets/topic_list.dart | 78 +++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/widgets/topic_list.dart b/lib/widgets/topic_list.dart index 16111fc678..0034bfae02 100644 --- a/lib/widgets/topic_list.dart +++ b/lib/widgets/topic_list.dart @@ -264,43 +264,47 @@ class _TopicItem extends StatelessWidget { topic: topic, someMessageIdInTopic: maxId), splashFactory: NoSplash.splashFactory, - child: Padding(padding: EdgeInsetsDirectional.fromSTEB(6, 8, 12, 8), - child: Row( - spacing: 8, - // In the Figma design, the text and icons on the topic item row - // are aligned to the start on the cross axis - // (i.e., `align-items: flex-start`). The icons are padded down - // 2px relative to the start, to visibly sit on the baseline. - // To account for scaled text, we align everything on the row - // to [CrossAxisAlignment.center] instead ([Row]'s default), - // like we do for the topic items on the inbox page. - // TODO(#1528): align to baseline (and therefore to first line of - // topic name), but with adjustment for icons - // CZO discussion: - // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/topic.20list.20item.20alignment/near/2173252 - children: [ - // A null [Icon.icon] makes a blank space. - _IconMarker(icon: topic.isResolved ? ZulipIcons.check : null), - Expanded(child: Opacity( - opacity: opacity, - child: Text( - style: TextStyle( - fontSize: 17, - height: 20 / 17, - fontStyle: topic.displayName == null ? FontStyle.italic : null, - color: designVariables.textMessage, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - topic.unresolve().displayName ?? store.realmEmptyTopicDisplayName))), - Opacity(opacity: opacity, child: Row( - spacing: 4, - children: [ - if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign), - if (visibilityIcon != null) _IconMarker(icon: visibilityIcon), - if (unreadCount > 0) _UnreadCountBadge(count: unreadCount), - ])), - ])))); + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: 40), + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB(6, 4, 12, 4), + child: Row( + spacing: 8, + // In the Figma design, the text and icons on the topic item row + // are aligned to the start on the cross axis + // (i.e., `align-items: flex-start`). The icons are padded down + // 2px relative to the start, to visibly sit on the baseline. + // To account for scaled text, we align everything on the row + // to [CrossAxisAlignment.center] instead ([Row]'s default), + // like we do for the topic items on the inbox page. + // TODO(#1528): align to baseline (and therefore to first line of + // topic name), but with adjustment for icons + // CZO discussion: + // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/topic.20list.20item.20alignment/near/2173252 + children: [ + // A null [Icon.icon] makes a blank space. + _IconMarker(icon: topic.isResolved ? ZulipIcons.check : null), + Expanded(child: Opacity( + opacity: opacity, + child: Text( + style: TextStyle( + fontSize: 17, + height: 20 / 17, + fontStyle: topic.displayName == null ? FontStyle.italic : null, + color: designVariables.textMessage, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + topic.unresolve().displayName ?? store.realmEmptyTopicDisplayName))), + Opacity(opacity: opacity, child: Row( + spacing: 4, + children: [ + if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign), + if (visibilityIcon != null) _IconMarker(icon: visibilityIcon), + if (unreadCount > 0) _UnreadCountBadge(count: unreadCount), + ])), + ])), + ))); } } From 89b85dcb79b640187191c2c7fdd9e7356c3a4c1c Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 2 Oct 2025 18:49:40 -0700 Subject: [PATCH 3/3] topic_list: Use UnreadCountBadge, replacing ad hoc private widget The private widget, following the Figma, made the badge a bit more compact, with 1px less padding on each side, and a font size smaller by 1 (but with the same line height). That was helpful for keeping the rows at uniform height regardless of unreads (without text scaling anyway)...but I've accomplished that by adding a minimum row height (40), in the previous commit. --- lib/widgets/topic_list.dart | 34 +++++------------------------ lib/widgets/unread_count_badge.dart | 5 ++++- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/lib/widgets/topic_list.dart b/lib/widgets/topic_list.dart index 0034bfae02..846f2202f7 100644 --- a/lib/widgets/topic_list.dart +++ b/lib/widgets/topic_list.dart @@ -14,6 +14,7 @@ import 'page.dart'; import 'store.dart'; import 'text.dart'; import 'theme.dart'; +import 'unread_count_badge.dart'; class TopicListPage extends StatelessWidget { const TopicListPage({super.key, required this.streamId}); @@ -301,7 +302,10 @@ class _TopicItem extends StatelessWidget { children: [ if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign), if (visibilityIcon != null) _IconMarker(icon: visibilityIcon), - if (unreadCount > 0) _UnreadCountBadge(count: unreadCount), + if (unreadCount > 0) + UnreadCountBadge( + count: unreadCount, + channelIdForBackground: null), ])), ])), ))); @@ -324,31 +328,3 @@ class _IconMarker extends StatelessWidget { color: designVariables.textMessage.withFadedAlpha(0.4)); } } - -// This is adapted from [UnreadCountBadge]. -// TODO(#1406) see if we can reuse this in redesign -// TODO(#1527) see if we can reuse this in redesign -class _UnreadCountBadge extends StatelessWidget { - const _UnreadCountBadge({required this.count}); - - final int count; - - @override - Widget build(BuildContext context) { - final designVariables = DesignVariables.of(context); - - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: designVariables.bgCounterUnread, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Text(count.toString(), - style: TextStyle( - fontSize: 15, - height: 16 / 15, - color: designVariables.labelCounterUnread, - ).merge(weightVariableTextStyle(context, wght: 500))))); - } -} diff --git a/lib/widgets/unread_count_badge.dart b/lib/widgets/unread_count_badge.dart index 88d30d2fad..828d724dc9 100644 --- a/lib/widgets/unread_count_badge.dart +++ b/lib/widgets/unread_count_badge.dart @@ -13,8 +13,11 @@ import 'theme.dart'; /// /// Currently this widget supports only those other contexts (not the main menu) /// and only the component's "kind=unread" variant (not "kind=quantity"). -/// For example, the "Channels" page: +/// For example, the "Channels" page and the topic-list page: /// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6205-26001&m=dev +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6823-37113&m=dev +/// (We use this for the topic-list page even though the Figma makes it a bit +/// more compact there…the inconsistency seems worse and might be accidental.) // TODO support the main-menu context, update dartdoc // TODO support the "kind=quantity" variant, update dartdoc class UnreadCountBadge extends StatelessWidget {