| index 37d8b8c..8919142 100644 |
| --- a/ios/LChat/Views/NativeChatScreen.swift |
| +++ b/ios/LChat/Views/NativeChatScreen.swift |
| @@ -237,36 +237,71 @@ struct NativeChatScreen: View { |
| // wears a green halo; away/offline/unknown look like any other |
| // header. (Web + Mac keep the dot+word line — they have the room.) |
| // zIndex(1) keeps the ring above the glass capsule below. |
| + // Decorative here — the title/pill below carries the name to |
| + // VoiceOver, so the avatar needn't be its own element. |
| Avatar(name: thread.title, size: 54, source: thread.avatar, |
| onlineRing: thread.kind == "dm" && dmPresence == "online") |
| .scaleEffect(avatarPop ? 1 : 0.7) |
| .zIndex(1) |
| - VStack(spacing: 0) { |
| - Text(thread.title) |
| - .font(.headline) |
| - .lineLimit(1) |
| - .foregroundStyle(.primary) |
| - if let stats = memberStats { |
| - Text(stats) |
| - .font(.system(size: 11)) |
| - .foregroundStyle(.secondary) |
| - .lineLimit(1) |
| - } |
| + .accessibilityHidden(true) |
| + // In a group the pill is a fast path to the members list — the |
| + // counts *read* as tappable, so make them so (View Members also |
| + // stays in the ⋯ menu as the labelled/fallback path). The pill is |
| + // already ~44pt tall, so no hit-area hacks, and it becomes its own |
| + // actionable a11y element instead of a flattened, action-less one. |
| + // (Groups own their tap here; the DM whole-header tap below never |
| + // fires for them, since memberStats and dmPartner are exclusive.) |
| + if memberStats != nil { |
| + Button { showMembers = true } label: { headerPill } |
| + .buttonStyle(.plain) |
| + .accessibilityElement() |
| + .accessibilityLabel(headerAccessibilityLabel) |
| + .accessibilityHint("View members") |
| + .accessibilityAddTraits(.isButton) |
| + } else { |
| + headerPill |
| } |
| - .padding(.horizontal, 12) |
| - .padding(.vertical, 4) |
| - .floatingGlass(in: Capsule()) |
| } |
| .frame(maxWidth: .infinity) |
| .padding(.bottom, 6) |
| .offset(y: -52) // rise level with the back button / ⋯ menu |
| - .accessibilityElement(children: .combine) |
| + // DMs: the whole header taps through to the partner's profile (#577). |
| + // Groups handle their own tap on the pill above, so keep them separate |
| + // a11y elements (.contain) rather than flattening the pill button away. |
| + .accessibilityElement(children: dmPartner != nil ? .combine : .contain) |
| .contentShape(Rectangle()) |
| .onTapGesture { if let uid = dmPartner { headerProfile = HeaderProfileTarget(id: uid) } } |
| .accessibilityAddTraits(dmPartner != nil ? .isButton : []) |
| .accessibilityHint(dmPartner != nil ? "Shows their profile" : "") |
| } |
| |
| + // The name + optional member-stats capsule; extracted so the group case can |
| + // wrap it in a Button without duplicating the layout. |
| + private var headerPill: some View { |
| + VStack(spacing: 0) { |
| + Text(thread.title) |
| + .font(.headline) |
| + .lineLimit(1) |
| + .foregroundStyle(.primary) |
| + if let stats = memberStats { |
| + Text(stats) |
| + .font(.system(size: 11)) |
| + .foregroundStyle(.secondary) |
| + .lineLimit(1) |
| + } |
| + } |
| + .padding(.horizontal, 12) |
| + .padding(.vertical, 4) |
| + .floatingGlass(in: Capsule()) |
| + } |
| + |
| + // "Standup, 5 members, 2 agents, 3 online" — the pill's counts spoken |
| + // cleanly (" · " reads awkwardly); the "View members" action rides the hint. |
| + private var headerAccessibilityLabel: String { |
| + guard let stats = memberStats else { return thread.title } |
| + return thread.title + ", " + stats.replacingOccurrences(of: " · ", with: ", ") |
| + } |
| + |
| // A group's shared-app surface: schema-described apps (To-do, Reminders) |
| // render the shared instance NATIVE, folded from the group's log; the |
| // rest keep the webview embed. |