feat(voip): tap-to-hide call controls with animations#7078
feat(voip): tap-to-hide call controls with animations#7078diegolmello wants to merge 9 commits intofeat.voip-lib-newfrom
Conversation
Add toggle/show controls visibility for tap-to-hide animation support in CallView. Includes convenience selector and animation duration constant.
Wrap CallerInfo with Pressable to toggle controlsVisible on tap. Animate caller name row with fade + slide using Reanimated. Avatar remains always visible and centered.
Wrap CallButtons in Animated.View with opacity and translateY animations driven by controlsVisible store state. Set pointerEvents to 'none' when hidden to block ghost taps.
Update 'caller-info' to 'caller-info-toggle' to match the testID rename from the CallerInfo toggle implementation.
Replace active-call View with Animated.View that slides up and fades out when controlsVisible is false. Animation only applies when focused (expanded call view); collapsed header bar remains always visible.
Show controls automatically when stateChange, trackStateChange events fire or when toggling focus, so users never miss important state updates.
WalkthroughAdds a Changes
Sequence DiagramsequenceDiagram
actor User
participant CallerInfo as CallerInfo<br/>(Pressable)
participant Store as useCallStore
participant CallButtons as CallButtons<br/>(Animated)
participant Header as MediaCallHeader<br/>(Animated)
participant AnimSys as Reanimated
User->>CallerInfo: Tap
CallerInfo->>Store: toggleControlsVisible()
Store->>Store: controlsVisible = !controlsVisible
Store-->>CallButtons: notify controlsVisible
Store-->>Header: notify controlsVisible
CallButtons->>AnimSys: update animated style (opacity, translateY)
AnimSys-->>CallButtons: animate (~300ms)
CallButtons->>CallButtons: set pointerEvents (auto/none)
Header->>AnimSys: update animated style if focused && !controlsVisible
AnimSys-->>Header: animate (~300ms)
Header->>Header: set pointerEvents (auto/none)
Note over Store: On call state/track change or toggleFocus
Store->>Store: showControls() -> controlsVisible = true
Store-->>CallButtons: notify controlsVisible
Store-->>Header: notify controlsVisible
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/lib/services/voip/useCallStore.ts (1)
208-210: Consider whethershowControlsis needed.The
showControlsaction is defined but the auto-reveal logic uses inlineset({ controlsVisible: true })instead. This action may be useful for external consumers or future enhancements. If it's not needed externally, you could simplify by removing it, or alternatively refactor the inline sets to use this action for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/useCallStore.ts` around lines 208 - 210, The store defines a showControls action but other code uses inline set({ controlsVisible: true }), causing duplication; either remove the showControls action if it has no external usage, or replace the inline set({ controlsVisible: true }) calls with calls to showControls for consistency. Locate the showControls function in useCallStore and the places that call set({ controlsVisible: true }) and choose one approach: delete showControls and keep inline sets (and remove its export), or refactor those inline sets to call showControls so all visibility changes go through the single action.app/lib/services/voip/useCallStore.test.ts (1)
33-35: Forward event payloads in the mock emitter.This helper currently invokes listeners without args, which can mask regressions if store handlers later depend on event payloads.
♻️ Proposed tweak
- const emit = (ev: string) => { - listeners[ev]?.forEach(fn => fn()); - }; + const emit = (ev: string, ...args: unknown[]) => { + listeners[ev]?.forEach(fn => fn(...args)); + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/useCallStore.test.ts` around lines 33 - 35, The mock emitter function emit currently calls listeners without forwarding payloads; update the emit helper in useCallStore.test.ts so its signature accepts variadic payloads (e.g., emit(ev: string, ...args)) and invoke each listener with those payloads (fn(...args)); also adjust the listeners typing if necessary so listener functions can receive arguments, ensuring tests exercise real event payload handling used by the store handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/views/CallView/components/CallButtons.tsx`:
- Around line 30-33: The animated worklet in containerStyle captures
controlsVisible at creation time so it won't react to zustand updates; convert
the zustand boolean (from useControlsVisible) into a reanimated shared value
(useSharedValue) and sync it in a useEffect when the selector changes, then
reference that shared value inside useAnimatedStyle so opacity/translateY use
withTiming against the shared value (keep CONTROLS_ANIMATION_DURATION as-is) to
restore reactivity.
In `@app/views/CallView/components/CallerInfo.tsx`:
- Around line 17-20: The animated style uses the plain JS boolean
controlsVisible inside useAnimatedStyle which harms Reanimated performance;
convert controlsVisible into a Reanimated shared value and reference that inside
callerRowStyle. Specifically, create a useSharedValue (e.g.,
controlsVisibleShared), update it when the React prop/store changes (via
useEffect or a derived value) and then replace references to controlsVisible in
the useAnimatedStyle callback for callerRowStyle with
controlsVisibleShared.value while keeping CONTROLS_ANIMATION_DURATION and the
same withTiming logic.
---
Nitpick comments:
In `@app/lib/services/voip/useCallStore.test.ts`:
- Around line 33-35: The mock emitter function emit currently calls listeners
without forwarding payloads; update the emit helper in useCallStore.test.ts so
its signature accepts variadic payloads (e.g., emit(ev: string, ...args)) and
invoke each listener with those payloads (fn(...args)); also adjust the
listeners typing if necessary so listener functions can receive arguments,
ensuring tests exercise real event payload handling used by the store handlers.
In `@app/lib/services/voip/useCallStore.ts`:
- Around line 208-210: The store defines a showControls action but other code
uses inline set({ controlsVisible: true }), causing duplication; either remove
the showControls action if it has no external usage, or replace the inline set({
controlsVisible: true }) calls with calls to showControls for consistency.
Locate the showControls function in useCallStore and the places that call set({
controlsVisible: true }) and choose one approach: delete showControls and keep
inline sets (and remove its export), or refactor those inline sets to call
showControls so all visibility changes go through the single action.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 38495b52-7fdb-4b65-86d3-caa69559972d
⛔ Files ignored due to path filters (3)
app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/__snapshots__/index.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snapis excluded by!**/*.snap
📒 Files selected for processing (11)
app/containers/MediaCallHeader/MediaCallHeader.test.tsxapp/containers/MediaCallHeader/MediaCallHeader.tsxapp/lib/services/voip/useCallStore.test.tsapp/lib/services/voip/useCallStore.tsapp/views/CallView/components/CallButtons.test.tsxapp/views/CallView/components/CallButtons.tsxapp/views/CallView/components/CallerInfo.test.tsxapp/views/CallView/components/CallerInfo.tsxapp/views/CallView/index.test.tsxapp/views/CallView/styles.tsprogress-controls-animation.md
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-10T15:21:45.098Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7046
File: app/containers/InAppNotification/NotifierComponent.stories.tsx:46-75
Timestamp: 2026-03-10T15:21:45.098Z
Learning: In `app/containers/InAppNotification/NotifierComponent.tsx` (React Native, Rocket.Chat), `NotifierComponent` is exported as a Redux-connected component via `connect(mapStateToProps)`. The `isMasterDetail` prop is automatically injected from `state.app.isMasterDetail` and does not need to be passed explicitly at call sites or in Storybook stories that use the default (connected) export.
Applied to files:
app/views/CallView/components/CallerInfo.test.tsxapp/views/CallView/components/CallButtons.tsxapp/views/CallView/components/CallerInfo.tsx
🔇 Additional comments (13)
app/views/CallView/styles.ts (1)
5-6: LGTM!Good practice extracting the animation duration as a shared constant. This ensures consistent timing across all animated components (
CallButtons,CallerInfo,MediaCallHeader) and makes future adjustments easy.app/lib/services/voip/useCallStore.ts (1)
204-210: LGTM - clean state management implementation.The
toggleControlsVisibleandshowControlsactions follow Zustand patterns correctly. Auto-revealing controls onstateChange,trackStateChange, andtoggleFocusprovides good UX by ensuring users see the controls during important call events.app/views/CallView/components/CallButtons.tsx (1)
52-55: LGTM for the Animated.View setup.The
pointerEventstoggle is a good practice to prevent accidental taps on hidden controls, and maintainingtestIDensures testability.app/views/CallView/components/CallerInfo.test.tsx (1)
57-70: LGTM - good test coverage for toggle behavior.The test properly verifies the complete toggle cycle (true → false → true) and directly accesses store state which is appropriate for testing Zustand stores.
app/containers/MediaCallHeader/MediaCallHeader.test.tsx (1)
179-210: LGTM - comprehensive test coverage for pointer events behavior.The three test cases properly cover the visibility matrix:
focused=true, controlsVisible=false→pointerEvents='none'focused=true, controlsVisible=true→pointerEvents='auto'focused=false→pointerEvents='auto'(header always interactive when collapsed)This correctly validates the conditional hiding logic.
app/views/CallView/index.test.tsx (1)
94-95: LGTM - testID references updated consistently.All assertions correctly updated from
caller-infotocaller-info-toggleto match the renamed/restructuredCallerInfocomponent.Also applies to: 105-106, 135-138, 302-303, 314-315
app/views/CallView/components/CallerInfo.tsx (1)
26-35: LGTM - clean implementation of tap-to-hide with correct separation.The avatar correctly remains outside the animated view (always visible), while only the caller name row animates. The
Pressableprovides the expected tap target covering the entire caller info area.app/containers/MediaCallHeader/MediaCallHeader.tsx (2)
33-38: Logic is correct; same animated style note applies.The
shouldHide = focused && !controlsVisiblelogic correctly ensures:
- Header hides only when the CallView is focused AND controls are toggled off
- Header stays visible when collapsed (not focused), regardless of
controlsVisibleThis matches the PR objective: "collapsed header bar remains visible."
50-61: LGTM - well-structured animated header.The Animated.View correctly applies the combined styles and manages pointer events. The empty state branch appropriately remains a plain View since no animation is needed when there's no call.
app/lib/services/voip/useCallStore.test.ts (2)
57-118: Good coverage forcontrolsVisiblelifecycle.The suite exercises default state, action transitions, reset behavior, focus toggling, and auto-reveal on call events—this is solid coverage for the new store contract.
174-174: Nice fix in stale-timer setup.Passing
createMockCall('x').callkeeps the test aligned withsetCallinput shape while preserving the helper’s event emitter API.app/views/CallView/components/CallButtons.test.tsx (1)
41-63: Targeted interaction-gating tests look good.Verifying
pointerEventsfor both hidden and visible states gives clear coverage for preventing ghost taps when controls are hidden.progress-controls-animation.md (1)
1-123: Well-structured rollout documentation.The slice breakdown, demos, and decision log are clear and actionable for both implementation tracking and QA validation.
| const containerStyle = useAnimatedStyle(() => ({ | ||
| opacity: withTiming(controlsVisible ? 1 : 0, { duration: CONTROLS_ANIMATION_DURATION }), | ||
| transform: [{ translateY: withTiming(controlsVisible ? 0 : 100, { duration: CONTROLS_ANIMATION_DURATION }) }] | ||
| })); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
react-native-reanimated 3 useAnimatedStyle reactivity with JavaScript values
💡 Result:
In React Native Reanimated 3, useAnimatedStyle primarily reacts to changes in shared values (created with useSharedValue) for automatic UI thread updates without triggering React re-renders. The official documentation states: "Styles are automatically updated whenever an associated shared value or React state changes." However, direct use of plain JavaScript values (like regular React state from useState) inside the useAnimatedStyle worklet does not automatically trigger updates, as the worklet captures dependencies at creation time and plain JS values are not tracked like shared values. To achieve reactivity with JavaScript values: 1. Mirror React state in a shared value: Update the shared value in a useEffect when the JS value changes. Example: const [jsState, setJsState] = useState(0); const sharedValue = useSharedValue(0); useEffect( => { sharedValue.value = jsState; }, [jsState]); const animatedStyle = useAnimatedStyle( => ({ opacity: sharedValue.value, })); This ensures the style updates smoothly on the UI thread. The optional dependencies array is mainly for web without the Babel plugin and does not enable reactivity to plain JS values on mobile. Known issues exist where styles may not update on re-render if shared values are set during render (e.g., GitHub issues #6276, #6298), fixed in later patches like 3.14.0. Always update shared values outside render (e.g., in event handlers or useEffect). For optimal performance, prefer shared values over React state for animation drivers, as plain JS state changes cause re-renders while shared values do not.
Citations:
- 1: https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedStyle/
- 2: https://docs.swmansion.com/react-native-reanimated/docs/3.x/core/useAnimatedStyle
- 3: https://docs.swmansion.com/react-native-reanimated/docs/3.x/core/useSharedValue
- 4: useAnimatedStyle does not update Animated.View styles on rerender software-mansion/react-native-reanimated#6298
- 5: Setting shared value used in
transformprop ofuseAnimatedStyledoesn't trigger UI change if assigned on component re-render software-mansion/react-native-reanimated#6276
🏁 Script executed:
# First, let's look at the CallButtons.tsx file around the relevant lines
head -50 app/views/CallView/components/CallButtons.tsx | tail -30Repository: RocketChat/Rocket.Chat.ReactNative
Length of output: 1079
🏁 Script executed:
# Also check what useControlsVisible returns
rg -n "useControlsVisible" app/views/CallView/components/CallButtons.tsx -A 2 -B 2Repository: RocketChat/Rocket.Chat.ReactNative
Length of output: 502
🏁 Script executed:
# Find where useControlsVisible is defined
fd -name "*.ts" -o -name "*.tsx" | xargs rg -l "export.*useControlsVisible" | head -5Repository: RocketChat/Rocket.Chat.ReactNative
Length of output: 363
🏁 Script executed:
cat -n app/lib/services/voip/useCallStore.ts | head -100Repository: RocketChat/Rocket.Chat.ReactNative
Length of output: 3857
🏁 Script executed:
cat -n app/lib/services/voip/useCallStore.ts | tail -100Repository: RocketChat/Rocket.Chat.ReactNative
Length of output: 3160
Animated style should use a shared value for proper reactivity.
In react-native-reanimated 3.x, useAnimatedStyle worklets capture dependencies at creation time. The controlsVisible value comes from a zustand store selector (not a shared value), so changes to it won't automatically trigger the worklet to re-run. Use useSharedValue to sync the zustand state, or extract it to a shared value via useEffect:
const controlsVisibleBool = useControlsVisible();
const controlsVisibleShared = useSharedValue(controlsVisibleBool);
useEffect(() => {
controlsVisibleShared.value = controlsVisibleBool;
}, [controlsVisibleBool]);
const containerStyle = useAnimatedStyle(() => ({
opacity: withTiming(controlsVisibleShared.value ? 1 : 0, { duration: CONTROLS_ANIMATION_DURATION }),
transform: [{ translateY: withTiming(controlsVisibleShared.value ? 0 : 100, { duration: CONTROLS_ANIMATION_DURATION }) }]
}));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/views/CallView/components/CallButtons.tsx` around lines 30 - 33, The
animated worklet in containerStyle captures controlsVisible at creation time so
it won't react to zustand updates; convert the zustand boolean (from
useControlsVisible) into a reanimated shared value (useSharedValue) and sync it
in a useEffect when the selector changes, then reference that shared value
inside useAnimatedStyle so opacity/translateY use withTiming against the shared
value (keep CONTROLS_ANIMATION_DURATION as-is) to restore reactivity.
| const callerRowStyle = useAnimatedStyle(() => ({ | ||
| opacity: withTiming(controlsVisible ? 1 : 0, { duration: CONTROLS_ANIMATION_DURATION }), | ||
| transform: [{ translateY: withTiming(controlsVisible ? 0 : 10, { duration: CONTROLS_ANIMATION_DURATION }) }] | ||
| })); |
There was a problem hiding this comment.
Same animated style reactivity consideration as CallButtons.
The controlsVisible JavaScript value is used directly inside useAnimatedStyle. This may work in practice since the component re-renders when the store updates, but for optimal Reanimated performance, consider using shared values.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/views/CallView/components/CallerInfo.tsx` around lines 17 - 20, The
animated style uses the plain JS boolean controlsVisible inside useAnimatedStyle
which harms Reanimated performance; convert controlsVisible into a Reanimated
shared value and reference that inside callerRowStyle. Specifically, create a
useSharedValue (e.g., controlsVisibleShared), update it when the React
prop/store changes (via useEffect or a derived value) and then replace
references to controlsVisible in the useAnimatedStyle callback for
callerRowStyle with controlsVisibleShared.value while keeping
CONTROLS_ANIMATION_DURATION and the same withTiming logic.
- Call showControls() from stateChange, trackStateChange, and toggleFocus instead of inlining controlsVisible in set() patches - Mock media emitter forwards variadic args in useCallStore tests - Add test and JSDoc for controls visibility / animation consumers Made-with: Cursor
Proposed changes
This PR adds tap-to-hide (and tap-to-show) VoIP call controls on the in-call screen, with Reanimated opacity and translation transitions and a single source of truth in the call Zustand store.
User-facing behavior
Pressablearound avatar + name) toggles whether secondary controls are visible.pointerEventsis set tononeso taps do not hit invisible controls.controlsVisible, so the mini header bar remains usable.Store and lifecycle
controlsVisibleflag (defaulttrue) withtoggleControlsVisible,showControls, and selectoruseControlsVisiblefor components that animate or gate interaction.showControls()is the single path for forcing controls back on after important events:stateChangeandtrackStateChangeon the media call emitter, andtoggleFocuswhen switching between expanded and collapsed call UI. That keeps “force visible” in one place and avoids scatteringcontrolsVisiblepatches next to unrelatedset({ … })updates.reset()restores initial store state (includingcontrolsVisible) like other call UI fields.Shared animation timing
CONTROLS_ANIMATION_DURATION(300 ms) is defined once in CallView styles and reused by CallButtons, CallerInfo, and MediaCallHeader so timing stays consistent.Tests
stateChange/trackStateChange. The in-memory media-call mock’semitforwards variadic arguments to listeners (aligned with real emitters) with a test that locks that behavior in.caller-info-toggletestID, pointerEvents when controls are hidden vs visible (CallButtons and MediaCallHeader), and existing CallView flows.Architecture note (Zustand vs Reanimated shared values)
controlsVisiblestays in Zustand. Subscribers re-render when it changes;useAnimatedStylereads the boolean on those updates. This PR does not mirror that flag into a Reanimated shared value for these animations—intentionally, to avoid duplicating state and to keep the feature easy to follow in one store.Issue(s)
How to test or reproduce
yarn test. Pay particular attention toapp/lib/services/voip/useCallStore.test.ts, CallView-related tests,CallerInfo,CallButtons, andMediaCallHeadertests.Screenshots
Types of changes
Checklist
Further comments
Suggested reply if a reviewer asks to drive these animations only from Reanimated shared values synced via
useEffect: We keepcontrolsVisiblein Zustand; components that subscribe re-render when it changes, which updatesuseAnimatedStyle. We are not mirroring this flag into shared values for this feature.