fix(voip): suppress incoming voice calls locally when microphone denied#7354
fix(voip): suppress incoming voice calls locally when microphone denied#7354diegolmello wants to merge 18 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughIntroduce a cross-platform VoIP microphone permission result ({granted, canAskAgain, prompted}), add check-only and pre-acquire flows, denial alert UI, native push-layer rejection when mic is denied, MediaSessionInstance idempotent answer gating, and update tests, docs, saga wiring, and i18n. ChangesVoIP Permissions & Push-gate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 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. Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
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: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/lib/methods/voipCallPermissions.ts`:
- Around line 13-37: The requestVoipCallPermissions function currently calls
PermissionsAndroid.request, Audio.getPermissionsAsync, and
Audio.requestPermissionsAsync without handling rejections; wrap the entire body
of requestVoipCallPermissions in a try/catch, catch any thrown errors from
PermissionsAndroid, Audio, or permission calls, log the error (e.g. via
console.error or the app logger) including which API failed, and return a safe
denied result { granted: false, canAskAgain: false } on failure so callers get a
controlled denial UX; ensure references to PermissionsAndroid, Audio, and
PermissionStatus remain intact.
🪄 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: bcdcc091-13c0-4ccd-8875-cbb71589521d
📒 Files selected for processing (5)
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.test.tsapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbersUse TypeScript with strict mode and baseUrl set to app/ for import resolution
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Use Prettier with tabs, single quotes, 130 char width, no trailing commas, arrow parens avoid, bracket same line
Use@rocket.chat/eslint-configbase with React, React Native, TypeScript, Jest plugins
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.ts
app/containers/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Reusable UI components should be placed in app/containers/ directory
Files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
app/lib/services/voip/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
VoIP service implementation should use Zustand stores (not Redux) and include native CallKit (iOS) and Telecom (Android) integration in app/lib/services/voip/
Files:
app/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.
Applied to files:
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.ts
🔇 Additional comments (5)
app/lib/methods/voipCallPermissions.test.ts (1)
1-144: LGTM!app/lib/services/voip/MediaSessionInstance.ts (1)
36-37: LGTM!Also applies to: 176-181, 238-241
app/lib/services/voip/MediaSessionInstance.test.ts (1)
132-137: LGTM!Also applies to: 248-249, 700-709, 877-915
app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx (1)
142-142: LGTM!app/lib/methods/voipCallPermissions.ts (1)
3-3: ⚡ Quick winExpo-av
Audio.getPermissionsAsync()status can be compared toexpo-camera’sPermissionStatus
Audio.getPermissionsAsync()(expo-av) andPermissionStatusused byexpo-cameraboth come from Expo’s sharedexpo-modules-coreenum, so the comparison isn’t a brittle cross-package contract (app/lib/methods/voipCallPermissions.ts, lines 3 and 29). Optionally, importPermissionStatusfromexpo-modules-corefor clarity.> Likely an incorrect or invalid review comment.
|
Android Build Available Rocket.Chat 4.73.0.108985 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNS__32oABVGneMrdnlkqLu9q2iqsMyqMSuQ0KZ3r7zS-sBD-tSngWZdakKZ-QKi9t3Ydday7lCTV9-jH_S6 |
|
iOS Build Available Rocket.Chat 4.73.0.108986 |
diegolmello
left a comment
There was a problem hiding this comment.
Code review — incoming VoIP mic-permission gate
Solid fix on the happy path with good unit coverage. The main risks are on the error / background path of the incoming-answer flow (where the new await was inserted), plus some duplication with the existing audio-recording permission code in MicOrSendButton. Inline comments below, roughly highest-severity first.
Verified non-issues: PermissionStatus from expo-camera matches expo-av's status enum (both re-export from expo-modules-core; same pattern already ships in MicOrSendButton); Android result !== NEVER_ASK_AGAIN covers all return values; all i18n keys (Ok/Cancel/Settings/…) exist; no dead imports.
|
iOS Build Available Rocket.Chat 4.73.0.109006 |
|
Android Build Available Rocket.Chat 4.73.0.109005 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNR8XgVARz7XAWRwuK-OKKoa5YUPW95EyR1tcy2r1yNPkpXkXKJXSDjxVVcwH2h-VZajvcqCVesLuThozELV |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/sagas/login.js`:
- Around line 268-273: The mediaSessionInstance.init(userId) call is currently a
floating promise allowing failures to escape checkVoipPermission's try/catch and
letting preAcquireVoipMicPermission run even if init fails; change the flow to
await mediaSessionInstance.init(userId) (or explicitly return
mediaSessionInstance.init(userId).then(...)) and only call
preAcquireVoipMicPermission() after init resolves, and funnel any errors from
both operations into the single existing catch/log path (use the same log
function used by the surrounding try/catch) so failures are observed and
pre-acquire does not run on init failure.
🪄 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: ba3b64af-78bc-4d03-a206-b76ad230df1b
📒 Files selected for processing (17)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatch.ktandroid/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.ktandroid/app/src/test/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatchTest.ktapp/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsxapp/i18n/locales/en.jsonapp/lib/methods/voipCallPermissions.test.tsapp/lib/methods/voipCallPermissions.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/docs/ARCHITECTURE.mdapp/lib/services/voip/docs/FLOWS.mdapp/lib/services/voip/docs/PLATFORMS.mdapp/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.mdapp/lib/services/voip/docs/adr/0002-reject-incoming-at-push-layer-when-mic-denied.mdapp/sagas/login.jsios/Libraries/AppDelegate+Voip.swiftios/Libraries/VoipService.swift
✅ Files skipped from review due to trivial changes (3)
- app/lib/services/voip/docs/adr/0002-reject-incoming-at-push-layer-when-mic-denied.md
- app/i18n/locales/en.json
- app/lib/services/voip/docs/ARCHITECTURE.md
🚧 Files skipped from review as they are similar to previous changes (1)
- app/containers/NewMediaCall/VoipCallLifecycle.integration.test.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions
Files:
app/sagas/login.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
**/*.{js,jsx,ts,tsx,json}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Prettier formatting with tabs, single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses
Files:
app/sagas/login.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Enforce ESLint rules from
@rocket.chat/eslint-configwith React, React Native, TypeScript, and Jest plugins
Files:
app/sagas/login.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbersUse TypeScript with strict mode enabled
Files:
app/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
app/lib/services/voip/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Zustand stores (not Redux) for VoIP feature in 'app/lib/services/voip/' directory with native CallKit (iOS) and Telecom (Android) integration
Files:
app/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.ts
🧠 Learnings (2)
📚 Learning: 2026-05-07T13:19:52.152Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7304
File: app/sagas/deepLinking.js:237-243
Timestamp: 2026-05-07T13:19:52.152Z
Learning: In this codebase’s Redux-Saga usage, remember that `yield put(action)` dispatches through the Redux store synchronously, and any saga(s) that synchronously react via action listeners (and synchronous `put` chains) will run to completion before the calling saga resumes at its next `yield`. As a result, within a single saga there is no scheduler interleaving between a `yield select(...)` and a subsequent `yield take(...)` at the next `yield` point, so a check-then-take pattern like `const state = yield select(...); if (state !== TARGET) { yield take(a => a.type === TARGET); }` is safe from TOCTOU races under the synchronous `put`/take model described above.
Applied to files:
app/sagas/login.js
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.
Applied to files:
app/lib/services/voip/MediaSessionInstance.tsapp/lib/methods/voipCallPermissions.test.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/methods/voipCallPermissions.ts
🪛 LanguageTool
app/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.md
[grammar] ~5-~5: Ensure spelling is correct
Context: ... answer path check-only: it answers iff the permission is currently granted, ...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (12)
app/lib/methods/voipCallPermissions.ts (1)
20-54: LGTM!Also applies to: 63-74, 84-92, 94-107
app/lib/methods/voipCallPermissions.test.ts (1)
39-163: LGTM!Also applies to: 165-351
app/lib/services/voip/MediaSessionInstance.ts (1)
54-55: LGTM!Also applies to: 172-228, 256-260
app/lib/services/voip/MediaSessionInstance.test.ts (1)
132-139: LGTM!Also applies to: 250-252, 879-1055
app/lib/services/voip/docs/FLOWS.md (1)
9-10: LGTM!Also applies to: 40-40, 52-52, 113-114, 129-137, 142-142, 167-167
app/lib/services/voip/docs/PLATFORMS.md (1)
23-26: LGTM!Also applies to: 66-67
app/lib/services/voip/docs/adr/0001-pre-acquire-microphone-at-login.md (1)
1-12: LGTM!android/app/src/main/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatch.kt (1)
9-9: LGTM!Also applies to: 14-22, 25-33
android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt (1)
488-532: LGTM!Also applies to: 689-709
android/app/src/test/java/chat/rocket/reactnative/voip/VoipIncomingCallDispatchTest.kt (1)
40-74: LGTM!ios/Libraries/AppDelegate+Voip.swift (1)
83-94: LGTM!ios/Libraries/VoipService.swift (1)
1-1: LGTM!Also applies to: 170-180, 577-586
|
iOS Build Available Rocket.Chat 4.74.0.109042 |
|
Android Build Available Rocket.Chat 4.74.0.109074 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNQw3ll9lF5ec3DhV3EHYgVFYtM36QlFRpF1MNW82QmvasshoU1058VRbCU72y1rExi7XcLRt_bU2t2qbvZI |
|
iOS Build Available Rocket.Chat 4.74.0.109082 |
|
iOS Build Available Rocket.Chat 4.74.0.109086 |
Request mic access before accept() so denied permissions hang up immediately instead of stalling signaling until the 10s timeout. Adds settings redirect for permanent denial on both platforms. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- requestVoipCallPermissions never rejects: a thrown permission check is treated as denied + logged, so a locked/backgrounded answer ends promptly instead of hanging until the 10s signaling timeout - answerCall dedupes concurrent invocations per callId (in-flight Set, cleared in finally) to restore the idempotency its doc comment claims - simplify the iOS permission branch and collapse the denied-mic alert variants into a single Alert.alert call - cover the iOS live-prompt denial and permission-throw paths; tighten the denied-answer test to assert the reject() wire signal and native teardown
Request the OS microphone permission proactively at session init (login → checkVoipPermission → preAcquireVoipMicPermission), fire-and-forget and suppressed while a call is active; alert only on a fresh denial. The incoming answerCall path becomes check-only (hasVoipCallPermission): it never prompts — the device may be locked/backgrounded — and rejects the call silently when the mic is not currently granted. Outgoing startCall still requests in-context and alerts; a new voice-call-specific alert string keeps the shared recorder copy untouched. This fixes the locked/backgrounded incoming-call case where a permission dialog is impossible at answer time, so the permission is acquired earlier instead. Docs: add CONTEXT.md glossary + ADR 0001; update FLOWS/ARCHITECTURE for the pre-acquire step and the check-only answer gate.
When the OS microphone permission (RECORD_AUDIO) is denied, decline an incoming VoIP push silently — no Telecom registration, no notification, no ringing — via the existing REST reject mechanism, rather than ringing a call that can never carry audio. - decideIncomingVoipPushAction gains a REJECT_NO_PERMISSION outcome and a hasMicPermission input (default granted, so existing callers/tests keep their outcomes); precedence stale -> no-permission -> busy -> show. - onMessageReceived reads RECORD_AUDIO and feeds the decision; the no-permission outcome maps onto a shared silent-reject helper (rejectNoMicPermissionCall) with its own debug log line. - Docs: ADR-0002 (push-layer gate + iOS PushKit limitation), CONTEXT glossary (Incoming-push gate, Reject-without-ringing), ARCHITECTURE microphone section, and FLOWS §3. The JS answer-time gate is retained as a defence-in-depth backstop.
Mirror the Android push-layer gate on iOS. When the OS microphone permission is denied, the PushKit incoming-push handler declines the call without a sustained ring: it sends the REST reject and reports a placeholder call it ends immediately (a sub-second CallKit flash — PushKit mandates a report before the handler returns), then skips prepareIncomingCall so nothing is stashed for JS. - VoipService.hasMicrophonePermission() reads the grant via AVAudioApplication on iOS 17+ and AVAudioSession earlier; only .granted passes (denied/undetermined are not-granted), matching the Android/JS gate. - VoipService.rejectNoMicPermissionCall wraps the private REST reject as a public push-handler entry point (no CallKit side effects). - AppDelegate+Voip didReceiveIncomingPushWith gates the non-China branch; the granted path is unchanged. - Docs: PLATFORMS.md iOS + Android push-handler sections reference the gate and ADR-0002. A truly silent path is impossible on iOS (PushKit kills the app if the handler returns without reporting); the momentary flash is the floor.
checkVoipPermission fired mediaSessionInstance.init() as a floating promise, so an init rejection escaped the surrounding try/catch as an unhandled rejection. Give it its own .catch(log), kept independent of pre-acquire so a failed init never suppresses microphone pre-acquisition. reset() now clears answeringCallIds alongside pendingHangups so the transient-call-state wipe stays uniform.
A reject from one device ends the call for every device of the user, so a phone with a denied mic would silently prevent answering on an authorized device. The push-layer mic gate now signals nothing: Android ignores the push entirely (log-only, IGNORE_NO_PERMISSION); iOS reports a momentary CallKit placeholder and ends it (PushKit mandates a report before the handler returns). The call keeps ringing on the user's other devices. The JS answer-time backstop still ends the call when the mic is revoked in the push-to-answer window, since native has already sent the REST accept.
…remaining locales
…icit mic-permission arg
dd250b1 to
f16b18e
Compare
|
Android Build Available Rocket.Chat 4.74.0.109104 Internal App Sharing: https://play.google.com/apps/test/RQQ8k09hlnQ/ahAO29uNQiMGONlbPD9TDf31VhKCyU1S6pJWKvqvzsJTlFdJcPNqUDIueQ_5M76eenADO-Xe0wCVN3_6QBbYeYLGCu |
|
iOS Build Available Rocket.Chat 4.74.0.109105 |
Proposed changes
When the OS microphone permission is denied, an incoming voice call used to ring anyway — but answering it produced a dead call, because the app cannot open the microphone. On a locked or backgrounded device the app cannot even show a permission dialog at answer time, so the user was offered a call they could never take. The ring was pure noise.
This PR moves the decision to the native push layer: a call that can never carry audio on this device is suppressed locally the moment the push arrives — before any ringing UI — and nothing is signalled to the server, so the call keeps ringing on the user's other devices (desktop, another phone) and can still be answered there. As symmetrically as each platform allows:
VoipNotification.onMessageReceivedruns a pure routing decision (decideIncomingVoipPushAction, precedencestale → no-permission → busy → show). WhenRECORD_AUDIOis denied the push is ignored (log-only) and returns: no Telecom registration, no notification, no full-screen incoming screen, no REST call.An earlier iteration of this change sent a REST
rejectfrom this device. That was wrong: a reject ends the call for every device of the user, so a phone with a denied mic would silently prevent the user from answering on an authorized device. The gate now signals nothing.Two supporting layers are retained:
MediaSessionInstance.init, fire-and-forget, suppressed during an active call), so the common case stays granted and calls ring normally.The granted path is unchanged on both platforms; outgoing
startCallbehaviour is unchanged (in-context request + denial alert).Design and vocabulary are documented in-repo under
app/lib/services/voip/docs/(CONTEXT.md,ARCHITECTURE.md,FLOWS.md§3,PLATFORMS.md).Issue(s)
https://rocketchat.atlassian.net/browse/NATIVE-1139
How to test or reproduce
Screenshots
Types of changes
Checklist
Further comments
rejectfrom one device ends the call for the whole user, which would stop other authorized devices from answering. A local-capability problem (this device's mic) must not produce a user-wide signal — so the gate mirrors the existing stale/expired-push handling and signals nothing.VoipIncomingCallDispatchTest: denied → ignored, denied-while-busy precedence, granted, stale). The native push wiring (Kotlin/Swift) is bound to framework singletons with no Swift unit harness, so it is verified by build plus manual QA on locked/backgrounded devices. iOS manual QA (real incoming call, mic denied, locked + backgrounded) is pending.Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests