Skip to content

fix(mobile): split escaped voice-note links into their own segment - #8940

Merged
brow merged 3 commits into
mainfrom
mobile-escaped-voice-note-segment
Nov 2, 2026
Merged

brow merged 3 commits into
mainfrom
mobile-escaped-voice-note-segment

Conversation

@brow

@brow brow commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #8925, from a Codex finding there.

After #8925, message Markdown arrives with backslash escapes shielded, but imeta is keyed by plain URLs. Two lookups used the raw, still-escaped destination:

  • Bubble segmentation (bubble_segments.dart): an attachment link such as [voice.mp4](https://…/voice\_note.mp4) missed its imeta entry and stayed inside the text bubble, even though the link builder rendered it as an attachment. The fix unshields the URL before the lookup.
  • Trailing image gallery (extractTrailingImageGallery in message_gallery.dart): with two or more trailing images, an escaped destination kept its backslash, so the carousel loaded the wrong URL and lost the imeta alt text. The fix unescapes the destination the same way as the label beside it. Its callers in message_media_avatar.dart and message_skeleton_body.dart get the fix too. This gap was already on main.

Validation

  • The existing attachments stay outside bubbles and text order is preserved journey now uses an escaped destination with a plain imeta URL. The existing carousel journey now has one escaped image destination and taps it by its plain URL. Each fails with its fix reverted.
  • flutter analyze is clean and the full mobile suite passes (4151 tests).
  • Limitation: no simulator run. Both changes are key lookups, and the widget journeys exercise the rendered layout. The full-app run for the surrounding escape handling is recorded in fix(mobile): render Markdown backslash escapes #8925.

🤖 Generated with Claude Code

Bubble segmentation sees Markdown with escapes held as stand-ins, but
imeta is keyed by plain URLs. A voice-note link whose destination has an
escape, such as `voice\_note.mp4`, therefore missed its imeta entry and
stayed inside the text bubble even though the link builder rendered it as
audio. Unshield the URL before the lookup.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T19:01:14.021353Z e588aba Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@brow
brow marked this pull request as ready for review November 2, 2026 18:45
@brow
brow requested a review from a team as a code owner November 2, 2026 18:45
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 2fd5c122e8bb6d826549d070cb918747a95432ac...e588abac225ae262cd96990a3bb9d34bdfa336a0.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review e588abac225ae262cd96990a3bb9d34bdfa336a0 to authorize a new review.
Any previous review applies only to its recorded range.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4996e40ee3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

extractTrailingImageGallery used the raw destination, so an escaped URL
like two\_2.png kept its backslash and missed its imeta entry once two or
more trailing images formed a carousel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>

@johnmatthewtennant johnmatthewtennant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed 4996e40 with the complete teammate-review workflow (60 lenses, 120 outcomes). The escaped voice-note segmentation fix matches the rendering lookup; no correctness blockers introduced by this change. The separately reported gallery gap predates this patch and is accepted for a later push. No Flutter tests or device journey run by me.

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
Per review, cover escaped attachment segmentation in the existing
attachments-stay-outside-bubbles journey instead of a standalone test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Nov 2, 2026
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: e588abac22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@johnmatthewtennant johnmatthewtennant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed e588aba with the complete teammate-review workflow (62 lenses, 124 outcomes). The escaped attachment segmentation and gallery destination fixes match their rendering paths; no correctness blockers found. The existing bubble and carousel journeys cover the changed lookups. No Flutter tests or device journey run by me.

@brow
brow merged commit 35df866 into main Nov 2, 2026
53 checks passed
@brow
brow deleted the mobile-escaped-voice-note-segment branch November 2, 2026 19:18
brow pushed a commit that referenced this pull request Nov 2, 2026
A real-message diff against desktop's parser found three differences:

- A table typed directly under a caption line showed raw pipes. GFM lets
  a table interrupt a paragraph; plusparse doesn't, so a blank line is
  inserted before such a table.
- `> [!NOTE]` quotes became alerts. Desktop has no alerts, so they stay
  quotes.
- An unclosed backtick run paired with a run in a later block, flipping
  later code and prose, so math-delimiter escapes leaked into code as
  entities. A code span now ends with its paragraph.

Bubble segmentation now unescapes a link destination itself before the
imeta lookup, since content no longer arrives shielded (#8940 relied on
that). Also pins gpt_markdown to exactly 1.3.4.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
brow pushed a commit that referenced this pull request Nov 4, 2026
A real-message diff against desktop's parser found three differences:

- A table typed directly under a caption line showed raw pipes. GFM lets
  a table interrupt a paragraph; plusparse doesn't, so a blank line is
  inserted before such a table.
- `> [!NOTE]` quotes became alerts. Desktop has no alerts, so they stay
  quotes.
- An unclosed backtick run paired with a run in a later block, flipping
  later code and prose, so math-delimiter escapes leaked into code as
  entities. A code span now ends with its paragraph.

Bubble segmentation now unescapes a link destination itself before the
imeta lookup, since content no longer arrives shielded (#8940 relied on
that). Also pins gpt_markdown to exactly 1.3.4.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
brow pushed a commit that referenced this pull request Nov 12, 2026
A real-message diff against desktop's parser found three differences:

- A table typed directly under a caption line showed raw pipes. GFM lets
  a table interrupt a paragraph; plusparse doesn't, so a blank line is
  inserted before such a table.
- `> [!NOTE]` quotes became alerts. Desktop has no alerts, so they stay
  quotes.
- An unclosed backtick run paired with a run in a later block, flipping
  later code and prose, so math-delimiter escapes leaked into code as
  entities. A code span now ends with its paragraph.

Bubble segmentation now unescapes a link destination itself before the
imeta lookup, since content no longer arrives shielded (#8940 relied on
that). Also pins gpt_markdown to exactly 1.3.4.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
brow pushed a commit that referenced this pull request Nov 13, 2026
A real-message diff against desktop's parser found three differences:

- A table typed directly under a caption line showed raw pipes. GFM lets
  a table interrupt a paragraph; plusparse doesn't, so a blank line is
  inserted before such a table.
- `> [!NOTE]` quotes became alerts. Desktop has no alerts, so they stay
  quotes.
- An unclosed backtick run paired with a run in a later block, flipping
  later code and prose, so math-delimiter escapes leaked into code as
  entities. A code span now ends with its paragraph.

Bubble segmentation now unescapes a link destination itself before the
imeta lookup, since content no longer arrives shielded (#8940 relied on
that). Also pins gpt_markdown to exactly 1.3.4.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
brow added a commit that referenced this pull request Nov 13, 2026
Message Markdown on mobile moves from gpt_markdown 1.2.1's legacy regex
pipeline to the CommonMark parser that 1.3 added ("plusparse"). Before
this, passing `inlineComponents` (now deprecated, removed in 2.0.0) kept
every message on the legacy pipeline.

### What changes for users

Rendering moves closer to desktop (react-markdown + remark-gfm):

- `_italic_` and `__bold__` work, backslash escapes and HTML entities
decode, and `[a [b] c](url)` labels are correct.
- Inline code follows CommonMark pairing: `"```npm install```"` in a
sentence is inline code, and four-backtick fences work.
- Setext headings, reference links, footnotes, `~~~` fences, `1)` lists
and CommonMark list/quote continuation render.
- HTML comments are hidden, as on desktop.
- `\[`, `\]`, `\(` and `\)` stay literal, as on desktop. Plusparse
hard-codes them as LaTeX delimiters, so they're written as entities
before rendering.
- Side effect: an unfenced `__init__` now renders as bold "init", and a
line over `---` becomes a heading, same as desktop.

Device benchmark (iPhone SE, profile build, 600 synthetic messages, 3
launches per build): scrolling spends 9% less time building, and frames
with more than 8 ms of build drop from 63 to 22. Jumping onto unbuilt
messages is 11% cheaper, and raster time is unchanged.

App size: the release app grows about 4.6 MB uncompressed (80.6 to 85.2
MB), mostly Dart code from the new math and highlighting packages.

### Before and after

Synthetic messages that reproduce differences found in real channel
history, rendered through `MessageContent` on main and on this branch:


![01-escaped-brackets-in-code](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--01-escaped-brackets-in-code.png)


![02-inline-triple-backticks](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--02-inline-triple-backticks.png)


![03-four-backtick-fence](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--03-four-backtick-fence.png)


![04-entities](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--04-entities.png)


![05-stars-and-underscores](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--05-stars-and-underscores.png)


![06-ordered-list-paren](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--06-ordered-list-paren.png)


![07-setext-heading](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--07-setext-heading.png)


![08-dunder](https://raw.githubusercontent.com/block/buzz/49600d2f9b87c2c41fd1bcac450efd345de0d0db/pr-8945--08-dunder.png)

### Demo

A scroll through synthetic messages covering common formatting
(headings, lists, quotes, links, highlighted code, a table, a task list,
escapes), recorded on an iPhone 17 Pro simulator running this branch:


https://github.com/user-attachments/assets/bdc9680c-d20b-41ec-8e1a-a5b51c602f9d

### How

- Mentions, custom emoji and `#channel` tokens are `InlinePattern`s.
Links go through `inlineLinkBuilder` into the existing `_buildLink`, so
chips, media links and taps are unchanged.
- Removed workarounds the parser makes unnecessary:
  - the render-path escape shielding from #8925
- the `_MarkdownEscapeMd`, `_MarkdownLabelEntityMd` and `_MessageLinkMd`
components
  - the zero-width prefix before a leading `#channel`

Plain-text previews and gallery helpers keep their own escape handling.
`normalizeBareLinks` stays because attachment and gallery detection
depend on its output. Bubble segmentation unescapes a link destination
itself before its imeta lookup, since content no longer arrives
shielded.
- Kept desktop parity where plusparse differs from GFM:
- a table typed directly under a caption line gets a blank line inserted
before it, since plusparse can't start a table mid-paragraph
- `> [!NOTE]` quotes render as quotes (`alertBuilder` returns
`asBlockQuote()`), since desktop has no alerts
- `mapMarkdownProse` now ends an unclosed backtick run at its paragraph.
Before, it could pair with a run in a later block and flip code and
prose for the rest of the message.
- Synthesized autolink labels escape backslashes instead of using
`&#92;`, which the parser leaves undecoded in a URL-shaped label.
- Repo/PR/issue chips keep their own semantics node.
- Forum card previews clip at their 120 px cap instead of overflowing:
blocks now lay out as a column.
- Pinned to exactly 1.3.4. 1.4.0 renders HTML by default and themes
through the separate `material_ui` package, which would need a theme
bridge.

### Validation

- Full mobile suite passes and analyze is clean at 78b9c1a.
- All of #8925's and #8940's backslash-escape cases pass on the new
parser, including the strike-across-lines case that used to be a known
limitation.
- Real-message diff: 2,259 recent messages from 13 busy channels,
rendered through `MessageContent` on main and on this branch. 29
differed in text or styling. On inline and fenced code, the branch
matched desktop's parser (remark-gfm) in 23 of them and main in 7. The
review found the three parity gaps above, now fixed. The rest are
CommonMark behavior that desktop shares.
- Added tests, each confirmed to fail without its fix:
  - a leading `#channel` renders as a link, not a heading
  - a table under its caption renders as a table
  - an alert-marker quote stays a quote
  - an unclosed backtick run doesn't flip later code
- the transcript tests detect a rendered link by its style; the old
`LinkButton` check passed even with autolink on
- Automated simulator run (iPhone 17 Pro, iOS 27, debug build, signed in
to a Builderlab community): channel list, two channels with history
scrolled back several pages (mentions, link previews, thread summaries,
images), an agent Solo chat with long agent messages, and the inbox. No
rendering errors or layout breaks; the Solo chat screen also matched
main's build side by side.

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

---------

Signed-off-by: Tom Brow <[email protected]>
Co-authored-by: Tom Brow <[email protected]>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>

This branch was previously deployed

1 inactive deployment
codex-review — e588abac Deployed Nov 2, 2026 by brow via Run Codex Security Review #11879
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants