Skip to content

feat: add real time updates - #13833

Merged
MounirDhahri merged 14 commits into
mainfrom
claude/eigen-realtime-messages-gfj5hd
Aug 10, 2026
Merged

MounirDhahri merged 14 commits into
mainfrom
claude/eigen-realtime-messages-gfj5hd

Conversation

@MounirDhahri

@MounirDhahri MounirDhahri commented Jul 27, 2026 •

Copy link
Copy Markdown
Member

Description

This PR adds real-time conversation updates using websockets by introducing a new useConversationsWebsocket hook that subscribes to Gravity's ConversationsChannel. When messages are delivered to the user's conversations, the hook triggers a refetch of conversation data and updates the unread count.

Key changes:

  • New useConversationsWebsocket hook that manages websocket subscriptions to conversation events
  • Integrates with the existing GravityWebsocketContext for cable management
  • Automatically refetches conversation data when new messages arrive
  • Updates unread conversation count in real-time
  • Feature-flagged behind AREnableConversationsRealtime to allow safe rollout
  • Integrated into both the Conversations list view and individual Conversation detail view

Implementation details:

  • The hook accepts a subscriptionKey to allow multiple simultaneous subscriptions (e.g., inbox list + open conversation)
  • Uses a ref to keep the latest callback without resubscribing on every render
  • Properly cleans up listeners and unsubscribes on unmount
  • Respects the feature flag, enabled state, and user authentication status

Screen.Recording.2026-07-15.at.16.19.03.mov

PR Checklist

  • I have tested my changes on the following platforms:
    • Android.
    • iOS.
  • I hid my changes behind a [feature flag], or they don't need one.
    • Feature flag: AREnableConversationsRealtime
  • I have included screenshots or videos at least on Android, or I have not changed the UI.
    • No UI changes; this is a backend integration feature
  • I have added tests, or my changes don't require any.
    • Added comprehensive unit tests for useConversationsWebsocket
  • I added an [app state migration], or my changes do not require one.
    • Not needed
  • I have documented any follow-up work that this PR will require, or it does not require any.
    • No follow-up work required
Changelog updates

Changelog updates

Dev changes

  • Add real-time conversation updates via websockets with useConversationsWebsocket hook

https://claude.ai/code/session_01YQB2YVTL96ynGhYNTgbz2d

@MounirDhahri MounirDhahri changed the title Claude/eigen realtime messages gfj5hd feat: add real time updates Jul 27, 2026
@MounirDhahri MounirDhahri self-assigned this Jul 27, 2026
Comment thread src/app/Scenes/Inbox/Screens/Conversation.tsx
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
Comment thread src/app/Scenes/Inbox/Components/Conversations/Messages.tsx Outdated
Comment thread src/app/utils/Websockets/conversations/useConversationsWebsocket.tsx Outdated
@github-actions github-actions Bot deleted a comment from claude Bot Jul 27, 2026
Comment thread src/app/utils/Websockets/conversations/useConversationsWebsocket.tsx Outdated
Comment thread src/app/Scenes/Inbox/Screens/Conversation.tsx
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
@github-actions github-actions Bot deleted a comment from claude Bot Jul 27, 2026
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
Comment thread src/app/Scenes/Inbox/Screens/Conversation.tsx
@MounirDhahri
MounirDhahri force-pushed the claude/eigen-realtime-messages-gfj5hd branch from cf537a8 to 14d7539 Compare July 29, 2026 12:12
@github-actions github-actions Bot deleted a comment from claude Bot Jul 29, 2026
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
@github-actions

github-actions Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

🎉 Beta Versions Generated (commit: 14d7539)

Android 🤖

  • 9.14.0 (2026072912) - Available on Firebase
  • 9.14.0 (2026072912) - Available on Play Store

iOS 🍏

  • 9.14.0 (2026.07.29.12) - Available on Firebase
  • 9.14.0 (2026.07.29.12) - Available on TestFlight

@github-actions github-actions Bot deleted a comment from claude Bot Jul 31, 2026
Comment thread src/app/Scenes/Inbox/Screens/Conversation.tsx
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
Comment thread src/app/Scenes/Inbox/Screens/Conversation.tsx
Comment thread src/app/Scenes/Inbox/Components/Conversations/Conversations.tsx
@MounirDhahri MounirDhahri modified the milestone: 7m2843ju Aug 4, 2026
MounirDhahri added a commit that referenced this pull request Aug 4, 2026
…reshes

- Gate the subscription on useIsFocusedInTab("inbox") so it doesn't keep
  refetching the conversations connection while the user is on another
  bottom tab (isActiveTab alone doesn't reflect tab focus).
- Track a pendingRefresh flag so an event landing while refetchConnection
  is already in flight triggers a follow-up refresh instead of being
  silently dropped.
- Hoist the duplicated onEvent/onConnected body into one refreshInbox fn.

Addresses PR review feedback on #13833.
@github-actions github-actions Bot deleted a comment from claude Bot Aug 7, 2026
Comment thread src/app/system/notifications/visibleConversation.ts
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 Beta Versions Generated (commit: e642b32)

iOS 🍏

  • 9.15.0 (2026.08.07.10) - Available on Firebase

@github-actions github-actions Bot deleted a comment from claude Bot Aug 7, 2026
Comment thread src/app/system/notifications/visibleConversation.ts
claude and others added 14 commits August 7, 2026 16:16
Subscribe to Gravity's ConversationsChannel (over the existing app-wide
ActionCable connection) while the inbox tab or a conversation is open.
The channel broadcasts a minimal signal when Impulse delivers a message;
in response we refetch the affected Relay connections and refresh the
unread badge, so new messages appear without pull-to-refresh.

- useConversationsWebsocket: screen-scoped channel subscription hook,
  gated by the AREnableConversationsRealtime feature flag (off by
  default) and the user's session token
- Conversation screen refetches when an event matches the open thread
- Inbox list refetches on any event while the tab is active

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQB2YVTL96ynGhYNTgbz2d
…annels fix

The mock had `channels: {}` on the cable mock instead of the
channelsHolder mock, matching the pre-fix code that incorrectly
referenced `cable.channels`. CI was failing because the real fix
(delete channelsHolder.channels[channelKey]) now throws against this
stale mock shape.
- track last marked message id so messages arriving on an open thread
  still get marked as read
- debounce received websocket events (leading + trailing) to collapse
  bursts into at most two refetches
- refetch on socket reconnect via a new onConnected callback so nothing
  broadcast during a drop is lost
- unsubscribe the raw subscription if setChannel returns nothing
- memoize message grouping in Messages and the partner offer event so
  the FlatList data prop keeps a stable identity
- document why from_principal events are not filtered out

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deliver received events directly; keeping the behavior simple is
preferred over collapsing bursts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Assisted-by: Claude:Fable-5 [claude-code]
…reshes

- Gate the subscription on useIsFocusedInTab("inbox") so it doesn't keep
  refetching the conversations connection while the user is on another
  bottom tab (isActiveTab alone doesn't reflect tab focus).
- Track a pendingRefresh flag so an event landing while refetchConnection
  is already in flight triggers a follow-up refresh instead of being
  silently dropped.
- Hoist the duplicated onEvent/onConnected body into one refreshInbox fn.

Addresses PR review feedback on #13833.

Assisted-by: Claude:Sonnet-5 [claude-code]
- Hoist hasConnectedOnce to a ref so a resubscribe (enabled toggled with
  screen focus) fires onConnected as a catch-up refetch instead of
  swallowing it as the initial connection. This also covers refreshing
  the inbox when the user returns to the tab.
- Log rejected subscriptions so a broken rollout is visible.
- Gate the conversation subscription on useIsFocused so a hidden screen
  doesn't refetch and silently mark unseen messages as read; refresh the
  unread badge for events from other conversations too.
- Refetch the inbox connection at its loaded count so a paginated list
  isn't truncated back to the first page on every event.
- Drain pendingRefresh after loadMore as well, not just refetchConnection.

Addresses PR review feedback on #13833.
…read

The oldest loaded message group always rendered the artwork/show preview
above it, so the preview appeared mid-thread while older pages were still
loading. Gate it on relay.hasMore() so it only shows once there is nothing
older left to fetch; until then the group renders like any other (with the
loading indicator above it while paginating).

Also bump the messages page size from 10 to 20.
Switching between staging and production left realtime dead until the app
was restarted. The effect that creates the ActionCable consumer ran on
`isStaging` but guarded its body with `if (!actionCable)`, so on an env
switch it saw the existing consumer and never created one for the new host.
Its cleanup read `actionCable` from state, which the creating run captured
as null, so the old consumer was never disconnected either — the socket
leaked on plain unmount too.

Key the effect on `wssUrl` and disconnect the cable created in that same
run. Consumers already have `cable` in their dep arrays, so they resubscribe
against the new host on their own.
…iOS)

A message arriving while the user is reading that same conversation showed a
banner over the thread they were already looking at. The websocket already
adds the message inline, so the banner is noise.

Native has to decide this synchronously inside willPresentNotification, and it
can't ask JS anything at that point, so React publishes the visible
conversation instead: onStateChange writes `visibleConversationID` into the
existing reactState channel on every navigation, and empties it whenever no
conversation is on screen. The delegate reads it back, and when the push's deep
link points at the same conversation it presents nothing instead of an alert.

The payload is still forwarded to React, so the unread badge keeps refreshing.
Native state is only consulted for conversation links, which also keeps the
existing strict mock in AppDelegate+NotificationsTests happy.

`reactStateUpdated` only accepted the full environment object; its keys are now
optional, which is what native already assumed since it merges the dictionary.
Android no-ops that method, so the shared call is safe there. Suppressing the
Android banner is a separate change.

Assisted-by: Claude:Opus-5 [claude-code]
…Android)

The Android counterpart to the iOS change. `messaging().onMessage` only fires
in the foreground and is what calls `notifee.displayNotification`, so skipping
that call is all it takes — no native code and no state pushed down, because JS
can read the focused route directly when the message arrives.

Uses the same two rules as iOS: the push has to point at a conversation, and
that conversation has to be the focused route.

Only reaches pushes that fall through to React Native Firebase. Braze displays
its own notifications natively in BrazePushListenerService, which never hits
this handler.
@MounirDhahri
MounirDhahri force-pushed the claude/eigen-realtime-messages-gfj5hd branch from 2b8d56a to 714cc90 Compare August 7, 2026 14:17
@artsyit

artsyit commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

This PR contains the following changes:

  • Dev changes (Add real-time conversation updates via websockets with useConversationsWebsocket hook - MounirDhahri)

Generated by 🚫 dangerJS against 714cc90

@github-actions github-actions Bot deleted a comment from claude Bot Aug 7, 2026
@claude

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review

Summary

Adds real-time conversation updates over Gravity's ActionCable websocket behind the AREnableConversationsRealtime flag. A new useConversationsWebsocket hook subscribes per-screen (inbox list + open thread), refetches the affected Relay connections on message events and on reconnect, and refreshes the unread badge. Also suppresses the foreground push banner (iOS + Android) when the message is for the conversation already on screen, plus some memoization cleanup in Messages/usePartnerOfferEvent.

The hook lifecycle (feature flag + auth + focus gating, ref-held callbacks, hasConnectedOnce catch-up, pendingRefresh drain, paginated refetch count) is carefully thought through, and test coverage for the hook is good. Nice work.

Issues Found

🟡 Important — env switch can leave live-auction sockets dead (src/app/utils/Websockets/GravityWebsocketContext.tsx:30-47)
The effect now correctly tears down and recreates the cable on wssUrl change. But AuctionSocketContext's subscribe effect is keyed only on [enabled] (src/app/utils/Websockets/auctions/AuctionSocketContext.tsx:47-57), not on cable/channelsHolder. Previously the cable was never recreated (the old if (!actionCable) guard), so this was moot. Now, on an environment change the old cable is disconnected but the auction context keeps its channel on the dead cable and never resubscribes until it remounts. useConversationsWebsocket is safe here because it lists cable/channelsHolder in its deps. Env switching is a dev-menu action, so real-user impact is low — flagging so it's a conscious trade-off rather than a surprise.

🟢 Suggestion — PR checklist says "No UI changes", but there are a few

  • Messages.tsx:164 gates the thread-subject preview on !relay.hasMore(), changing where it renders mid-pagination.
  • Inbox.tsx:40-43 changes the default selected tab from always bids to bids/inquiries based on active bids (this aligns activeTab with the already-inquiries initialTabName — a real fix, good).
    Worth confirming the screenshots/recording cover these on both iOS and Android, since the checklist currently claims none.

Areas Reviewed

  • Bugs & edge cases: conversationIDFromURL (JS regex) and conversationIDFromURLString (iOS path-component walk) agree on /conversation/:id, /conversation/:id/details, and /user/conversations/:id; both routes are named Conversation so conversationIDFromRoute matches. reactStateStringForKey returns nil when unset and isEqualToString:nil is NO, so an empty visible-conversation state correctly presents the banner. Marking-as-read now tracks lastMarkedMessageID instead of a boolean, so a message arriving on an open thread still gets marked read — verified the memo dep chain (me.conversation → maybeMarkLastMessageAsRead → effect) drives it.
  • Performance: Messages grouping moved from useEffect+setState to useMemo with stable deps; usePartnerOfferEvent memoized. Both keep the FlatList data identity stable.
  • Focus gating: inbox subscription is gated on isActiveTab && isFocusedInInboxTab, so it's torn down while a thread is on top; the conversation subscription is gated on isFocused so a hidden screen can't silently mark messages read. Consistent with the comments.
  • Testing: useConversationsWebsocket has solid unit coverage. No new tests for the Conversations/Conversation wiring or the notification-suppression helpers — reasonable to add coverage for the pure conversationIDFromURL/conversationIDFromRoute helpers since they encode the deep-link matching contract shared with native.

Questions for Author

  1. On environment switch (see the auction note above), is losing the live-auction socket until remount acceptable, or should AuctionSocketContext also depend on cable?
  2. The commit trailers disclose LLM assistance (Assisted-by: Claude ...) and the description links a Claude session, which is great. Per the LLM-disclosure RFC, consider adding an explicit "Assisted by" line in the PR description body too.

@olerichter00 olerichter00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👌

params?: object | undefined
}): string => {
if (route?.name !== "Conversation") {
return ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Shouldn't it, in this case, return a null instead to clearly indicate there's no ID?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question, the reason behind this is to align the default value inside ios native modules with the one in react-native. happy to change this if you feel strong about it.

@MounirDhahri

Copy link
Copy Markdown
Member Author

Merging this now right after code freeze to get 2 weeks of testing this

@MounirDhahri

Copy link
Copy Markdown
Member Author

Fwiw, it already passed QA 🎉

@MounirDhahri
MounirDhahri merged commit f3c0aaa into main Aug 10, 2026
11 checks passed
@MounirDhahri
MounirDhahri deleted the claude/eigen-realtime-messages-gfj5hd branch August 10, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants