eleven / messenger Public

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>
Paul Campbell 25d02ee799f5a8b6dc64753d0e09bc5e1609cfcd parent b7892eb
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 {