Skip to content

refactor: migrate RoomView to a function component - #7482

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

refactor: migrate RoomView to a function component#7482
diegolmello wants to merge 338 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 6, 2026 16:14
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`).
…ion (#7635)

* test(RoomView): replace LoadMore snapshot with state assertions

* fix(RoomView): cancel useThreadFollowing subscription on fast unmount

useThreadFollowing subscribed inside a promise callback, so a room opened
and closed before getMessageById resolved left the observer running: the
cleanup ran while unsubscribe was still undefined. A cancelled flag set in
cleanup and checked before subscribing closes that window, and also covers
a tmid/userId change, where cleanup runs before the pending resolve.

Dropped the paired suggestion to swap observe() for
observeWithColumns(['replies']): observeWithColumns is a Query method, not
a Model one, so it is not available on the record getMessageById returns.
The derived value is a boolean, so setState already bails out when it does
not change.

* perf(RoomView): stop observing last_message on the room record

The room observer woke on every incoming message, rebuilding roomUpdate and
re-rendering the root, providers, list, footer and composer. Livechat is the
only consumer of last_message: it now gets its own observer, created only for
t === 'l', publishing lastMessageFromAgent into the room store.

* refactor(RoomView): drop redundant hideSystemMessages re-filter

* refactor(RoomView): clone the observed messages only when appending the thread record

* refactor(RoomView): build the message query clauses once and drop Q.skip(0)

* refactor(RoomView): skip the readThread debounce timer outside threads

* refactor(RoomView): share the newer-loader lookup between the jump anchor and the rejoin

* refactor(RoomView): drop the double casts to AnchorMessage

* perf(RoomView): update the scroll FAB state only on a threshold crossing

* refactor(RoomView): type the animated message list instead of suppressing it

* refactor(message): shrink the inert message-action store to the action it serves

* refactor(RoomView): flatten the jumpToMessage target checks

* refactor(RoomView): share one footer action button between TakeOrJoin and OnHold

* refactor(RoomView): type roomUpdate from the subscription model instead of any

* test(RoomView): drop the tautological QUERY_SIZE assertion

* refactor(RoomView): drop the blank-label sentinel from LeftButtons

* test(RoomView): cover the visible system types clause

* perf(MessageComposer): read send-time values from the store instead of subscribing

* perf(RoomView): seed serverVersion into the room store at creation

joinRoom no longer reads the redux singleton at call time; the screen passes the version it already selects.

* perf(RoomView): skip the extra render when the acquired room store is unchanged

* refactor(RoomView): resolve thread names through fetchThreadName

pushThreadRoom carried its own copy of the removed-thread branch and had
drifted to `Thread`; the shared helper's `Message_removed` now wins for
both call paths.

* refactor(RoomView): one thread-press wiring for the message tree

The handlers hook wired pushThreadRoom a second time without onCancel, so
the loading overlay opened there had no cancel button. It now takes the
screen's onThreadPress, and onReplyInit delegates to it instead of
repeating the push.

* refactor(RoomView): call useReactionActions once per room screen

The handlers hook built a second set of reaction actions over the same
message action store; it now takes the ones the screen already created.

* refactor(RoomView): one sendRoomMessage wiring per room screen

The answer-button handler duplicated the screen's send wiring; it now
reuses it, and the send behaviour is covered on the service itself.

* refactor(message): one MessageRoomProvider

The provider forked into two components only to default timeFormat; the
setting is now read unconditionally and used when the caller passes none.

* test(RoomView): cover pushThreadRoom and fetchThreadName

pushThreadRoom is now the single owner of thread-name resolution and of
both reply paths, and neither it nor the helper had a test.

* feat(RoomView): render a retryable screen when room init fails

useRoomInit exposes failed and retry so a room whose init exhausted its
attempts no longer renders as an empty room.

* fix(RoomView): cancel pending debounced calls on unmount

useDebounce now returns DebouncedState so callers reach .cancel; the list
onEndReached and the readThread timer no longer fire after unmount.

* refactor(RoomView): share a RoomPlaceholder shell across blocking screens

* refactor(RoomView): gate blocking screens before mounting the room tree

RoomView's blocked-room checks ran after twelve hooks, so an invited or E2EE-blocked room opened the DDP subscription and ran init before being told it was blocked, and unblocking swapped component types at the same render position.

index.tsx is now a thin RoomGate that owns the screen-identity snapshot, the room store acquisition, the header and the blocked-screen checks; the room tree moves to RoomScreen.tsx and mounts only once unblocked. RoomLoadFailed stays in RoomScreen: it needs useRoomInit's failed/retry, which belongs to the mounted room.

* docs(message): remove ARCHITECTURE.md

* fix(RoomView): read the Workspace version when taking an inquiry

Warmed rid-keyed RoomStores were created without a server version, so joining an Omnichannel room from one selected the removed DDP path. takeInquiry now reads the version from the store at call time and the serverVersion plumbing is removed from RoomStore, joinRoom and RoomGate.

* fix(RoomView): harden Jump to Message navigation and cancellation

- jumping to a different Thread Parent compares the target id with the active tmid instead of relying on replies
- navigation callbacks may return promises and are awaited, so failures reach the existing error handling
- pushThreadRoom hides loading in a finally when the thread name lookup rejects
- cancellation bumps a generation token checked after each asynchronous stage

* fix(message): call the latest MessageRoomProvider callbacks

Replace the frozen callback contract with stable wrappers that call the latest prop, so callbacks recreated during normal renders are honored in production without re-rendering consumers.

* fix(RoomView): drop stale init failure and unreachable invite transition

Failure is only exposed while the room still has initialization work, so logging out after a failed run no longer keeps the failure screen. The invite-acceptance transition effect could never run because the hook is not mounted while invited.

* fix(RoomView): invalidate earlier jumps and keep store acquisition out of the state updater

Each jumpToMessage call now advances the generation so an older in-flight jump
cannot scroll or navigate after a newer one starts, and a stale failure no
longer cancels the newer jump. useRoomStoreForScreen acquires the registry
entry outside the setState updater. Adds explicit return types and an interface
for RoomPlaceholderProps.

* Derive room store type from subscription

* fix(RoomView): cache routing config per server and derive on-hold from room state (#7638)

* fix(RoomView): parse the route once into a valid screen identity or a failure state

* refactor(RoomView): split RoomScreen into useRoomMessaging and focused room components

RoomScreen now only wires the room lifecycle hooks and the render tree. Message
orchestration (message-action store, imperative handles, navigation, init, send)
lives in useRoomMessaging, grouped by the component that consumes each slice.
RoomMessageList owns the message tree and its settings; RoomAnnouncementBanner,
RoomUploadProgress and RoomMessageActions read the room and user themselves.

useRoomSubscription creates its own RoomClass. The route-seeded quote effect in
useRoomInit was a no-op (the store is already seeded with the quote) and is gone.

* fix(RoomView): cache routing config per server and derive on-hold from room state (#7638)

* fix(RoomView): stop the thread from opening after cancelling its loading overlay

Also move the new test files into __tests__, drop the explanatory comments added in this branch, and share one style and one useTheme call in RoomPlaceholder.

* refactor(RoomView): pass RoomGate props explicitly and drop the route parser comment

* chore(RoomView): drop references to the removed ARCHITECTURE.md

* fix(RoomView): replace the store room when the subscription row is recreated

observeRoom only rewrote room when a roomAttrsUpdate attribute differed from the previous snapshot, so a subscription row recreated with identical attributes (leave and rejoin while the screen is open) left the store pointing at the deleted model instance. A lastMessage-only change never had this problem: WatermelonDB re-emits the same cached instance, so the stored room already reflects it.

* fix(RoomView): clear livechat-only flags when the room type is not livechat

lastMessageFromAgent was only recomputed while the subscription row had t === 'l', and
useOmnichannelPermissions returned early for any other type, so both left their last
livechat value in the store. Derive every flag unconditionally: non-livechat rows always
write false.

* fix(RoomView): reset the cached routing config on logout

Since 5c7a16c the omnichannel routing config is cached per server URL in a module-level store. Logging out and back into the same URL kept the cached returnQueue value, so an admin-side routing change never reached the client. The store now resets when logout runs, matching the per-mount refetch that existed before the cache.

* fix(RoomView): gate setParams thread jumps on loaded thread messages

A jumpToMessageId delivered via setParams to a thread RoomView fired
immediately, bypassing the onThreadMessagesLoaded gate the mount path
uses. Before the thread window is populated a non-anchored thread jump
aborts and parks on the live tail. The setParams path now parks the id
in the same pending slot until the thread messages have loaded.

* refactor(RoomView): reuse the livechat check and rename the non-livechat flags test

* refactor(RoomView): name the recreated-row check in observeRoom

* fix(RoomView): key the thread jump gate to the loaded thread and tighten its test

* fix(RoomView): reset the routing config cache on account deletion and cover logout in tests

* test(RoomView): cover the thread jump gate across a thread switch

* refactor(logout): reset the routing config cache in removeServerData

* test(logout): drop the duplicated routing config reset case

* refactor(RoomView): pass useRoomMessaging results as explicit props

Flatten the hook result so RoomScreen names every prop it passes instead of
spreading consumer-shaped bundles, drop the explanatory comments, and keep
room init ahead of the room subscription as before the split.

* refactor(RoomView): useRoomMessaging has no JSX, use .ts

* fix(RoomView): re-render the banner and message list on room updates

* test(RoomView): replace adapter-dependent visible-row tests with predicate unit tests

Also moves visibleSystemMessages out of the hooks folder, since it exports no hooks.

* Move routing config cache to omnichannel redux
Pass provider props explicitly instead of spreading, flatten MessageRow into early returns, route goSearchView through navigateToScreen so the ts-ignore can go, and read settings through useSetting.

Hoist RoomPlaceholder's stylesheet to module scope, drop Banner's memo comparator that ignored title and closeBanner, and remove InvitedRoom's unused loading prop.
* refactor(MessageComposer): restore memoization and narrow store subscriptions

ComposerInput emits React Compiler errors, so it is never auto-memoized: restore its memo wrapper and the useFocusEffect useCallback deps, which the emitter cleanup depends on.

Hoist MessageComposerContainer's default children out of JSX so the file compiles again, narrow CancelEdit and useChooseMedia to useMessageActionKind, name the a11y announce delay and use optional call syntax for onClosed.

* chore: keep List hook tests in place

Two test file moves were committed here by mistake; they belong with the hooks changes that fix their import paths.
…rection (#7643)

Collapse multi-field room store reads into single useShallow selectors, rebuild useUnreadsCount on the shared useObservable helper, remove a duplicate rid subscription and a redundant live-ref layer, give the navigation mirror effect a dependency array, pass RoomHeader props explicitly, and move the two stray List hook tests into __tests__.
* refactor(RoomView): own room stores per screen

* refactor(RoomView): tidy room store and services

Return a discriminated result from loadRoom, dedupe init, and cut observeRoom down to a single state read with the room snapshot built only when the room actually changes. Derive blockAction params from the trigger type, split pushThreadRoom's name accumulator, name the jump commit wait, and move the store and service tests into __tests__.

* refactor(RoomView): re-apply the room store registry removal lost in a merge

Merge 0cf4e58 revived the rid-keyed registry that 201e293 removed. It had no production callers, so this deletes it again on top of the tidied store, collapses the observeRoom overload it required, threads init's abort signal into loadRoom, and drops the tests that only covered registry lifetime.

* refactor(RoomView): move omnichannel tests into __tests__ and drop stale rid-keyed claims

The routing-config suites were colocated instead of living under __tests__, and its reducer imported actionsTypes twice. The room/thread screen suite and useSubscriptionUnreads still described a rid-keyed store that no longer exists.

* test(MessageRoomStore): pass reactionInit explicitly instead of spreading a partial state
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.77.0.109576

The count skips subscriptions whose unread status is hidden, but the
observer only watched the unread column, so toggling hide unread status
on another room left the badge stale until an unrelated unread change.
…-hooks-5

# Conflicts:
#	app/lib/hooks/useShortnameToUnicode/index.tsx
* refactor: centralize jump navigation lifecycle in RoomView

* refactor(RoomView): share the jump generation check and tidy navigation tests

* test(RoomView): drop a no-op helpers mock in the composed navigation suite
…7657)

* refactor: give shared composer ownership of input and configuration

* test: seed message quotes through the restoration API

* test: obtain chooseFile through renderHook in the ShareView bridge tests

* refactor: share room contract across composer and screens (#7660)

* refactor: share room contract across composer and screens

* refactor: constrain room update patches to observed fields

* refactor: name the observed room fields for what they are

* test: extract media transfer ownership tests into a focused suite with per-instance probes

* test: restore real timers in afterEach for ShareView
@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.
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