Fix mobile heart and warning emoji with native font fallback (#7842)

Mobile heart and warning reactions and message text render as monochrome
symbols because bundled Inter supplies U+2764 and U+26A0. Remove only
those two character mappings from both regular and italic Inter fonts so
Flutter uses native font fallback. No production Dart or Markdown
parsing changes are needed.

The reproducible font-processing script verifies that every other
mapping and font table is unchanged, apart from the required checksum.
Glyphs, metrics, language coverage and symbols such as © ® ™ ↑ ↓ − ∕ are
preserved; the bundled Inter license is retained.

Validated on an actual iPhone 17 Pro simulator running iOS 26.5.
Reaction hearts and warnings remain centered within 0.33 logical pixels
of the pill center. Native screenshot checks cover reactions and plain,
emoji, explicit text-style, bold, italic, and enlarged emoji-only
message hearts and warnings, plus pixel-identical preserved text
samples.

**Fallback behavior:** on iOS 26.5, plain ❤ / ⚠ and explicit text-style
❤︎ / ⚠︎ also render as color emoji. Native font metrics add one logical
pixel to each of the three mixed-message lines in this fixture. Android
visual behavior has not been validated.

### Reactions

| Before | After |
| --- | --- |
| ![Monochrome hearts and warnings in
reactions](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--before-reactions.png)
| ![Centered native color hearts and warnings in
reactions](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--after-reactions.png)
|

### Message text

| Before | After |
| --- | --- |
| ![Monochrome message hearts and warnings with preserved
symbols](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--before-messages.png)
| ![Native color message hearts and warnings with preserved
symbols](https://raw.githubusercontent.com/block/buzz/cf60b78c79fbdc4ac314ba74f3fd27b76c823755/pr-7842--after-messages.png)
|

Images are cropped native simulator screenshots of production widgets
with synthetic content. Before uses the original bundled fonts; after
uses the processed fonts.

Added tests: the existing mobile CI test lane now checks both bundled
fonts against the verified generated artifacts, preventing restoration
of the old mappings or unintended font changes.

Validation: the two asset tests, font-table verification, byte-identical
regeneration, native screenshot regression checks, and Flutter analysis
pass. The screenshot test fails against the original rendering. Pre-push
checks, including the full mobile suite, pass. Full repository
validation also passes. The enlarged emoji-only screenshot check passes
and rejects the old normal-sized fixture. GitHub CI, Codex review, and
the security review pass for commit 538167a788. Both prior Codex
findings are addressed and resolved.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-09-24 07:30:30 -07:00
committed by GitHub
parent 4544ba1713
commit 9462d8884a
7 changed files with 324 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
# Inter heart and warning fallback
The two Inter 4.001 variable fonts are derived from the previously bundled
fonts by removing only U+2764 and U+26A0 from their Unicode character maps.
Flutter can then choose a native font for heart and warning characters,
including emoji presentation
in reactions and message text. No glyphs, language coverage, other symbols,
font metrics, or variation tables are removed. The license remains in
`Inter-LICENSE.txt`.
To reproduce from the repository root:
```sh
mkdir -p /tmp/buzz-inter-source
for font in InterVariable.ttf InterVariable-Italic.ttf; do
git show ec7ea38f62ea917f15e85a678bc94f3bbee5bb64:mobile/assets/fonts/$font > /tmp/buzz-inter-source/$font
done
python3 -m venv /tmp/buzz-inter-tools
/tmp/buzz-inter-tools/bin/pip install fonttools==4.60.1
/tmp/buzz-inter-tools/bin/python mobile/scripts/prepare-inter.py /tmp/buzz-inter-source
/tmp/buzz-inter-tools/bin/python mobile/scripts/prepare-inter.py /tmp/buzz-inter-source --check
```
The script checks pristine source hashes and verifies every character mapping
and every other font table. Only the `cmap` table and the font checksum may
change. Repeated generation from the same sources produces identical bytes.
©, ®, ™, arrows, math symbols, accents, and supported languages
keep their original Inter mappings.
Use `test/visual/heart_sim_app.dart` on an actual iOS simulator to compare
reactions, plain/emoji/text-presentation hearts, bold/italic message text,
warning signs, and preserved symbols. Font fallback for plain and explicit
text-presentation hearts and warnings is platform-dependent; do not infer their appearance
from a macOS widget-test renderer. On iOS 26.5, all three heart and warning
presentations fall back to the color emoji, including explicit VS15. Message line metrics
can change with the fallback font.
The native screenshot regression check (requires Pillow) covers both reaction
states, each heart and warning presentation, bold/italic text, and unchanged
symbols:
```sh
python3 mobile/test/visual/verify_heart_screenshots.py before.png after.png
```
Use original 1206×2622 captures from the documented simulator and fixture for
this check. PR images may be cropped afterward for readability.
`test/shared/fonts/inter_assets_test.dart` pins the verified generated font
artifacts in the existing Flutter test lane. Restoring either original font or
changing any other font data fails CI. When upgrading Inter, verify the
transformation and native rendering before updating the expected digests.