Skip to content

feat(emoji): skin-tone selector + grid scroll-cancellation in library sheet#345

Open
dmnyc wants to merge 1 commit into
mainfrom
feat/emoji-skin-tone-selector
Open

feat(emoji): skin-tone selector + grid scroll-cancellation in library sheet#345
dmnyc wants to merge 1 commit into
mainfrom
feat/emoji-skin-tone-selector

Conversation

@dmnyc

@dmnyc dmnyc commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two related changes to the full emoji library sheet — both shippable independently of the catalog generator and the "More reactions" hit-target work.

Skin-tone selector: a single horizontal row of 6 swatches (default + 5 Fitzpatrick tones) sits between the search field and the tab strip. Tapping a swatch persists the choice in UserDefaults via EmojiSkinTonePreference and every tone-capable cell in the grid re-renders in that tone immediately. The mechanism matches what other clients do — a single global preference rather than per-pick popovers, which conflicted with Button's tap recognizer when tried first.

Grid scroll-cancellation: the emoji and custom-image cell wrappers now use .contentShape(Rectangle()).onTapGesture instead of Button { } .buttonStyle(.plain). Button inside a LazyVGrid inside a ScrollView only cancels its tap on substantial finger movement, so a quick flick to scroll the long grid was registering as a pick on release. .onTapGesture cancels on any drag past the hit-test threshold, which is what the user expects for a scroll.

SkinTone.swift introduces:

  • EmojiSkinTone enum mapping each tone to its Fitzpatrick modifier codepoint, with a preview-swatch helper.
  • EmojiSkinTonePreference wrapper around UserDefaults that posts a notification when the value changes so live sheets refresh.
  • String.applyingSkinTone(_:) and removingSkinTones() helpers that handle both plain base emoji (append modifier) and single-human-glyph ZWJ sequences (insert modifier before the first ZWJ, with VS-16 awareness). Multi-person sequences are intentionally out of scope.
  • EmojiToneCapability — curated Set<String> of tone-capable base emoji. Membership check strips VS-16 so "👋" and "👋\u{FE0F}" both match.

Files

  • SkinTone.swift — new, ~135 lines
  • EmojiLibrarySheet.swiftskinToneSelector row, toneAwareCell rendering, preferredTone state + notification observer; Button.onTapGesture swap on both grids
  • wisp.xcodeproj/project.pbxproj — registers SkinTone.swift

Test plan

  • Library sheet shows the "Skin tone" row at the top with 6 swatches, active one outlined
  • Tap the dark swatch → every hand / body / person cell re-renders in dark tone
  • Dismiss + re-open → dark swatch still selected (persisted)
  • Pick a toned 👍 → reaction sent with the tone applied
  • Long-press grid + drag → scrolls cleanly, no accidental pick on release
  • Build clean for iOS Simulator

Closes #304.

… sheet

Two related changes to the full emoji library sheet — both shippable
independently of the catalog generator and the "More reactions" hit
target work.

Skin-tone selector: a single horizontal row of 6 swatches (default + 5
Fitzpatrick tones) sits between the search field and the tab strip.
Tapping a swatch persists the choice in `UserDefaults` via
`EmojiSkinTonePreference` and every tone-capable cell in the grid
re-renders in that tone immediately. The mechanism matches what other
clients do — a single global preference rather than per-pick popovers,
which conflicted with `Button`'s tap recognizer when tried first.

`SkinTone.swift` introduces:
- `EmojiSkinTone` enum mapping each tone to its Fitzpatrick modifier
  codepoint, with a preview-swatch helper.
- `EmojiSkinTonePreference` wrapper around `UserDefaults` that posts a
  notification when the value changes so live sheets refresh.
- `String.applyingSkinTone(_:)` and `removingSkinTones()` helpers that
  handle both plain base emoji (append modifier) and single-human-glyph
  ZWJ sequences (insert modifier before the first ZWJ, with VS-16
  awareness). Multi-person sequences are intentionally out of scope.
- `EmojiToneCapability` — curated `Set<String>` of tone-capable base
  emoji. Membership check strips VS-16 so "👋" and "👋\u{FE0F}" both
  match.

Grid scroll-cancellation: the emoji + custom-image cell wrappers now
use `.contentShape(Rectangle()).onTapGesture` instead of `Button { }
.buttonStyle(.plain)`. `Button` inside a `LazyVGrid` inside a
`ScrollView` only cancels its tap on substantial finger movement, so a
quick flick to scroll the long grid was registering as a pick on
release. `.onTapGesture` cancels on any drag past the hit-test
threshold, which is what the user expects for a scroll.

Closes #304.
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.

feat(emoji): skin tone and hair variants via long-press on emoji cells

1 participant