feat(ui): size-aware avatar renditions + BlurHash placeholders#1131
Merged
Conversation
Profile-picture avatars were grabbing an arbitrary (often the smallest, 32px) rendition by role, which looked grainy on larger surfaces. The server ships several sizes per role (THUMBNAIL 32/160/320, DISPLAY 800/1600), so selection now keys off the avatar's measured pixel size instead of role alone. - MediaItem: add renditionForSize / urlForSize (smallest rendition whose longest side >= target, ORIGINAL excluded, degrades to the role ladder when dimensions are absent), renditionBelow (next-smaller, for progressive placeholders), and blurhash(). - ContactAvatar: measure bounds via BoxWithConstraints and request the matching rendition; bridge the load with an instant BlurHash preview plus any already-cached smaller rendition (placeholderMemoryCacheKey), with a deterministic memoryCacheKey so renditions are reused across surfaces. New MediaItem overload; own-profile + tips call sites use it. - NotificationService: size the person icon to the platform large-icon dimension instead of the grainy 32px thumbnail. - BlurHash: public-domain decoder + rememberBlurHashPainter helper. - Tests for MediaItem selection and BlurHash decoding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGyi9wiGyHv3AHMPSfQ2iM
This was referenced Jul 24, 2026
bmc08gt
added a commit
to code-payments/code-ios-app
that referenced
this pull request
Jul 25, 2026
…rs (#515) Carry the thumbnail rendition's BlurHash on ProfilePicture and render it as an instant blurred preview while a counterpart's avatar downloads, instead of a blank gradient. Ported from android code-payments/code-android-app#1131 (lean scope: BlurHash + placeholder; the existing largest-thumbnail selection is kept, avatars are fixed-size). - BlurHash: public-domain decoder + process-wide decode cache (FlipcashUI) - ProfilePicture.thumbnailBlurhash, read off the selected thumbnail rendition - SQLite: persist the blurhash (new column, SQLiteVersion 25 -> 26) - ContactAvatarView renders the blurhash while imageData is nil (precedence: bytes -> blurhash -> monogram) - Threaded through the tip/DM counterpart surfaces (conversation title + profile card, tip conversations list) - Tests: BlurHash decoder vectors; ProfilePicture blurhash extraction + legacy JSON back-compat
bmc08gt
added a commit
to code-payments/code-ios-app
that referenced
this pull request
Jul 25, 2026
* feat: add BlurHash progressive placeholders for profile-picture avatars Carry the thumbnail rendition's BlurHash on ProfilePicture and render it as an instant blurred preview while a counterpart's avatar downloads, instead of a blank gradient. Ported from android code-payments/code-android-app#1131 (lean scope: BlurHash + placeholder; the existing largest-thumbnail selection is kept, avatars are fixed-size). - BlurHash: public-domain decoder + process-wide decode cache (FlipcashUI) - ProfilePicture.thumbnailBlurhash, read off the selected thumbnail rendition - SQLite: persist the blurhash (new column, SQLiteVersion 25 -> 26) - ContactAvatarView renders the blurhash while imageData is nil (precedence: bytes -> blurhash -> monogram) - Threaded through the tip/DM counterpart surfaces (conversation title + profile card, tip conversations list) - Tests: BlurHash decoder vectors; ProfilePicture blurhash extraction + legacy JSON back-compat * chore: add fetch-protos skill and proto-change-tracer agent Adds a /fetch-protos skill that regenerates the Core and Payments Swift bindings via Scripts/run, verifies the build, and dispatches a proto-change-tracer agent to map field/RPC changes through the service -> client -> consumer chain. * feat: sync protos, add cash tip action and push chat type Regenerate Core and Payments bindings. Core adds three additive fields: CashContent.action, Metadata.is_hidden, and Payload.chat_metadata. Wire CashContent.action through the chat stack: ConversationMessage carries a CashAction, persisted in a new conversation_message.cashAction column (SQLiteVersion 26 -> 27), and ChatCashContent renders a shared sent/tipped caption in both the in-app cash card and the notification transcript. Add NotificationPayload.chatType(_:) exposing the push's DM kind from Payload.chat_metadata, alongside the existing chatID/isContactJoin accessors. is_hidden is intentionally left unwired (no user blocking yet).
bmc08gt
added a commit
to code-payments/code-ios-app
that referenced
this pull request
Jul 25, 2026
* feat: add BlurHash progressive placeholders for profile-picture avatars Carry the thumbnail rendition's BlurHash on ProfilePicture and render it as an instant blurred preview while a counterpart's avatar downloads, instead of a blank gradient. Ported from android code-payments/code-android-app#1131 (lean scope: BlurHash + placeholder; the existing largest-thumbnail selection is kept, avatars are fixed-size). - BlurHash: public-domain decoder + process-wide decode cache (FlipcashUI) - ProfilePicture.thumbnailBlurhash, read off the selected thumbnail rendition - SQLite: persist the blurhash (new column, SQLiteVersion 25 -> 26) - ContactAvatarView renders the blurhash while imageData is nil (precedence: bytes -> blurhash -> monogram) - Threaded through the tip/DM counterpart surfaces (conversation title + profile card, tip conversations list) - Tests: BlurHash decoder vectors; ProfilePicture blurhash extraction + legacy JSON back-compat * feat: rename Tipcode to Tip Card across chat surfaces Align the tip-DM counterpart label with the product name: the enum case becomes .tipcard and the subtitle reads "via Tip Card". * feat: add a dismiss handler to DialogItem Carry an optional onDismiss on DialogItem, fired when the dialog leaves the screen by any means (action button, cancel, or scrim tap), so callers can tear down state the dialog was fronting regardless of how it was dismissed. * feat: polish tipcard rendering Render the tipcard over a frosted BackdropBlur and refine the card layout. * feat: refine tip amount and token selection - Clear the selected preset and custom amount when the tip currency changes, so a stale amount can't carry to a token that may not cover it. - Format the custom-amount chip to drop the fraction only when whole ($11, not $11.00) while keeping real fractions ($2.50). - Disable currencies in the picker that can't fund the tip minimum via a new isEnabled predicate on SelectCurrencyScreen. - Defer the giveable-balance gate to sheet presentation so the card always shows, blocking only the Send a Tip sheet via the dialog's onDismiss. * fix: position the tipcard to clear the Send a Tip sheet Raise the tipcard just enough to keep a fixed 42pt gap above the Send a Tip sheet, and only when the sheet would crowd it. Measure the sheet's content height to derive its top edge, and animate the canvas offset so the card is nudged as the sheet slides in rather than jumping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Profile-picture avatars were selecting a rendition by role alone and often landed on the smallest (32px)
THUMBNAIL, which looks grainy on anything larger than a favicon. The server derives several sizes per role (THUMBNAIL32/160/320,DISPLAY800/1600), so role is ambiguous — the pixel size of the surface is what should drive selection.What
MediaItem— size-aware selection:renditionForSize(px)/urlForSize(px): smallest available rendition whose longest side ≥ target (ORIGINALexcluded as arbitrarily large; degrades to the existing role ladder, capped atDISPLAY, when renditions carry no dimensions).renditionBelow(px): next-smaller rendition, used as a progressive placeholder.blurhash(): first available BlurHash.ContactAvatar— measures its bounds withBoxWithConstraintsand requests the matching rendition. Bridges the load with two placeholders: an instant self-contained BlurHash preview, and any already-cached smaller rendition viaplaceholderMemoryCacheKey. A deterministicmemoryCacheKey(url)lets renditions be reused across surfaces (e.g. the info card at 320 reuses the 160 the list already fetched). NewMediaItemoverload; own-profile header and tips-list call sites migrated to it.NotificationService— sizes the person icon to the platform large-icon dimension (density-scaled) instead of the grainy 32px thumbnail.BlurHash— public-domain decoder +rememberBlurHashPainterCompose helper (Coil-free).Tests
MediaItemTest— rendition selection (smallest ≥ target, fallback to largest, ORIGINAL handling, skips uploading, degrades to role ladder),renditionBelow,urlForSize,blurhash.BlurHashTest(Robolectric) — valid decode to size, null/blank/malformed handling, dimension validation.Verified:
:services:flipcash:testDebugUnitTest,:apps:flipcash:shared:common-ui:testDebugUnitTestpass;:notificationsand:features:myaccountcompile clean.