Skip to content

fix(mobile): draw a whole character in an avatar fallback - #5998

Open
Chessing234 wants to merge 3 commits into
block:mainfrom
Chessing234:fix/mobile-avatar-initial-grapheme
Open

fix(mobile): draw a whole character in an avatar fallback#5998
Chessing234 wants to merge 3 commits into
block:mainfrom
Chessing234:fix/mobile-avatar-initial-grapheme

Conversation

@Chessing234

@Chessing234 Chessing234 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

The mobile half of #5992, which fixed the same bug in desktop's getInitials. No issue filed.

Seven avatar fallbacks took their letter with label[0] or label.substring(0, 1). Both return a single UTF-16 code unit, so a display name starting with an emoji or a CJK Extension B character — ordinary in Chinese and Japanese given names — produced half a surrogate pair, and the avatar drew :

  • UserProfile.initial (the profile model every surface reads)
  • invite and channel-management labels
  • the members sheet
  • both pulse note surfaces
  • people results in search

Reading grapheme clusters also keeps a base letter together with its combining marks, so a Devanagari or Burmese name keeps its vowel sign instead of showing a bare consonant.

This follows the codebase's own practice rather than introducing anything. characters is already a direct dependency in pubspec.yaml, and mobile already reaches for it wherever text has to be treated as text — shared/emoji/emoji_only.dart iterates .characters, and the message preview in features/channels/message_actions.dart truncates by .characters rather than by index. The avatar fallbacks were the ones that hadn't caught up.

The pubkey-derived fallbacks (forum_post_card.dart, forum_thread_page.dart, user_profile_sheet.dart) are deliberately untouched: hex is ASCII, and taking one code unit of it was never wrong.

Verification note (corrected). An earlier version of this description said there was no Flutter or Dart SDK on this machine. That was wrong — the repo pins both through hermit (./bin/dart, ./bin/flutter), which I had been using all along for ./bin/cargo without noticing. I am running the mobile checks and will post the results in the thread.

Note: I'm an outside contributor, so the workflow runs here sit at action_required until a maintainer approves them; only the DCO check reports on its own.

`label[0]` and `label.substring(0, 1)` return one UTF-16 code unit. Anything
outside the Basic Multilingual Plane — every emoji, and CJK Extension B, which
appears in ordinary Chinese and Japanese given names — is stored as a
surrogate pair, so those return half of one: not a character, and drawn as `�`
in the avatar.

Reading grapheme clusters also keeps a base letter together with its combining
marks, so a Devanagari or Burmese name keeps its vowel sign instead of showing
a bare consonant.

The `characters` package is already a direct dependency and the codebase
already reaches for it where text has to be treated as text —
`shared/emoji/emoji_only.dart` and the message preview in
`features/channels/message_actions.dart`. This is the same rule, for avatars.

No call sites yet.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Seven avatar fallbacks took their letter with `label[0]` or
`substring(0, 1)`. Both return a UTF-16 code unit, so a display name starting
with an emoji or a CJK Extension B character produced half a surrogate pair
and the avatar drew `�`: the profile model, invite and channel-management
labels, the members sheet, both pulse note surfaces, and people results in
search.

They now go through `avatarInitial`, which also keeps a combining mark with
the letter it belongs to, so a Devanagari or Burmese name no longer loses its
vowel sign.

The pubkey-derived fallbacks elsewhere are left alone — hex is ASCII, and
taking one code unit of it was never wrong.

Signed-off-by: Taksh <takshkothari09@gmail.com>
@Chessing234
Chessing234 requested a review from a team as a code owner August 16, 2026 00:15

@themiguelamador themiguelamador 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.

The shared avatarInitial helper is correct and its focused test passes, but the same bug remains in two avatar fallbacks:

  • mobile/lib/features/channels/compose_bar/suggestions.dart still uses name[0].toUpperCase(). This still renders half a surrogate pair for astral names and can throw when a candidate label is empty.
  • mobile/lib/features/channels/channels_page/community.dart still uses trimmedName.substring(0, 1).toUpperCase(), so community names beginning with an astral character still render U+FFFD.

Please route both through avatarInitial (adding the imports to the parent part libraries). Minor documentation correction: “every emoji” is outside the BMP is too broad; “most emoji” is accurate.

I applied and verified this exact patch locally: the focused Flutter test passes, flutter analyze reports no issues, and the mobile pre-commit gate passes. I attempted to push the signed-off fix directly because maintainer_can_modify is true, but GitHub returned 403 for my account.

Review found two the earlier commit missed, both inside `part of` libraries
so their imports belong on the parent:

- `compose_bar/suggestions.dart` used `name[0].toUpperCase()`, which draws
  half a surrogate pair for an astral name and throws outright when a
  candidate label is empty
- `channels_page/community.dart` used `trimmedName.substring(0, 1)`, so a
  community name starting with an astral character rendered U+FFFD

`avatarInitial` covers the empty case with `?`, which is what both sites
already wanted.

Also corrects the doc: "every emoji" is outside the BMP was too broad. Most
are; ☺ and ✌ are not.

Signed-off-by: Taksh <takshkothari09@gmail.com>
@Chessing234

Copy link
Copy Markdown
Contributor Author

both confirmed and fixed in c46fb059.

  • compose_bar/suggestions.dart:50name[0].toUpperCase(), now avatarInitial(name). you're right that it also throws outright on an empty label rather than just rendering badly; avatarInitial returns ?, which is what the surrounding code already wanted.
  • channels_page/community.dart:493trimmedName.substring(0, 1), now avatarInitial(trimmedName ?? ''), keeping the existing null/empty → ? behaviour.

both are part of libraries as you said, so the imports went on compose_bar.dart and channels_page.dart rather than the part files.

grep -rn "name\[0\]\|substring(0, 1)" mobile/lib is now clean apart from the helper's own doc comment. the pubkey-derived fallbacks in forum_post_card.dart, forum_thread_page.dart, user_profile_sheet.dart, small_avatar.dart, inbox_row.dart and reaction_row.dart are deliberately untouched — hex is ascii, so one code unit was never wrong there. say the word if you'd rather they were uniform anyway.

wording: fixed — "most emoji" in the doc.

i deleted my earlier comment on this pr because it was wrong on a point of fact. it said there was no flutter or dart sdk on this machine and that i therefore couldn't verify mobile changes. that isn't true — the repo pins both through hermit (./bin/dart, ./bin/flutter), which i'd been using all along for ./bin/cargo without noticing. i'm bootstrapping it now and will post flutter analyze and flutter test results for this branch rather than leaving it on your verification.

@themiguelamador themiguelamador 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.

Re-reviewed current head c46fb059a. This update resolves every prior finding: the compose mention and community avatar fallbacks now use avatarInitial, the imports are correctly placed in their parent libraries, and the Unicode documentation is accurate.

Verified with flutter analyze, Dart formatting, and the affected helper, compose-bar, and channels-page test files (129/129 passed). No remaining findings.

@Chessing234

Copy link
Copy Markdown
Contributor Author

verification for this branch, now that i have the toolchain i wrongly said i didn't:

  • dart format --output=none --set-exit-if-changed . — clean
  • flutter analyze — no issues found
  • flutter test1,422 passed

so your local run and mine agree. nothing further needed from you on this one.

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.

2 participants