Files
buzz/mobile/lib/features/activity
Tom Brow be1ac392e8 perf(mobile): render inbox and search previews as plain text (#8675)
### Summary

Inbox rows and search results show each message as a 2-line preview.
That preview was a capped `MessageContent`, so gpt_markdown parsed and
laid out the whole source (syntax-highlighted code blocks and measured
tables included), even though only two lines are painted. A native trace
on an iPhone 12 mini attributed a 102 ms inbox-open frame mostly to this
work.

Previews now skip Markdown entirely. `messagePreviewSource` takes only
the leading lines a preview can show, and at most the first 4,000
characters of a single long line, so link normalization, mention binding
and emoji lookup never scan the rest of a long message.
`messagePreviewPieces` is then a linear pass over those lines, similar
to desktop's `getMarkdownPreviewText`. It drops block syntax (headings,
quotes, list markers, rules, table dividers, code fences) and unwraps
inline emphasis and `<u>`, honoring backslash escapes and keeping inline
(including multi-backtick) and fenced code as literal text. It also
replaces links with their label. The new `MessagePreview` widget renders
the result as a single `Text.rich` with `maxLines` and an ellipsis.

Tokens keep the look they have in the thread view:
- A bound mention is the same `MessageMentionPill` chip, with the bot
icon for agents. An ambiguous mention stays as text.
- `#channel` tokens and `buzz://channel/...` links are the same channel
token chip.
- A `buzz://message`, repo, PR or issue link that shows its own URL
previews with the thread view's short label, such as `general ·
abcdef01`. Both use the shared `buzzLinkLabel` helper.
- Custom emoji render as inline images. Only shortcodes the message
names are looked up in the palette.
- Media links show as "Image", "Video" or "Audio".

Chips in previews are not tappable, and tapping the row opens the
message. Bold, italic, inline code and link styling are no longer shown
in previews. The timeline and thread view are unchanged, and so is the
profile status line, which still uses `MessageContent`.

This replaces this PR's earlier approach of cutting the Markdown source
before parsing. That approach needed delimiter-balance heuristics, and a
security review found a real case (`**` inside a link label) where the
heuristic misfired.

### Validation

- `preview_text_test.dart` is a table test of the parser: block syntax,
emphasis that must and must not unwrap (`snake_case`, a lone `*`),
inline and fenced code, unpaired backticks, links, tables, the line
limit and the long-line cap. It also covers mentions (bound, ambiguous,
inside emphasis, link labels and code), custom emoji (including adjacent
`🅰️b:`), channel tokens and links, message and repo link labels, and
media. The long-line row also asserts that `messagePreviewSource`
returns a bounded prefix.
- `message_preview_test.dart` checks the widget: no `GptMarkdown`, one
capped and ellipsized paragraph, mention chip, custom emoji image,
channel chip, media label and empty content.
- Existing inbox and search tests updated to find `MessagePreview`,
including the agent-chip test in search.
- `flutter analyze` and the full mobile test suite pass at 8c721ba3a
(with `--dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example`, as
`just mobile-test` runs it).
- Device A/B on an iPhone 12 mini, using Profile builds against the real
inbox on the internal relay. Each run was a cold launch followed by 6
Inbox opens, in the order main, fix, main, fix:

  | | main eda26b942 | this PR 6661d594a |
  |---|---|---|
| First open, worst frame (run 1 / run 2) | 40.4 / 34.9 ms | 22.4 / 23.2
ms |
| First open, build total (run 1 / run 2) | 77.8 / 91.1 ms | 38.5 / 52.3
ms |
  | All opens, median worst frame | 27.2 ms | 23.3 ms |
  | All opens, median build total | 43.4 ms | 26.6 ms |

First open drops about 40% in build time and about 14 ms in worst frame.
On warm opens, build time also drops about 40%, but the worst frame
barely moves, so what remains in those frames is not the preview parse.

## Codex review status

Codex reviewed this PR over four rounds. Every finding about cost or
correctness is fixed, and those threads are resolved. Later rounds
turned up narrower and narrower preview edge cases, so the author
decided to stop iterating on Codex findings. Any Codex thread still
open, or any finding Codex posts after f538853ed, is a deliberately
accepted preview limitation and doesn't block merging. Example: a [code
span that continues across a line
break](https://github.com/block/buzz/pull/8675#discussion_r4415196032)
can preview a mention as a chip. The full message view is unaffected.
Only the one- or two-line inbox and search previews can differ from it.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:54:32 -07:00
..