mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
## Context Each message in a channel shows its text as Markdown. The `GptMarkdown` widget parses that text. Parsing is slow for long messages. `GptMarkdownTheme` gives Markdown colours and styles to the widgets below it. When it gets a new theme data object, it tells all `GptMarkdown` widgets below it to parse their text again. It compares the old and new objects by identity, because `GptMarkdownThemeData` has no value equality. Thus a new object with the same values also causes a parse. ## Problem Two widgets made a new theme data object on each build: - `MessageContent` made one for each message bubble. - `AppMarkdownTheme` made one for the full app. Thus each rebuild parsed all messages on the screen again. Many events cause a rebuild: a batch of profiles, a read marker, a keyboard frame. In a large channel such as #4things (1,235 members), these events occur almost all the time. ## Change - `MessageContent` keeps its bubble theme until one of its inputs changes. The inputs are: the app Markdown theme, the link colour, own or other message, the ink colour and `onSurface`. - `AppMarkdownTheme` makes new theme data only when `ThemeData` changes or when an outer Markdown theme changes. A change of colour, of brightness, or of own or other message still makes new theme data. Then the Markdown parses again with the new colours. This is correct. ### Related issue None found. ## Testing - New regression tests fail on main. They pass on this branch. - `message_content_stability_test.dart`: When a bubble rebuilds with the same inputs, it keeps the same theme object and the same parsed `TextSpan`. The test does this for own messages and for other messages. When own or other changes, the bubble gets a new theme. - `app_markdown_theme_test.dart`: The app keeps one theme object when it rebuilds. It makes a new one when light or dark mode changes. - `just mobile-install mobile-check mobile-test` passes. - Device test: iPhone 12 mini, Profile build, Tom's Block community, open #4things. The two runs had different live traffic and different cached content. Thus they show the cause and the fix. They do not give a controlled speed measurement. - Before: In the first 2.4 s, parses from rebuilds (`performRebuild`) used approximately 90 ms. One frame used 116 ms. Approximately 48 ms of that frame parsed visible messages again. - After: In 30 s, rebuilds caused no parses. Only the first build of each message (`_firstBuild`) parsed. - An independent agent review found no defects. It found one small item: `ChatColors` has no value equality. Thus a rebuild of `MaterialApp` still makes new app theme data one time. This costs only a little performance. We did not change it. Not tested: A human test did not occur. The mobile testing exception waives it, and an agent did the device test. No test occurred on an Android device. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.4 KiB
Dart
55 lines
2.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:gpt_markdown/gpt_markdown.dart';
|
|
|
|
import 'app_theme.dart';
|
|
import 'code_style.dart';
|
|
|
|
/// States the app's code style on every `GptMarkdown` below it and leaves the
|
|
/// rest of the Markdown theme as it already was.
|
|
///
|
|
/// Registering a [GptMarkdownThemeData] as a `ThemeExtension` would be the
|
|
/// shorter route, but its factory is not a partial override: it builds a fresh
|
|
/// stock Material theme and fills every heading, rule and highlight field from
|
|
/// it, so registering one stops `GptMarkdownTheme.of` deriving those from the
|
|
/// app. Restating them in the extension does not close the gap either — the
|
|
/// ambient headings carry the localized text geometry, which exists only once
|
|
/// `Theme.of` has run and a `ThemeData` factory cannot see. Overriding the one
|
|
/// field on the ambient theme is what leaves non-code Markdown untouched.
|
|
class AppMarkdownTheme extends HookWidget {
|
|
const AppMarkdownTheme({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final outer = context
|
|
.dependOnInheritedWidgetOfExactType<GptMarkdownTheme>()
|
|
?.gptThemeData;
|
|
// GptMarkdownTheme notifies by data identity, and every notification makes
|
|
// each GptMarkdown in the app re-parse its text. Derive new data only when
|
|
// what it is derived from changes.
|
|
final data = useMemoized(() {
|
|
final scheme = theme.colorScheme;
|
|
return GptMarkdownTheme.of(context).copyWith(
|
|
// Inline code is otherwise drawn in gpt_markdown's own bundled face, at
|
|
// its own size, tinted from `onSurface`. Stating the app's code style
|
|
// gives every Markdown surface the face, size and chip colours a fenced
|
|
// code block already uses.
|
|
inlineCode: InlineCodeStyle(
|
|
fontFamily: CodeStyle.fontFamily,
|
|
fontSizeFactor: CodeStyle.fontSizeFactor,
|
|
color: scheme.onSurface,
|
|
backgroundColor: CodeStyle.background(scheme),
|
|
borderColor: CodeStyle.border(scheme),
|
|
// A chip is small, so it takes the smallest step of the app's radius
|
|
// scale rather than the `Radii.card` a block uses.
|
|
borderRadius: const Radius.circular(Radii.xs),
|
|
).resolve(scheme),
|
|
);
|
|
}, [theme, outer]);
|
|
return GptMarkdownTheme(gptThemeData: data, child: child);
|
|
}
|
|
}
|