Skip to content

feat: add real time updates - #13833

Open
MounirDhahri wants to merge 6 commits into
mainfrom
claude/eigen-realtime-messages-gfj5hd
Open

feat: add real time updates#13833
MounirDhahri wants to merge 6 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

🎉 Beta Versions Generated

iOS 🍏

9.13.0 (2026.07.16.10) - Available on Firebase

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
claude and others added 6 commits July 29, 2026 14:12
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 <[email protected]>
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 <[email protected]>
Deliver received events directly; keeping the behavior simple is
preferred over collapsing bursts.

Co-Authored-By: Claude Fable 5 <[email protected]>

Assisted-by: Claude:Fable-5 [claude-code]
@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
@artsyit

artsyit commented Jul 29, 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 14d7539

Comment on lines +93 to +105
useConversationsWebsocket({
subscriptionKey: "inbox",
enabled: isActiveTab,
onEvent: () => {
refreshConversations()
GlobalStore.actions.bottomTabs.fetchCurrentUnreadConversationCount()
},
// Catch up on anything broadcast while the socket was down.
onConnected: () => {
refreshConversations()
GlobalStore.actions.bottomTabs.fetchCurrentUnreadConversationCount()
},
})

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.

isActiveTab tracks the inner Bids/Inquiries tab, not whether the Inbox screen is on screen. Inbox stays mounted when the user switches bottom tabs (isVisible is never passed from routes.tsx:1004, so Inbox.tsx:173 always gets false), so once someone opens Inquiries the subscription stays open and every incoming message runs refetchConnection(PAGE_SIZE) plus an unread-count fetch while the app is showing Home/Search/etc.

There's an existing helper for this: useIsFocusedInTab("inbox") (src/app/utils/hooks/useIsFocusedInTab.ts), so enabled: isActiveTab && isFocusedInTab.

Also, onEvent and onConnected are the same body — worth hoisting to one const handleUpdate = () => {...}.

Comment on lines +91 to +92
channel.on("received", handleReceived)
channel.on("connected", handleConnected)

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.

No rejected or disconnected listener. If Gravity rejects the subscription (expired/invalid access_token, channel not deployed) the hook stays silent and conversations just never update in real time — indistinguishable from "no messages arrived". AuctionSocketContext wires both (AuctionSocketContext.tsx:70-73); at minimum logging rejected would make rollout failures visible.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review

Summary

Adds a useConversationsWebsocket hook that subscribes to Gravity's ConversationsChannel over the existing ActionCable connection and refetches Relay data + the unread badge when a message is delivered. Wired into the Inbox list and the open conversation, gated by AREnableConversationsRealtime. Also fixes Inbox's initial activeTab and memoizes the message grouping in Messages.

The hook follows the existing AuctionSocketContext pattern (setChannel / removeListener / unsubscribe / delete channelsHolder.channels[key]) and gets the holder-vs-consumer distinction right, which the auction code does not. Cleanup, the callback refs, and the enabled / flag / token guards look correct, and the test file covers subscribe, receive, reconnect, teardown, resubscribe and every skip path.

Two unrelated fixes ride along and both look right: Inbox.tsx:43 now seeds activeTab from hasActiveBids so ConversationsContainer gets isActiveTab={true} when Inquiries is the landing tab (previously MyBids started its polling interval for a tab that was never shown), and dropping name: null from the optimistic response matches Message_message, which only selects from { email }.

Issues Found

🟡 ImportantConversations.tsx:93-105: the inbox subscription stays open while the Inbox screen is off screen. Details in the inline comment; useIsFocusedInTab("inbox") already exists for this.

🟢 SuggestionuseConversationsWebsocket.tsx:91-92: no rejected/disconnected handling, so a rejected subscription fails silently. Inline comment has the detail.

🟢 Suggestionsrc/app/utils/Websockets/conversations/useConversationsWebsocket.tsx contains no JSX, so per docs/best_practices.md ("Files containing a JSX component end in .tsx and files that don't end in .ts") it should be useConversationsWebsocket.ts. The test file keeps .tsx since it renders a wrapper.

Areas Reviewed

Performance — the Messages memoization is an improvement over the old useEffect + useState keyed on allMessages.length / allOrderEvents.length: content changes at a constant length (optimistic message replaced by the real one) now flow through. usePartnerOfferEvent's useMemo deps are complete, and hasActivePartnerOffer is still recomputed per render inside usePartnerOffer (getTimer(offer.endAt)), so an expiring offer still drops out of the memo on the next render.

Security — the user access token travels in the subscription identifier over wss, same shape as the auction channel's xapp_token. Nothing new exposed.

TestinguseConversationsWebsocket is well covered. Nothing covers the two integrations, so the event.conversation_id === conversationID filter in Conversation.tsx:81 and the enabled: isActiveTab gating are untested; Conversations.tests.tsx and Conversation.tests.tsx already exist as homes for that if you want it.

Questions for Author

  1. Sending from this device. from_principal is deliberately not filtered, so your own send triggers a forced refetch while the mutation's optimisticUpdater/updater are inserting the same edge via ConnectionHandler.insertEdgeBefore (the mutation selects no cursor). Did you see any flicker or a duplicated bubble on send during testing? keyExtractor uses group[0].__id, so a duplicate would surface as a React duplicate-key warning.

  2. Double refetch. With the Inbox mounted and a conversation open, both subscriptions are live, so a message in the open thread refetches twice (parent ConversationRefetchQuery + refetchConnection) and hits fetchCurrentUnreadConversationCount twice. Intentional, or should the conversation screen suppress the inbox one?

  3. Dropped events. refreshConversations no-ops when relay.isLoading(), and there is no retry. An event landing during an in-flight refetch is dropped, which is fine if the in-flight response is guaranteed to already contain the message, but that ordering isn't guaranteed. Acceptable for the first rollout?

  4. Android recording. The description has an iOS video only. The behavior is user visible, so a short Android recording would be good to have alongside it.

@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

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.

3 participants