Files
buzz/docs/adr/0002-notification-message-text.md
Tom Brow 95dd9d67dd feat(mobile/ios): show the message's own text in notifications (#9346)
Expanding a Buzz notification today shows the same stub as the banner:
one line of at most 180 characters, with code replaced by `[code]` and
links by `[link]`. A long message can only be read by opening the app.

The notification body is now the message's own text. It keeps line
breaks, the text of code and bare URLs, and drops the Markdown syntax
around them (`[text](url)` shows `text`). It is capped at 256 UTF-16
units, which is the most iOS shows even when a notification is expanded.
iOS cuts longer bodies mid-word with no ellipsis, so the cap stops at
255 units and adds "…" without splitting a character. A link label,
image alt text or bold run may contain code, and a code fence with no
closing fence runs to the end of the message. The body is built from at
most the first 4096 UTF-16 units of the message, so very long or hostile
Markdown stays fast (under 300 ms for a 256 KB message in an optimized
build); when that cut drops text, the body also ends with "…".

| Before (expanded) | After (expanded) |
| --- | --- |
| <img width="300" alt="Before"
src="https://github.com/user-attachments/assets/7201791f-9556-446a-86d2-a50f024cf9dc">
| <img width="300" alt="After"
src="https://github.com/user-attachments/assets/3fa52db8-ebcf-470f-9175-47fe5ef4ed12">
|

[ADR 0002](docs/adr/0002-notification-message-text.md) records the rules
so other mobile platforms follow them, and the cases in
`test-fixtures/notification-body.json` are the contract. The iOS tests
run every case, and Android's notification code can run the same file
when it lands.

**What gets better, what doesn't get worse:** only the body text of
message notifications changes. The title, subtitle, grouping, avatars,
navigation and dedupe are untouched, and the change lives in one pure
function. A notification can now show code and URLs from the message, as
the message itself does, and the user's lock-screen preview setting
still applies.

### Validation

- Added tests: `testPreviewBodyMatchesSharedNotificationBodyCases` runs
the 30 shared cases (links, mentions, images, fenced and inline code,
code inside link labels and bold runs, Markdown inside code, unclosed
and cut fences, whitespace, the cap at 256 and 257 units, emoji and ZWJ
sequences at the cap, the 4096-unit input cut). The full BuzzPushKit
suite passes: 112 XCTest and 70 Swift Testing tests.
- iOS limit measured on an iPhone 17 Pro simulator (iOS 27) with a local
notification: the expanded body and its accessibility label both stopped
at exactly 256 UTF-16 units (256 ASCII characters, 128 emoji), cut
mid-word with no ellipsis. A 40-line body showed about 31 lines.
- The screenshots show this function's before and after output for the
same message, posted as a local notification. The simulator doesn't run
the Notification Service Extension for `simctl push`, so the full push
path (relay fetch through to display) was not exercised. An iPad was not
measured.
- Human testing is waived; the automated run above covered display of
the changed body only, since nothing else in the app changes.

🤖 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: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 07:17:32 -08:00

2.7 KiB

Notification message text

A mobile push notification for a message shows the message's own text as its notification body, derived the same way on every platform. The body keeps the message's line breaks, the text of code, and bare URLs. It drops the Markdown syntax around them: code fences and their language, inline-code backticks, the […](…) of links and images (keeping the link text or image alt text), and ** and ~~ outside code. A link label or a bold run may contain code, and a code fence with no closing fence runs to the end of the text, as in CommonMark. Runs of spaces collapse outside code, and three or more line breaks become one blank line. Other Markdown (quotes, lists, headings, single * or _) stays as typed, because stripping it would also mangle ordinary text such as snake_case and code identifiers.

The body is cut only where the platform would otherwise cut it. iOS shows at most 256 UTF-16 code units of a body, even when the notification is expanded, and cuts the rest without an ellipsis, often mid-word. So the body is at most 256 UTF-16 code units: a longer one keeps whole characters up to 255 units and ends with "…", so the reader can tell there is more. A platform that shows more of a body than iOS may raise its own cap to what it shows, but never lower it below that. The body is built from at most the first 4096 UTF-16 code units of the content (whole characters), which bounds the work for very long messages; when that cut drops text, the body ends with "…" too.

The cases in test-fixtures/notification-body.json are the contract. Each platform's notification code runs them in its tests.

Considered options

  • No cap of our own. Rejected because iOS would then cut long messages silently mid-word, which reads as the whole message.
  • A short preview with code and links replaced by [code] and [link]. This was the previous design: one line of at most 180 characters. Expanding a notification showed the same stub, so a long message could only be read by opening the app.
  • A custom notification view (a Notification Content Extension on iOS). Needed to show more than the platform's own limit, or formatted text. It is a separate, larger change that builds on this body rather than replacing it.

Consequences

Notifications can show code and URLs that a message contains, as the message itself does, subject to the user's own system setting for showing previews on a locked device.

iOS also limits an expanded notification's height, so a body made of many short lines can be cut by height before it reaches 256 code units.