Skip to content

refactor: migrate RoomView to a function component - #7482

Open
diegolmello wants to merge 349 commits into
developfrom
native-34-roomview-hooks
Open

refactor: migrate RoomView to a function component#7482
diegolmello wants to merge 349 commits into
developfrom
native-34-roomview-hooks

Conversation

@diegolmello

@diegolmello diegolmello commented Jul 9, 2026

Copy link
Copy Markdown
Member

Proposed changes

Migrates app/views/RoomView from a 1726-line class component to a function component. Behavior-preserving — no user-visible change intended.

  • Function component, no shouldComponentUpdate; connect(mapStateToProps) and all HOCs preserved.
  • Room/subscription observation moves into a rid-keyed, reference-counted RoomStore registry (stores/RoomStore.ts): self-hydrates from the DB, shared by a room and its threads, torn down on last release; goRoom warms it at nav time.
  • RoomContext replaced by a per-instance composer Zustand store (stores/ComposerStore.tsx).
  • Logic extracted into focused, unit-tested hooks (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.
  • SearchMessagesView results are wrapped in A11yGateProvider so 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

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Stacked on native-22-message-hooks (PR #7455) and targets it until NATIVE-22 lands on develop, after which this will be rebased and retargeted to develop.

Summary by CodeRabbit

  • New Features
    • Jump-to-message now shows loading, supports cancel, and routes correctly between rooms and threads (including reply/thread context).
    • Room UI updates: improved header setup plus a dynamic footer and message actions (read-only, preview/join prompts, on-hold, blocked, federation messaging), with long-press actions and in-app feedback/haptics.
  • Documentation
    • Updated glossary for room/conversation viewing states and clarified message state responsibilities.
  • Tests
    • Added/expanded Jest coverage for jump-to-message, header setup, message actions, room/store behavior, and omnichannel permissions.

diegolmello and others added 30 commits July 7, 2026 14:50
Resolve conflicts in MessagesView/SearchMessagesView: keep MessageRoomProvider
wrapping while adopting develop's insets/paddingBottom on the FlatList.
The Timestamp story renders a fixed unix time with the relative (`R`)
format, so its snapshot drifts with wall-clock time (`2 years ago` -> `a year ago`).
…e (NATIVE-22)

Replace the writable module-level fallbackStore with a frozen inertStore whose
actions throw on call, closing the latent cross-room write vector for
no-provider consumers (search/pinned rows). Document useIsBeingEdited's
graceful-degradation contract via JSDoc, and expand the test suite: no-provider
isolation, useIsBeingEdited across all kinds, addQuote dedup guard, clear() from
each kind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lands the message-action-store map into native-22-message-hooks. Pure
refactor of the interaction store — no intended behavior change.

- Rename InteractionStore -> MessageActionStore, normalize action verbs
- Collapse useMessageAction + useSelectedMessages into one useMessageAction()
- Harden the no-provider fallback with an inert MessageActionStore

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…IVE-22)

Relocates MessageActionStore from views/RoomView into containers/message/stores
so reusable layers no longer import upward from a view, and moves the
TMessageActionState discriminated union into app/definitions (where the
derived TMessageAction already lives), inverting the definitions -> views
arrow. RoomView/ShareView keep creating and providing the store.
…s (NATIVE-22)

JitsiBranch rendered a bare <Content /> after the refactor, and
useIsInfoMessage() explicitly excludes jitsi_call_started (it isn't a
compact/non-touchable info row), so InfoContent never rendered the
localized "started a call" line — only the join button remained.

Give Content a local isInfo override so JitsiBranch alone can force
InfoContent, matching the pre-refactor output (User + Started_call text
+ join button) without touching useIsInfoMessage or any other branch.

Add a jitsi_call_started story to cover it going forward.
Ticket 004: confirmed the stuck-false transition bug at the unit level,
but the only caller (useMediaAutoDownload) keys its message list by the
same id, so a real messageId change always remounts the hook with a
fresh seed. Ruled the behavior fix out of scope; kept the test as a
documented contract.
blocks now types to @rocket.chat/ui-kit's LayoutBlock union (the type
already rendered by UiKitMessage), and e2eMentions matches
getE2EEMentions's real return shape. Closes the any hole at the
useBlocks() hook boundary.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.77.0.109596

* test: lock the send contract in the useRoomMessaging tests

* refactor: inline sendRoomMessage into useRoomMessaging
…7666)

* refactor: pass composer roomTitle and t as scalars instead of room

ComposerStore no longer mirrors room/roomUpdate. RoomScreen and ShareView
compute roomTitle via getRoomTitle and pass it alongside t through
RoomProviders/ComposerProvider. useComposerRoom is removed in favor of
useComposerRoomTitle and useComposerType.

* refactor(room-store): observe the subscription record directly, publish every emission

Find the subscription row once by rid, observe it with room.observe() when present,
or fall back to a query observable until a row appears. Every emission now reaches
setState unconditionally. On record destruction, joined flips to false for non-DM
rooms. subscribed is removed from RoomState.

* refactor(room-view): derive livechat on-hold agent check in selector

Remove lastMessageFromAgent from RoomStore; useCanPlaceLivechatOnHold now computes it from room.t, room.lastMessage and room.onHold.

* refactor(room-view): select scalar fields in header and right buttons instead of the whole room

* refactor(room-footer): select scalar room fields instead of the whole room

TakeOrJoin, useFooterMessage, useRoomFooterState, RoomAnnouncementBanner,
useReadOnly and useE2EEStatus now select the fields they use (t, onHold,
ro, roles, encrypted, E2EKey, announcement, bannerClosed, and helper
outputs like isBlocked/isRoomFederated) instead of subscribing to the
whole room. useCloseBanner takes the RoomStore and reads room
imperatively at call time instead of receiving it as a prop.

* refactor: select room fields instead of whole room in message list/row/actions

RoomMessageList, MessageRow, RoomMessageActions, the message handlers hook
and the thread badge color hook now select only the room fields they need
from RoomStore, since a whole-room selector never re-renders (WatermelonDB
mutates the same model instance in place). callJitsi now reads
getState().room imperatively at call time instead of closing over a
rendered room.

* refactor(room-view): delete whole-room snapshot machinery

RoomStore now publishes the live room on every observe() emission with
no diffing. roomUpdate, roomObservedFields, the observed column map,
useRoomWithUpdateFromStore and useRoomWithUpdate are gone; every
RoomView reader already selects the fields it needs. RoomScreen and
RoomGate no longer hold a whole-room selector.

* fix(room-view): address review findings on scalar-selector migration

Kill the last whole-room render reads in useGoRoomActionsView and
LeftButtons (the latter was genuinely stale on rename), narrow
RoomGate's invite/encrypted-room props, drop the unused subscription
field from useSubscriptionUnreads, guard RoomStore's synchronous-emission
race in observeQueryUntilPresent, and make ComposerState.roomTitle
required.

* fix(room-view): drop the comment and var trick from the RoomStore race fix

Use a subscribed flag set after subscribe() returns instead of var
hoisting, so the callback never touches the subscription binding
before it's safely assigned.

* fix(room-view): select badge colors and simplify room observation

* fix(room-view): narrow room instead of casting in the header selector

The selector cast `room` to `ISubscription` and then read subscription-only
fields off it, reachable with the preview-room literal that has none of them.
Narrow once through the same `'id' in room` check the selector already uses.

* test(database): pin Subscription @JSON reference stability

RoomStore's field selectors compare @JSON references across observe() emits,
which holds only while the columns keep WatermelonDB's memo option. Dropping
it from the model fails 16 of these assertions.

* refactor(room-view): room predicates, membership tri-state and observable test helper

Add a shared BehaviorSubject-backed test helper for database.active so the
RoomStore, room-and-thread screens, MessageRow and messages list suites stop
hand-rolling observe fakes.

Replace the structural `in` checks on a room with isSubscriptionModel and
isPreviewRoom, drop `status` from TPreviewRoom, and narrow before reading
presence in the header.

Replace the boolean `joined` on RoomState with a RoomMembership tri-state of
preview, invited and subscribed. RoomGate now selects a single nullable
invitation. RoomFooter renders once the store is ready, so a subscribed room
no longer flashes the join footer while its subscription row is still being
looked up.

Match only WatermelonDB's not-found error as an absent row and log anything
else, still falling back to preview mode.

* docs(context): add Room Membership to the glossary
Centralize the Subscription-model discrimination that every render-time
Room field read repeated. fromSubscription(select, previewValue) applies
select when the room is a Subscription model and returns the reader's own
fallback in Preview Mode.

Migrate the render selectors in the message list, announcement banner,
message row, encryption status, read-only, omnichannel department, footer
federation, thread badge and the grouped useShallow selectors in header,
right buttons and subscription unreads. Persistence, initialization,
membership derivation and action-time guards keep the explicit check.
* perf(room-view): memoize MessageRow

* fix(message): derive separators inside MessageProvider

MessageRow is memoized, but WatermelonDB mutates records in place. A message
sent before midnight and confirmed after it keeps the same record identity, so
the memo skipped the re-render and the date separator stayed stale.

Move the derivation into MessageProvider, which already subscribes both the
message and the previous message to the same tick. MessageRow keeps calling the
shared getMessageSeparators for the LoadMore branch, which sits outside the
provider.
…ntity (#7670)

* refactor: render the empty room background when RoomView mounts without a route identity

* refactor: name the RoomBackground export and type its return

* test(RoomView): drop assertions on a testID the suite can never render

* refactor: keep rid and t required on the room screen props

* refactor: declare tmid on the room screen props
* test: prove RoomMessageActions sheet handle attachment

* refactor: pass RoomMessageActions refs directly

* test: reuse typed store factories in RoomMessageActions test
* test(RoomView): assert an exact-null render in RightButtons routing guards

* test(RoomView): drop composed RightButtons cases duplicated by routing and leaf suites
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.77.0.109609

@@ -0,0 +1,14 @@
import { type ReactElement, type ReactNode } from 'react';
import { A11y } from 'react-native-a11y-order';

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.

We should check if the lib doesn't do the verification about accessibility enable or not and no-op

@@ -57,27 +58,13 @@ const useMessageRoomStore = <T,>(selector: (state: MessageRoomState) => T): T =>
// Handlers and room constants captured once at mount by MessageRoomStoreProvider below (everything
// outside the reactive-tail resync effect). Callers MUST pass referentially stable values for these.
const FROZEN_KEYS = [

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.

Can we get rid of this?

const isIgnored = useIsIgnored(item?.u?._id);
const threadBadgeColor = useThreadBadgeColor(item.id);
const { lastSeen } = useRoomScreen();
const { dateSeparator, showUnreadSeparator } = getMessageSeparators(item, previousItem, lastSeen);

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.

What's the performance of this? Shouldn't we memo it?

Comment on lines +31 to +32
const room = useRoomStore(s => s.room);
const readOnly = useReadOnly();

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.

The components could use the slice hooks instead.

const navigation = useNavigation<IRoomViewProps['navigation']>();
const isMasterDetail = useMasterDetail();
// `t` comes from the store (seeded at mount) rather than route.params, which navigation can wipe.
const room = useRoomStoreByRid(rid, s => s.room);

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.

Do we need to get the whole room here?

Comment on lines +212 to 222
// Latest-closure ref so the trigger effect can key on messages alone (one run per re-observe) with
// honest deps: fetchMessages re-keys on highTs, so listing the handler would also fire this mid-jump.
const onReObserveRef = useRef(onReObserve);
// Refreshed in a layout effect declared first — layout effects run in declaration order, so the
// trigger below always reads this commit's closure.
useLayoutEffect(() => {
onReObserveRef.current = onReObserve;
});
useLayoutEffect(() => {
onReObserveRef.current();
}, [messages]);

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.

Over-engineered or slop?

aboveTs: Date | number | string,
direction: 'nearest' | 'closestToLiveTail'
): Promise<TAnyMessageModel | null> => {
const rows = (await database.active

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.

Do we need to await?

Comment thread app/views/RoomView/reactCompilerContract.test.ts Outdated
Comment thread jest.config.js Outdated
Comment thread package.json Outdated
#7678)

* chore: drop unused reassure devDependency

* test(MessageComposer): rename media transfer ownership suite after its component

* test(RoomView): move observableDatabase helper out of __tests__
… openThread/openRoom services (#7679)

Rename the hook to useJumpToMessage to match the CONTEXT.md Positional State row.
Move Thread and Room opening into plain async services with explicit deps.
Isolate route-param consumption in a non-exported helper hook so the
generation counter has a single owner. Behavior and public surface unchanged;
the three existing suites pass with only import renames.

NATIVE-1550
… compiler contract test (#7680)

* refactor(MessageActions): read the room at action time instead of during render

* test(RoomView): remove the React Compiler contract test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant