From 8312419652d9e5ad5dbaae9c2a8e83b4e95f0961 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Tue, 3 Nov 2026 22:10:44 +0000 Subject: [PATCH] fix(mobile): size reply labels correctly with Bold Text (#9013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System Bold Text made single-participant reply labels such as “12 replies” wrap or truncate: the handoff measured normal-weight glyphs, then rendered bold glyphs inside that narrower box. Resolve the inherited text style and platform bold override before measuring, and use that same style for rendering. Addresses the fix-before-merge blocker identified by [Tom's agent on #8987](https://github.com/block/buzz/pull/8987#issuecomment-6611883349), specifically [the Bold Text finding](https://github.com/block/buzz/pull/8987#discussion_r4458491084). Tom classified unloaded-root typing, legacy reply-only paging, and working-state label clipping as acceptable later follow-ups; those remain outside this patch. Validation: - Regression loads the bundled Inter font (the default test font masks weight differences), toggles Bold Text off/on/off, and covers idle/working footers at 1x/2x scale. All four cases failed before the production fix. - Formatting and full analysis passed; 4,252 Flutter tests passed (44 skipped), plus 4 unconfigured-push tests. All 27 crypto tests passed. Mandatory push hooks passed. - Self-review found no blockers in this narrow change. Draft pending app/human verification required by AGENTS.md. To verify: enable system Bold Text, open a channel with a single-participant thread, and confirm “12 replies” remains complete; repeat with enlarged text and toggle Bold Text off. No physical-device validation is claimed. Signed-off-by: kenny lopez Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> --- .../channels/reply_activity_handoff.dart | 11 ++- .../reply_summary_bold_text_test.dart | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 mobile/test/features/channels/reply_summary_bold_text_test.dart diff --git a/mobile/lib/features/channels/reply_activity_handoff.dart b/mobile/lib/features/channels/reply_activity_handoff.dart index 9b460ec05b..cb12ea5443 100644 --- a/mobile/lib/features/channels/reply_activity_handoff.dart +++ b/mobile/lib/features/channels/reply_activity_handoff.dart @@ -45,9 +45,16 @@ class ReplyActivityHandoff extends HookWidget { }, [working, reducedMotion]); final animated = useAnimation(controller); final progress = reducedMotion ? (working ? 0.0 : 1.0) : animated; - final style = context.textTheme.labelMedium?.copyWith( - color: context.colors.onSurfaceVariant, + // Match Text's inherited style and platform Bold Text override so the + // measured box fits the glyphs that are actually rendered. + var style = DefaultTextStyle.of(context).style.merge( + context.textTheme.labelMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), ); + if (MediaQuery.boldTextOf(context)) { + style = style.merge(const TextStyle(fontWeight: FontWeight.bold)); + } return LayoutBuilder( builder: (context, constraints) { final text = TextPainter( diff --git a/mobile/test/features/channels/reply_summary_bold_text_test.dart b/mobile/test/features/channels/reply_summary_bold_text_test.dart new file mode 100644 index 0000000000..7bf35dd1d2 --- /dev/null +++ b/mobile/test/features/channels/reply_summary_bold_text_test.dart @@ -0,0 +1,85 @@ +import 'package:buzz/features/channels/reply_summary_link.dart'; +import 'package:buzz/features/channels/timeline_message.dart'; +import 'package:buzz/shared/profile/user_cache_provider.dart'; +import 'package:buzz/shared/profile/user_profile.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/widget_helpers.dart'; + +void main() { + setUpAll(() async { + // Ahem has identical regular/bold metrics and hides this regression. + final font = FontLoader('Inter') + ..addFont(rootBundle.load('assets/fonts/InterVariable.ttf')); + await font.load(); + }); + + for (final working in [false, true]) { + for (final scale in [1.0, 2.0]) { + testWidgets( + 'reply label honors Bold Text: working=$working scale=$scale', + (tester) async { + Widget view(bool bold) => WidgetHelpers.testable( + overrides: [userCacheProvider.overrideWith(_Profiles.new)], + child: MediaQuery( + data: MediaQueryData( + boldText: bold, + textScaler: TextScaler.linear(scale), + ), + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: ReplySummaryLink( + summary: const ThreadSummary( + threadHeadId: 'root', + replyCount: 12, + participantPubkeys: ['participant'], + ), + working: working ? const ['participant'] : const [], + onTap: () {}, + ), + ), + ), + ), + ); + + for (final bold in [false, true, false]) { + await tester.pumpWidget(view(bold)); + final paragraph = tester.renderObject( + find.text('12 replies'), + ); + final expected = TextPainter( + text: paragraph.text, + textDirection: paragraph.textDirection, + textScaler: paragraph.textScaler, + )..layout(); + // The entire one-line label fits at this viewport; its allocated + // width must match the text actually rendered, including bold. + expect(paragraph.size.width, closeTo(expected.width, 0.01)); + expect(paragraph.size.height, closeTo(expected.height, 0.01)); + expect(paragraph.didExceedMaxLines, isFalse); + if (bold) { + expect(paragraph.text.style!.fontWeight, FontWeight.bold); + } + expected.dispose(); + expect(tester.takeException(), isNull); + } + }, + ); + } + } +} + +class _Profiles extends UserCacheNotifier { + @override + Map build() => const { + 'participant': UserProfile( + pubkey: 'participant', + displayName: 'Participant', + ), + }; +}