eleven / ocho Public
π€ 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
globalFrametracker 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
- Layout loop β open the media chat; raise/dismiss the keyboard and scroll, repeatedly. Expect the
onChange(of: CGRect)warning to spam (near deterministic). - Back delay β with churn active, open the media chat and immediately tap back; repeat ~20Γ. Watch for the visible delay +
System gesture gate. - 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.
- Blank feed β from the list, tap the media chat β immediately back β immediately tap it again, ~10Γ fast. Feed opens empty; a tiny scroll redraws.
- 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.shouldPushskips 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.globalframe with aGeometryReader+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 flagsonChange(of: CGRect) action tried to update multiple times per frame. (Not a self-invalidation loop β moving the write off@Statedid NOT silence it, confirmed by breakpoint atMessageBubbleimage 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 β everyonImageTapconsumer takes_), so the tracker is compiled out on iOS viatrackingFrame(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).