← Back to the blog

A week of GChat beta: calls, iCloud, and the build that finally linked

Late April 2026. Calls work, then they crash, then Xcode fails because iCloud touched the Pods folder. Honest notes on fixes, regressions, and what is still open.

  • gchat
  • flutter
  • ios
  • beta

Last week was dense for GChat. Not in the «I shipped a hundred commits overnight» sense. More in the «calls feel great today, tomorrow the app dies when you tap Call, the day after the build breaks because the disk sync layer decided to duplicate folders» sense. That is beta with friends on the server and TestFlight on the phone.

I am writing it down so six months from now I do not have to reconstruct the pain from gut feeling.

Calls and WebRTC: the ghost was in the plugin

We iterated on session state, iOS audio session behavior, incoming call UI. Sometimes everything lined up. Sometimes the app crashed the moment you started a call. Server logs looked fine: call created, push sent. So the bug lived on the client.

The Xcode stack did not land in our Dart or in FastAPI. It landed in flutter_webrtc: postEvent schedules work on the main queue and calls sink(event). If Flutter has already torn down the EventChannel by the time the block runs, the sink is gone and you get a classic EXC_BAD_ACCESS. The _Nonnull annotation in the plugin does not change runtime reality.

The fix upstream is a nil check before and inside the dispatch. I applied the same idea locally and rebuilt. After that, calls stopped feeling like roulette: the server accepting a call should not mean the client dies because WebRTC fired an event after the channel closed.

Lesson filed under «when the crash sits on __postEvent_block_invoke, read the native dependency before you revert your own last two commits».

Chat UI: small things users actually feel

In parallel we shipped UX tweaks that sound like «adjust padding» in a ticket but change how the app feels.

Text bubbles went through several passes: extra padding, width not tracking content, reactions that blew up voice tiles. For voice notes we moved reactions to an overlay so the capsule does not stretch.

Swipe-to-reply briefly only liked your own messages because of gesture direction. We aligned it with Telegram-style behavior: always swipe left, regardless of author.

Call rows in chat history use color: missed red, incoming green, outbound blue. Tiny detail, instant readability.

Emoji panel: dismiss on outside tap, sensible default tab when Recents is empty, smoother transition back to the keyboard. Sounds easy until you reconcile focus, keyboard inset, and animation on iOS.

Voice and video circles got Telegram-style lock-to-record, haptics on start, a dedicated cancel control in locked mode, and permission warm-up at launch so the mic dialog does not leave the record button stuck.

Attachments: PDFs open in the system viewer; MP3 plays inline with the same audio path as voice notes.

Presence and backend zombies

Friends reported: app closed for hours, header still shows online. We tightened the client with a background timer plus lifecycle disconnect logic. On the server we fixed an edge case: if a WebSocket worker dies hard, Redis could keep a ghost connection entry forever. Then even a careful janitor cannot infer that the user is gone.

We added TTL on connection keys with refresh while the session is alive, plus reconciliation between the online set and actual socket counts. After deploy, offline stopped «sticking» for a day without cause.

iCloud and iOS builds: a side quest

Part of the tree lived under iCloud Drive. Fine for Markdown. Awful for Pods, DerivedData, and Flutter binaries: extended attributes, duplicated folders like nanopb 2 from sync conflicts, and suddenly thirty-five duplicate symbols at link time.

We moved the mobile client under ~/Developer/ and kept backend and landing where GitHub workflows feel natural. For signing we had to strip junk from artifacts (a codesign wrapper plus ditto). That is not elegant engineering. That is the tax of syncing your dev folder.

Git once tried to die from duplicate refs like refs/remotes/origin/HEAD 2. Removing the stray copies fixed the repo. If iCloud mirrors your home folder, watch for ghost «copy 2» files inside .git.

What still hurts (said out loud)

Video circles: the server caps a blob around twelve megabytes, while the client lived with a different mental model and no dedicated transcode step for «circle» quality. Net effect: a long clip at a high bitrate hits the ceiling and fails to send even though pieces of the pipeline look almost green. Camera flip for circles is wired at the recording service layer, but a visible control still needs gesture work (you cannot honestly flip mid-recording with the camera API path we use today). That is the next pass, not something to pretend is already polished.


The week was not about «everything perfect». It was about real people running real builds, filing real bugs, and me being able to say: here is what we fixed, here is what we parked on purpose. If you read this from a future where GChat is in the store, weeks like this are why.