Skip to content

feat: allow only one swiped room item at a time - #7719

Merged
diegolmello merged 62 commits into
developfrom
diegolmello/global-room-item-swipe
Sep 30, 2026
Merged

diegolmello merged 62 commits into
developfrom
diegolmello/global-room-item-swipe

Conversation

@diegolmello

@diegolmello diegolmello commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Proposed changes

Only one room item can be swiped open at a time. With a row open, pressing or tapping another row closes the open row with a smooth spring animation, and that tap does not navigate or open the long-press action sheet; the next tap navigates normally. Tapping the open row itself closes it without navigating. Swiping another row closes the open one while the new row follows the drag. A long swipe (auto-toggle-read/hide) and a long press (action sheet) behave as before.

Swipe actions follow a WhatsApp-style layout:

  • The left action (mark read or unread) opens to the same width as each right action (favorite, hide).
  • A light haptic fires when a swipe crosses the full-swipe threshold, on both sides, only while the finger is down. Release and spring animations never fire it.
  • Releasing from closed opens a side only past half of one action width; otherwise the row springs back closed. An open row stays open unless it is swiped fully closed. The release decision lives in getSwipeRelease, covered by unit tests.

The swipe springs share one critically damped spring config (mass 1, stiffness 150, damping 24.5), so the auto-close animation matches the manual swipe feel, and the close animation starts on the UI thread.

The "currently open row" is tracked in a module-level Reanimated value (makeMutable), so pressing another row closes the open one directly on the UI thread.

The row now refreshes when its room record changes. React Compiler cached the row's read state, title, avatar and invite state on the record object, which WatermelonDB updates in place, so the row kept stale values and the swipe kept sending "mark read" instead of toggling. These values now come from a useSyncExternalStore subscription to the record, and the custom memo comparator is removed. Rooms with only unread thread replies count as unread, matching the unread badge.

The focused-row highlight only shows in master-detail layout, where the selected room is visible next to the list; on phones a row no longer stays highlighted after returning from its room.

Issue(s)

https://rocketchat.atlassian.net/browse/NATIVE-1676

How to test or reproduce

  1. Swipe row A open; it stays open.
  2. Press down on row B, or start swiping it; row A closes with a smooth spring animation, and that press/tap does not navigate or open the action sheet.
  3. Tap row A while it is open; it closes without navigating.
  4. Tap a row with nothing open; it navigates as usual.
  5. Swipe row B while row A is open; row A closes while row B follows the drag.
  6. Swipe a row right open; the mark read action is as wide as each of the favorite and hide actions.
  7. Swipe a closed row a little and release; it springs back closed. Swipe past half an action and release; it opens.
  8. With a row open, swipe it partly toward closed and release; it springs back open. Swipe it all the way closed; it closes.
  9. Long swipe past the threshold to auto-trigger toggle-read/hide; one haptic fires as the threshold is crossed, none on release.
  10. Full left swipe a room repeatedly; it toggles between read and unread each time.
  11. Mark a room read or unread from another client; its row updates without scrolling it away.
  12. Long press a row to open the action sheet; unaffected by this change.

Verified on the iOS simulator. On the Android emulator, synthetic pan gestures did not open rows, so Android still needs manual verification.

Scrolling that starts on the open row does not close it. A touch that starts on another row (including a scroll) closes it.

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)

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

iOS demo (video): tap on another row closes the open row without navigating, then a normal tap navigates.

ios-tap-closes-final.mp4

iOS demo (video): swipe release rules on both sides, full swipes and threshold crossing.

room-swipe-release-rules-short.mp4

Summary by CodeRabbit

  • New Features
    • Swipe action widths now adapt to the room row, with actions revealing progressively as you swipe.
    • Haptic feedback is triggered when crossing swipe action thresholds during an active swipe.
    • Swipe animations respect the device’s reduced-motion setting.
  • Bug Fixes
    • Opening one room’s swipe actions now closes any other room’s open actions.
    • Taps or long presses used to close swipe actions no longer trigger the room’s normal actions.
    • Swipe release behavior more reliably opens, closes, or completes an action based on swipe distance.
    • Closing animations use a consistent spring effect, including when another room’s actions close.

diegolmello and others added 29 commits September 15, 2026 10:35
* chore: upgrade dependencies and patches to RN 0.86 / Expo 57

* chore: align upgrade toolchain floors and CI checks

* chore: update Jest toolchain for RN 0.86 and Expo 57
* fix: adapt app code to React Native 0.86 and Expo 57

* test: await scheduled gesture and floating date callbacks
* fix(ios): support React Native 0.86 and Expo 57

* test: fix jest failures under Node 22

Resolve Expo's lazy fetch global in jest setup so suites that fully mock
react-native no longer crash when jest enumerates globals at teardown.

Restore real timers only after Testing Library unmounts, so immediates
scheduled with fake timers are cancelled with the same clock.

* fix(android): adopt Expo 57 autolinking plugin and drop jcenter from patched libraries

Expo 57 removed scripts/autolinking.gradle in favour of the
expo-autolinking-settings and expo-root-project Gradle plugins.

Gradle 9 removed jcenter(), which @react-native-cookies/cookies and
react-native-math-view still declared.

Bump the iOS Pods cache key so runners stop restoring Local Podspecs
generated for the previous React Native version.

* fix(android): build with React Native 0.86 codegen and Expo 57 prebuilt modules

Bump @react-native/codegen, babel-preset and typescript-config to 0.86.3
so libraries stop resolving the stale 0.80 codegen.

Build the four patched Expo packages from source, since Expo 57 ships
prebuilt Android artifacts that ignore patch-package changes.

Patch camera-roll to drop string literal unions from its spec,
keyboard-controller for the non-null onConfigurationChanged signature,
math-view to drop the removed UIManagerModuleListener import, and
expo-font for the nullable loadedFonts field.

Declare androidx.localbroadcastmanager explicitly and resolve the
WRITE_EXTERNAL_STORAGE maxSdkVersion manifest conflict.

* fix: drop undefined request headers rejected by Expo fetch

Expo 57 installs its native fetch as the global and rejects header
entries whose value is undefined. getServerInfo sent X-Auth-Token and
X-User-Id as undefined before login, so every deep link login fell back
to the Add workspace screen and all E2E flows failed.

* test(e2e): wait for LoginView transition before keyboard navigation

* fix(login): keep focus on the password field when submitting with a hardware keyboard

* fix(ios): compile the release bundle with the hermesc matching the bundled Hermes runtime

* fix(ios): expose room header testIDs to accessibility tree

An accessible wrapper around the room header hides its children from
XCUITest on React Native 0.86, so Maestro could not find the header
title or the header button. Move the accessibility label and role to
the pressable header, which already exposes its children.

* fix(ios): expose rooms list header testIDs to accessibility tree

Same cause as the room header: an accessible wrapper inside the native
stack header hides its children from XCUITest, so Maestro could not find
the servers list button.

* fix(ios): bump react-native-linear-gradient to 2.8.3 to stop zero-size layer crash on mention autocomplete

* fix(ios): address upgrade review feedback

* fix(ios): exclude Android keyboard flow change
* fix: update Android build for React Native 0.86

* fix: restore Android Gradle 9 compatibility

* fix: align React Native codegen version

* fix: resolve Android storage permission merge

* fix: patch Android modules for React Native 0.86

* fix: restore Android local broadcast dependency

* fix: build patched Expo modules from source

* fix: raise iOS deployment target to 16.4 for Expo 57

* fix: materialize expo fetch global before jest mocks

* fix: keep setImmediate real under jest fake timers

* fix: key iOS pods cache by Podfile.lock hash

* fix: allow non-modular React includes in framework modules

* fix: drop undefined header values before fetch

expo/fetch, installed as the global fetch on native by Expo 57, forwards header entries to the native request verbatim, so an undefined value fails the Kotlin cast to Pair<String, String> and rejects the whole request.

* fix: build React-Core from source instead of prebuilt

* fix: match Expo import access level with generated provider

* fix: adopt Expo 57 AppDelegate contract

* fix: compile Hermes bytecode with the legacy compiler

* fix: expose header test ids on iOS

* fix: expose team channels header test id

* fix: remove iOS changes from Android upgrade
Removed the three Podfile opt-outs (RCT_USE_PREBUILT_RNCORE, RCT_USE_RN_DEP,
RCT_HERMES_V1_ENABLED) and the HERMES_CLI_PATH post_install override, letting
React Native 0.86 use its prebuilt React-Core/ReactNativeDependencies
tarball and Hermes V1 (250829098.0.17) instead of building from source.

Dropped the RCTViewManager accessibility-role patch: it only touched the
Paper renderer, the app runs on Fabric, and the file no longer compiles
under prebuilt Core. VoiceOver role-update behavior is covered by manual
smoke testing, not this patch.

RNFB pods forced to static libraries in the pre_install hook to avoid a
non-modular-include failure on RNFB 21.x; the fix ships in
react-native-firebase >= 26.3.0, so this can be dropped on that bump.

SocketRocket now ships vendored inside the ReactNativeDependencies
xcframework under a namespaced header path, so SSLPinning.mm's import
changed from a flat "SRWebSocket.h" to <SocketRocket/SRWebSocket.h>.

Copy Pods Resources phases regenerated by pod install, dropping the
boost/glog/RCT-Folly privacy bundles that no longer exist as separate pods.
* fix(e2e): reach login email field by focus instead of Dpad count

The keyboard navigation onboarding flow walked to the email field with
four blind Remote Dpad Down presses. Removing the decorative OR
separator from the accessibility focus chain shortened the chain by one,
so the email was typed into the password field and the password into the
email field, and login was never submitted.

Press Down until login-view-email reports focused, bounded by times: 10,
and assert focus before typing. Maestro returns normally when a repeat
bound is exhausted, so without the assertion an over-long chain would
fail later at the rooms-list-view wait with an unrelated error.

The separator only renders when totalServices > 0, so the chain length
depends on server config and no fixed count is correct.

* fix(e2e): reach password and submit by focus, hide keyboard first

Anchoring the email field exposed two more blind steps in the same flow.
The hop to the password field assumed Enter moves focus, and the submit
assumed Enter on the password field logs in. Neither had been exercised
before, because the flow failed earlier.

Press Down until each target reports focused, bounded and asserted like
the email field. The submit loop needs the soft keyboard hidden first:
with the keyboard up the Dpad presses go to the input method and never
reach the login screen, so the submit button is never focused.
* chore(deps): bump reanimated to 4.6.0 and worklets to 0.12.2

Reanimated 4.6.0 requires react-native-worklets 0.12.x, so both move
together. Worklets drops its caret to match reanimated's exact pin.

Reanimated 4.6.0 initializes CSS event handling at import time, which
the JS-only fallback used by Jest does not implement, so the mock entry
point throws on load. Stubbing setCSSEventHandler at its source keeps the
rest of the proxy module real.

* chore(ios): update Podfile.lock for reanimated and worklets

CocoaPods preserves versions already in Podfile.lock, so pod install
would not have picked these up on its own.

* chore(ios): bump ffi to 1.17.4 for Xcode 27 compatibility
iOS 27 SDK requires scene-based app lifecycle. The app now uses
Expo's ExpoAppSceneDelegate via a new SceneDelegate, with
startReactNative and boot splash initialization moved out of the
app delegate into the scene delegate.
…-pad

The RN 0.86 gesture-handler bump added an Android-only onKeyUp override
that intercepts DPAD_CENTER/ENTER/SPACE and activates whichever button
currently holds native view focus, bypassing the default performClick()
path. Every composer and header icon button is a BorderlessButton, so
this silently fired the wrong button (e.g. the header drawer button) on
hardware Enter/D-pad and swallowed the key event elsewhere. Removed the
hunk; the iOS press-handling hunk is untouched.
Track the currently open swipeable room item in a single UI-thread
mutable value (openSwipeItemId) instead of per-item state. Every
Touchable reacts to that value and springs itself closed once another
row claims it, so only one room can be swiped open at a time. Tapping
a room, long-pressing it, or opening its own actions all clear or
reassign ownership.

Also replaces the abrupt default spring with an explicit, softer
config (damping/mass/stiffness) shared by every open/close animation.
openSwipeItem.test.ts only asserted that a shared value keeps what
was written to it, which added no coverage. GestureDetector/pan
behavior isn't exercisable through the existing jest reanimated
mocks (useAnimatedReaction is a no-op), so a real cross-row close
test isn't practical here without a broader mocking change. Revert
the makeMutable jest mock added for that test since nothing else in
the suite needs it.
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.77.0.109809

Derive the open side from the resting offset instead of a separate row
state, release and register the row inside the gesture worklet, fire the
threshold haptic from the pan update, and flip gesture input and output
once for right-to-left layouts. Drop the duplicate long-press gesture and
skip the snapshot rebuild on the record's initial emission.
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.77.0.109814

Base automatically changed from diegolmello/native-1561-rn-upgrade to develop September 30, 2026 13:18
…-room-item-swipe

# Conflicts:
#	app/containers/RoomItem/Actions.tsx
#	app/containers/RoomItem/Touchable.tsx
#	app/containers/RoomItem/__snapshots__/RoomItem.test.tsx.snap

@OtavioStasiak OtavioStasiak 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.

LGTM

@diegolmello
diegolmello merged commit d24ad03 into develop Sep 30, 2026
2 of 4 checks passed
@diegolmello
diegolmello deleted the diegolmello/global-room-item-swipe branch September 30, 2026 20:53

This branch is waiting to be deployed

3 waiting deployments
android_build — 5b3c08ce Waiting Sep 30, 2026 by diegolmello via Build Android / Hold #6906
ios_build — 5b3c08ce Waiting Sep 30, 2026 by diegolmello via Build iOS / Hold #6906
approve_e2e_testing — 5b3c08ce Waiting Sep 30, 2026 by diegolmello via E2E Hold #6906
upload_android — d9b95019 Deployed Sep 30, 2026 by diegolmello via Build Android / Upload Hold #6866
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.

2 participants