Skip to content

feat(ui): size-aware avatar renditions + BlurHash placeholders#1131

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/avatar-image-rendition
Jul 24, 2026
Merged

feat(ui): size-aware avatar renditions + BlurHash placeholders#1131
bmc08gt merged 1 commit into
code/cashfrom
feat/avatar-image-rendition

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

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 (THUMBNAIL 32/160/320, DISPLAY 800/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 (ORIGINAL excluded as arbitrarily large; degrades to the existing role ladder, capped at DISPLAY, when renditions carry no dimensions).
    • renditionBelow(px): next-smaller rendition, used as a progressive placeholder.
    • blurhash(): first available BlurHash.
  • ContactAvatar — measures its bounds with BoxWithConstraints and requests the matching rendition. Bridges the load with two placeholders: an instant self-contained BlurHash preview, and any already-cached smaller rendition via placeholderMemoryCacheKey. A deterministic memoryCacheKey(url) lets renditions be reused across surfaces (e.g. the info card at 320 reuses the 160 the list already fetched). New MediaItem overload; 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 + rememberBlurHashPainter Compose 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:testDebugUnitTest pass; :notifications and :features:myaccount compile clean.

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
@github-actions github-actions Bot added area: network gRPC, connectivity, API, exchange rates area: notifications Push notifications, in-app messaging type: feature New functionality labels Jul 24, 2026
@bmc08gt
bmc08gt merged commit 74563db into code/cash Jul 24, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/avatar-image-rendition branch July 24, 2026 13:12
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: network gRPC, connectivity, API, exchange rates area: notifications Push notifications, in-app messaging type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant