mac,ios: reaction chips — flat grey, borderless; your own gets a flush accent ring (#336)
Tuning the perched Tapback pills per feedback: - Everyone else's reaction is now a plain grey pill (a shade darker than the them-bubble), no border — it was carrying a faint hairline that read as fussy. - Your OWN reaction keeps the accent ring, now flush at the pill edge: the backdrop-halo cutout is gone, so the ring is the outer edge instead of sitting inside a light band. - Smaller emoji (14→13), tighter count. Dropping the halo also removes the window/system-background dependency; the grey pill's own contrast seats it over the bubble corner. Shared MessageBubble, so Mac and iOS both get it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+19
−44
ios/LChatCore/Sources/LChatCore/MessageBubble.swift+19 −44
diff --git a/ios/LChatCore/Sources/LChatCore/MessageBubble.swift b/ios/LChatCore/Sources/LChatCore/MessageBubble.swift| index 17a9ad1..9430128 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/MessageBubble.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/MessageBubble.swift |
| @@ -304,68 +304,43 @@ public struct MessageBubble: View { |
| // top padding lifts the row into the bubble (~8pt, landing inside its |
| // 9pt bottom padding so it kisses the corner without covering text) and |
| // reclaims the space so following tags tuck up under it; the VStack's |
| - // trailing/leading alignment already hugs the tail. Each chip's backdrop |
| - // halo (below) is what keeps the overlap from reading as a collision — |
| - // the ingredient the earlier attempt lacked. |
| + // trailing/leading alignment already hugs the tail. The grey pill (a |
| + // shade darker than the bubble) carries its own contrast, so it seats |
| + // cleanly over the corner without a cutout halo. |
| .padding(.top, -11) // -(≈8 overlap + the VStack's 3pt row spacing) |
| .offset(x: message.mine ? 2 : -2) // lean the cluster off the corner |
| .padding(.horizontal, 2) |
| } |
| - // One reaction pill, finished like an iMessage Tapback: an opaque fill |
| - // (style.css .reaction-chip — them-bubble, or mine = accent 16% over the |
| - // panel, matching the web's color-mix), a hairline ring for a crisp edge, |
| - // a whisper of shadow for lift, and — the seating trick — a ring of the |
| - // feed backdrop around it so where it overlaps the bubble a clean gap opens |
| - // instead of two fills butting together. |
| + // One reaction pill: a flat grey Tapback, a shade darker than the them-bubble |
| + // so it holds up against the feed and any bubble it overlaps. Everyone else's |
| + // is just grey and borderless; YOUR own is the only one that gets the accent |
| + // ring — flush to the pill edge, no backdrop halo. The count appears only once |
| + // a second person piles on (a lone "1" is noise — matches the web). |
| @ViewBuilder private func reactionChip(_ r: Reaction) -> some View { |
| - let accent = theme.accent.resolved(scheme) |
| - // Ink for the count — the web colours the whole chip in --ink; mine |
| - // leans on the accent fill+ring for "this one's yours", not the text. |
| - let countInk = theme.bubbleThemInk.resolved(scheme).opacity(0.72) |
| - HStack(spacing: 3.5) { |
| - Text(r.emoji).font(.system(size: 14)) |
| + let ink = theme.bubbleThemInk.resolved(scheme) |
| + HStack(spacing: 3) { |
| + Text(r.emoji).font(.system(size: 13)) |
| if r.count > 1 { |
| Text("\(r.count)") |
| - .font(.system(size: 12, weight: .semibold)) |
| - .foregroundStyle(r.isMine ? accent : countInk) |
| + .font(.system(size: 11.5, weight: .semibold)) |
| + .foregroundStyle(ink.opacity(0.7)) |
| } |
| } |
| .padding(.horizontal, 8) |
| .padding(.vertical, 3) |
| .background { |
| - // Shape-only fill + hairline (kept off the glyphs). |
| + // Shape-only fill/ring/shadow (kept off the glyphs). |
| ZStack { |
| - // mine rides on --panel (web mixes accent INTO panel); theirs on |
| - // the them-bubble. Layering 16% accent over an opaque panel base |
| - // reproduces color-mix(accent 16%, panel) without a blend op. |
| - Capsule().fill(r.isMine ? theme.panel.resolved(scheme) |
| - : theme.bubbleThem.resolved(scheme)) |
| - if r.isMine { Capsule().fill(accent.opacity(0.16)) } |
| + Capsule().fill(theme.bubbleThem.resolved(scheme)) |
| + Capsule().fill(ink.opacity(0.08)) // nudge a shade darker than the bubble |
| } |
| + // Yours only — the accent ring sits flush at the pill edge (no halo). |
| .overlay { |
| - Capsule().strokeBorder( |
| - r.isMine ? accent : theme.bubbleThemInk.resolved(scheme).opacity(0.12), |
| - lineWidth: r.isMine ? 1 : 0.75) |
| + if r.isMine { Capsule().strokeBorder(theme.accent.resolved(scheme), lineWidth: 1.5) } |
| } |
| + .shadow(color: .black.opacity(scheme == .dark ? 0.22 : 0.06), radius: 1, y: 0.5) |
| } |
| - // The cutout halo: a 2pt band of the feed backdrop, so the pill reads as |
| - // floating clear of the bubble it overlaps. Below the bubble (over the |
| - // same backdrop) it's invisible; a whisper of shadow rides it for lift. |
| - .padding(2) |
| - .background(Capsule().fill(feedBackground)) |
| - .shadow(color: .black.opacity(scheme == .dark ? 0.30 : 0.08), radius: 1.5, y: 0.5) |
| - } |
| - |
| - // The feed's backdrop colour — no container sets an explicit one, so the |
| - // effective surface is the window/system background. The reaction halo |
| - // matches it so the cutout disappears where a chip hangs below the bubble. |
| - private var feedBackground: Color { |
| - #if canImport(AppKit) && !targetEnvironment(macCatalyst) |
| - Color(nsColor: .windowBackgroundColor) |
| - #else |
| - Color(uiColor: .systemBackground) |
| - #endif |
| } |
| @ViewBuilder private var bubble: some View { |