# 🤖 Reproducing the iOS threading issues (#580)

Working notes for validating the fixes on this branch. The user-visible hang is
intermittent because it needs a main-thread stall to coincide with a tap — so
instead of chasing the rare hang, reproduce the **mechanism signals** (console
warnings), which fire far more reliably, and treat "signal gone" as the pass
condition.

## The signals to watch (device console — Xcode debug console or Console.app)

| Signal | What it means |
|---|---|
| `onChange(of: CGRect) action tried to update multiple times per frame` | The `globalFrame` layout feedback loop in image/link/YouTube bubbles (`MessageBubble.swift`, `YouTubeCard.swift`, `ChatView.swift`). Main-thread layout churn. |
| `System gesture gate timed out` | The system back/edge gesture waited on the feed's gesture stack (per-row swipe-to-reply + long-press + iOS-26 content-pop) and gave up. |
| `Got a keyboard will change frame / will hide notification, but keyboard was not even present` | Spurious keyboard notifications — tied to the slow-to-focus symptom. |

Filter Console.app on the app process with text `multiple times per frame` or
`gesture gate`.

## Setup that maximizes the odds

- A space with **many chats** (warming starts 5 live feed-model sockets).
- Open a chat that has **images and/or a link-preview or YouTube card** on
  screen (those bubbles carry the `globalFrame` tracker that fuels the loop).
- A **second device / the web client** generating churn into the space: flap
  presence (foreground/background), type, send messages.
- Use the **slowest device** available — main-thread stalls are amplified there.

## Per-symptom recipe

1. **Layout loop** — open the media chat; raise/dismiss the keyboard and scroll,
   repeatedly. Expect the `onChange(of: CGRect)` warning to spam (near
   deterministic).
2. **Back delay** — with churn active, open the media chat and immediately tap
   back; repeat ~20×. Watch for the visible delay + `System gesture gate`.
3. **Phantom second copy** (the clearest one) — tap a chat row; if nothing
   commits, tap it again a few times; when it finally opens, hit back. On the
   buggy build "back" lands on a duplicate copy of the same chat.
4. **Blank feed** — from the list, tap the media chat → immediately back →
   immediately tap it again, ~10× fast. Feed opens empty; a tiny scroll redraws.
5. **Slow focus** — open the media chat, immediately tap the composer. Delay
   before the caret/keyboard; correlate with the CGRect warning.

## Fixes on this branch

- **Phantom second copy (#580, symptom 6):** navigation push is now idempotent —
  `ChatNavGuard.shouldPush` skips a push when that chat is already on top, so
  queued taps can't stack duplicate `.thread(id)` entries
  (`ThreadListView.onSelect`). Deterministic regression test:
  `ChatNavGuardTests`. This makes navigation robust *to* a stall; it does not
  remove the stall.

- **`onChange(of: CGRect)` layout churn (likely a root-cause contributor):** the
  image / video / YouTube cards tracked their `.global` frame with a
  `GeometryReader` + `onChange`. That's a *continuous observation of a value
  that changes on every layout pass* — and a chat open runs several layout
  passes per displayed frame (scroll-settle pin, avatar spring, composer inset,
  keyboard), so the frame changes several times per frame and SwiftUI flags
  `onChange(of: CGRect) action tried to update multiple times per frame`.
  (Not a self-invalidation loop — moving the write off `@State` did NOT silence
  it, confirmed by breakpoint at `MessageBubble` image bubble: the trigger is
  observing a high-frequency value at all.) The frame is read **only at tap
  time** and **only on macOS** (the lightbox zooms out from it; iOS ignores it —
  every `onImageTap` consumer takes `_`), so the tracker is compiled out on iOS
  via `trackingFrame(into:)` and kept on macOS. **Validate via the console
  signal** — the warning should no longer appear while scrolling / animating the
  keyboard in a media chat on iOS (runtime behaviour the test suite can't cover).

_Still to land (tracked, not yet on this branch): apply `EdgeOnlyBackGesture`
to `NativeChatScreen` on iOS 26; coalesce presence frames / skip no-op row
reconfigures; make the feed reveal robust to a missed opening pin (blank
feed)._
