refactor: migrate RoomView to a function component#7482
Conversation
…state vocabulary Rename UBIQUITOUS_LANGUAGE.md to CONTEXT.md (history preserved) and update its three references in CLAUDE.md, the VoIP docs README, and the RoomView architecture doc. Add the "Message Interaction & Position State" section distinguishing Interaction state (selection plus the reply/quote/edit/react action, owned by the per-Room interaction store) from Positional state (highlight plus jump/scroll position, owned by the Room view scroll machinery), since the two are migrated independently. Ref: NATIVE-1345 Claude-Session: https://claude.ai/code/session_01Cj1jHhEbiC9mWGe8dpqQeS
Switch every @json-decorated WatermelonDB model field to the memoized form
({ memo: true }) so an unchanged nested JSON value keeps its reference identity
across reads, enabling per-field re-render bailout once consumers adopt it.
The memo cache is invalidated only when the raw column string changes, so
in-place mutation of a memoized value would silently corrupt it. Convert the
two remaining in-place mutations to replacement:
- ReactionsList sorts a copy ([...reactions].sort) instead of the field.
- encryption quote-attachment decryption reassigns message.attachments instead
of pushing into it.
Ref: NATIVE-1346
Claude-Session: https://claude.ai/code/session_01Cj1jHhEbiC9mWGe8dpqQeS
Introduce useMessage, a reactive hook that takes a live WatermelonDB Message model and returns a plain Message Snapshot via useSyncExternalStore. It subscribes to model mutations, paints synchronously on first render with no flash, and reads the memoized @JSON getters directly so unchanged fields keep their reference for per-field re-render bailout. All WatermelonDB specifics are confined to the hook; it is not yet wired into the row container.
Convert MessageContainer from a class to a function component. - Replace manual experimentalSubscribe + forceUpdate with the reactive useMessage snapshot hook; model mutations now re-render through useSyncExternalStore. - Replace shouldComponentUpdate with React.memo and an equivalent custom comparator over the same nine props. - Move isManualUnignored to useState, theming to useTheme, and defaultProps to destructuring parameter defaults. - Keep the debounced onPress as a single lifetime instance via a ref, always invoking the latest closure. Render output is unchanged: the existing Message snapshot suite passes without updates. Adds a behavior test for the row container.
Move `action` and `selectedMessages` out of the RoomView class state into a per-Room Zustand store (InteractionStore), mirroring the MessageComposer store topology. Message rows now read their edit state via a `useIsBeingEdited` selector instead of a prop, so starting or cancelling an edit re-renders only the affected row instead of triggering a RoomView render and a FlatList walk. Composer consumers read the interaction values from the store while keeping their handlers on RoomContext, so the RoomContext value no longer changes when interaction state changes. ShareView provides its own store for its composer subtree. Behavior is unchanged.
Remove the umbrella IMessage/IMessageInner interfaces and the {...props}
bag spread from the message component tree. Each presentational component
now declares the narrow interface it consumes and receives explicit props.
Shared handlers (getCustomEmoji, navToRoomInfo, showAttachment, blockAction,
handleEnterCall, fetchThreadName) are read from MessageContext rather than
threaded as props, and MessageContext is now typed (IMessageContext) in
place of `any`. Behavior is unchanged.
…NATIVE-1351) Add the `'use memo'` opt-in directive to the three presentational memo components in Message.tsx (MessageInner, Message, MessageTouchable), keeping their explicit React.memo gate. This mirrors the existing precedent in message/index.tsx (MessageContainer) under the project's React Compiler annotation mode. Clean Message.stories.tsx to pass only the explicit data props each story exercises: drop the per-story handler props (onReactionPress, onErrorPress, replyBroadcast) and the dead `theme` prop, all of which are now supplied through the MessageContext provider value. Behavior is unchanged — the snapshot suite passes without updates.
Drop three confirmed-dead members from the RoomView container state: the unused reaction-modal visibility flag (reactionsModalVisible), the unused reply-with-mention flag (replyWithMention), and the redundant on-hold flag (isOnHold). reactionsModalVisible and replyWithMention were only ever initialized and type-declared, never read or re-set. isOnHold was set alongside roomUpdate and only compared in shouldComponentUpdate; that comparison is redundant because roomUpdate already carries onHold (it is part of roomAttrsUpdate), so on-hold transitions still trigger a re-render through the existing roomUpdate diff. No behavior change — the full test suite passes without snapshot updates.
Collect quote attachments from Promise.all and assign message.attachments once after it resolves, instead of racing read-modify-write across the concurrent decrypt branches (which dropped quotes on multi-quote messages).
Dead export with zero callers; consumers use the narrower hooks or read getState().actions directly.
…reads - Guard the model subscription so plain REST objects (search, starred, pinned, mentions, files) no longer throw 'experimentalSubscribe is not a function' on mount; serve a static snapshot when the model is not observable - Remove render-body ref writes from MessageContainer and useMessage so the React Compiler memoizes both (stable subscribe/getSnapshot, snapshot cached by item identity; debounced press handler created once, latest closure via effect) - areEqual compares previousItem.id (was ._id, always undefined on WatermelonDB models) and documents why item is intentionally omitted - isMessageLink no longer treats plain links as message links when attachments is undefined - read the translation from the useMessage snapshot instead of the live model - drop redundant/historical comments Adds a useMessage regression test for the non-observable plain-object path.
The message render tree is annotated for the React Compiler ('use memo').
A compiled parent emits referentially-stable child elements, so React.memo
on a child whose parent is also compiled is redundant. Remove the memo
wrappers and their hand-written comparators (both dequal deep-compares and
scalar shouldComponentUpdate-era allowlists) across the presentational
message components, leaving compilation as the single change-detection
mechanism.
The deep-compare removals are safe because the @JSON model fields
(attachments, urls, md, mentions, channels, reactions, replies) resolve to
stable references, making reference comparison equivalent to dequal.
MessageContainer (index.tsx) keeps its memo: its parent is RoomView's
renderItem class method, which the compiler never compiles, so that wrapper
is load-bearing.
Removing Thread's tcount-only comparator also fixes a latent bug: a tlm
change with unchanged tcount no longer suppresses the re-render that toggles
the thread button's visibility. Add Thread.test.tsx as a regression guard.
Also delete dead StyleSheet keys the memo wrappers had hidden from
react-native/no-unused-styles.
Message rendering funneled every field of a message through a single useMessage hook that snapshotted the whole record and prop-drilled it down the subtree, so a change to any one field re-rendered the entire message. Introduce a per-message store (MessageStore) created once per MessageProvider and ticked by the WatermelonDB record's experimentalSubscribe. Leaves read only the fields they need through granular selector hooks that bail via Object.is for a single field or useShallow for a domain group, so a field change re-renders just its consumers. Delete useMessage and drop the props that existed only to carry its snapshot. Attachments and Quote keep attachments/author as props: Reply renders them recursively with the nested attachment array, so a message-level hook there would recurse infinitely. Claude-Session: https://claude.ai/code/session_01Q27JMv1gF6V9urfyvGpWqn
…ubscription Row grouping depended on the previous row's mutable scalars, propagated only by the areEqual memo comparator — which cannot observe an in-place mutation of a stable model instance, so a previous message going Sent to Error never re-derived the next message's header until an unrelated re-render. Subscribe each per-message store to both its own record and the previous record; grouping and thread-position derivations read the previous model live. Delete areEqual and the MessagePrev scalar snapshot; the row keeps memo with the default comparator as a perf-only guard. Claude-Session: https://claude.ai/code/session_01PNjqp2s36bgagFz7Lo6xsV
The Timestamp story renders a dayjs fromNow relative to the real current
date; the stored snapshot ('a year ago') drifted to '2 years ago',
failing the ESLint and Test job on every run after the boundary.
Claude-Session: https://claude.ai/code/session_01PNjqp2s36bgagFz7Lo6xsV
…-1376) Adds pickMessageRoomState + a MessageProviders/renderWithMessageProviders helper and adopts it in the container test and stories so the room store is mounted alongside MessageContext during the dual-run. No production behavior change; 328 tests / 128 snapshots unchanged.
…ad (NATIVE-1376) First stable-field consumer off MessageContext. RepliedThread now subscribes only to fetchThreadName (a stable ref) via useFetchThreadName(), so it no longer re-renders on this row's send/decrypt/status transitions.
…sageStore hooks (NATIVE-1376) useMessageGrouping, useThreadPosition and useMessageText now read broadcast, Message_GroupingPeriod, isThreadRoom, user and auto-translate settings from the per-instance MessageRoomStore selectors instead of useContext(MessageContext). Consumer tests (Content, Thread, useMessageAccessibilityLabel, MessageStore) mount MessageRoomProvider alongside the legacy MessageContext.Provider, mirroring values via pickMessageRoomState during the dual-run migration.
… components (NATIVE-1376) MessageAvatar, User, Urls, CallButton, Video, Audio and Quote now read user, navToRoomInfo, getCustomEmoji, handleEnterCall, baseUrl, rid (room store) plus id and translateLanguage (per-message store) through selector hooks instead of useContext(MessageContext). No test or story changes needed: these leaves render under Message.stories and the container test, which already mount MessageRoomProvider and MessageProvider.
…s and hooks (NATIVE-1376) Attachments, Image/Container, CollapsibleQuote, useMediaAutoDownload and useMessageAccessibilityHint now read translateLanguage/id (per-message store) and user/baseUrl/showAttachment/getCustomEmoji/isThreadRoom (room store) via selector hooks instead of useContext(MessageContext). Their hand-written tests and stories mount MessageRoomProvider (and MessageProvider where a per-message hook is exercised) alongside the legacy MessageContext.Provider, mirroring values through pickMessageRoomState during the dual-run.
MessagePreview renders the message container outside RoomView/MessagesView/SearchMessagesView, which each already mount a MessageRoomProvider. After the message container's grouping/thread/translate hooks were repointed to the room store, this path had no provider and threw 'Message room hooks must be used within a MessageRoomProvider' on render, crashing ForwardMessageView. Seed a MessageRoomProvider from the props MessagePreview already holds (user, baseUrl, getCustomEmoji, rid); the remaining room fields stay unset to preserve the legacy preview defaults. Adds a regression test rendering MessagePreview under Redux only.
Blocks reads blockAction from the room store (useBlockAction); Reply reads user/baseUrl (room store) and id/isEncrypted (per-message store) via selector hooks instead of useContext(MessageContext). Drops the dead e2e read: the message MessageContext value built in index.tsx never sets e2e, so the guard (isEncrypted && !e2e) was always equivalent to isEncrypted. This retires the last stable/per-message-data consumers; only closure handlers remain on MessageContext.
…VE-1376) Step 4a of ADR 0018. Adds the room-global row handlers (onReactionPress, onReactionLongPress, reactionInit, onDiscussionPress, onThreadPress, replyBroadcast, errorActionsShow, onAnswerButtonPress, onEncryptedPress) plus archived to MessageRoomState, ROOM_STATE_KEYS and matching selector hooks, and seeds them from RoomView's MessageRoomProvider. Purely additive: no consumer reads them yet, MessageContext still carries the closures. MessagesView and SearchMessagesView provide none of these handlers, so they stay unwired.
…IVE-1376) Step 4b of ADR 0018. Extends the per-message zustand store with onPress, onLongPress and threadBadgeColor (mirrored from MessageProvider props every render, same pattern as MessageRoomProvider) and adds the composite hooks useMessageLongPress, useMessagePress and useOnLinkPress as a faithful relocation of the closures still living in MessageContainerInner. Adds closeEmojiAndAction to the room store (it was RoomView-internal, never in MessageContext) so useMessagePress can read it. Purely additive: the new hooks have no consumers yet and MessageContainerInner still builds the legacy Context closures, so behaviour and snapshots are unchanged.
…TIVE-1376) Move the 8 remaining MessageContext consumers (Discussion, Reactions, Broadcast, AttachedActions, MessageError, Encrypted, Thread, Content) onto the room-level and per-message store selector hooks, reconstructing each handler's argument from the per-message context. Seed the per-message store's initial state from the provider's props so the first render sees onPress/onLongPress/threadBadgeColor synchronously, matching MessageRoomStore and avoiding an extra mount render. Rewire Reactions.test onto the shared MessageProviders helper for the dual-run providers.
…o store hooks (NATIVE-1376) MessageContainer now renders the provider stack directly; the deleted MessageContainerInner's press and long-press logic moves into MessageTouchable via the useMessagePress and useMessageLongPress composite hooks instead of MessageContext. Relocate the manual-unignore state and the isBeingEdited lookup into MessageTouchable, the component that consumes them. RCTouchable reads the long-press guard from useMessageLongPress. MessageContext.Provider stays mounted with an empty value that no component reads, leaving step 5 a pure deletion. Wrap two attachment test harnesses in MessageProvider now that their Touchable subtree needs it.
diegolmello
left a comment
There was a problem hiding this comment.
Two-axis review (standards + spec) — findings inline. One non-inline note: the PR body lists useRoomLifecycle among the extracted hooks, but no hook by that name exists — lifecycle work landed as useRoomInit/useRoomAudioLifecycle/useRoomRemoved. Worth updating the body wording.
|
Android Build Available Rocket.Chat 4.75.0.109378 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNRi1rFsiRORIChrwO8nsFzE92vhRmliA2yVLD58dwP8iuV5XCB_eMbd3s2VgVq19KZwPp8Sl2hMAY4CKov- |
|
iOS Build Available Rocket.Chat 4.75.0.109379 |
…attern Extract useRoomWithUpdateFromStore primitive (holds the freshness comment once) and route useRoomWithUpdate, useReadOnly, useE2EEStatus and useComposerRoom through it, dropping the re-pasted room+roomUpdate double-subscribe. Split useReadOnly into a useReadOnlyForStore core + thin context wrapper and give useE2EEStatus the same optional-store override.
…parately The on-hold effect re-fetched routing config (getRoutingConfig REST call) on every on-hold/status transition. Fetch the config once (deps t/rid/joined) into local state, and derive canPlaceLivechatOnHold in a separate deps-only effect that reads the already-fetched config.
…active state - type navToRoomInfo with IRoomInfoParam (drop any) - fold view-level navToRoomInfo/showAttachment overrides into the handlers bag at provider construction so selectors read a single path - split state into FrozenState/ReactiveState; ReactiveSnapshot forces the resync effect payload to cover every reactive key
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…rom JoinCode Read master-detail via useMasterDetail() and take ref as a normal prop (React 19 ref-as-prop). Removes the empty connect() and withMasterDetail HOC.
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Proposed changes
Migrates
app/views/RoomViewfrom a 1726-line class component to a function component. Behavior-preserving — no user-visible change intended.shouldComponentUpdate;connect(mapStateToProps)and all HOCs preserved.RoomStoreregistry (stores/RoomStore.ts): self-hydrates from the DB, shared by a room and its threads, torn down on last release;goRoomwarms it at nav time.RoomContextreplaced by a per-instance composer Zustand store (stores/ComposerStore.tsx).useRoomInit,useRoomAudioLifecycle,useRoomRemoved,useHeader,useJumpToMessage,useMessageActions,useOmnichannelPermissions,useRoomNavigation) and presentational components (MessageRow,RoomFooter,RoomMessageActions, room-state screens).'use memo'throughout so the React Compiler owns memoization.SearchMessagesViewresults are wrapped inA11yGateProviderso long-press message actions work there too — a small a11y addition riding along with the shared message-handler extraction.Issue(s)
https://rocketchat.atlassian.net/browse/NATIVE-34
How to test or reproduce
Use a room end to end (send/edit/quote/react, drafts, autocomplete, threads, jump-to-message, join a not-subscribed room, header actions) on phone and tablet, plus an omnichannel/livechat room. Behavior should match the base branch.
Screenshots
Types of changes
Checklist
Further comments
Stacked on
native-22-message-hooks(PR #7455) and targets it until NATIVE-22 lands ondevelop, after which this will be rebased and retargeted todevelop.Summary by CodeRabbit