Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5dae4b5
first attempt
diegolmello Mar 27, 2026
d324a86
fix(android): guard hasActiveCall Telecom check for READ_PHONE_STATE
diegolmello Mar 30, 2026
266922e
feat(android): add READ_PHONE_STATE runtime request helper for VoIP
diegolmello Mar 30, 2026
bd7158c
feat(voip): request READ_PHONE_STATE when starting a VoIP call
diegolmello Mar 30, 2026
309cbba
fix(android): treat ringing/dialing CallKeep connections as busy for …
diegolmello Mar 30, 2026
328b646
fix(ios): skip initial-events stash for busy VoIP push
diegolmello Mar 30, 2026
8fc00f5
feat(voip): implement videoconference blocking logic and related tests
diegolmello Mar 30, 2026
f30a57a
feat(ios): CallKit call-waiting — multi-call observer, disable hold
diegolmello Mar 30, 2026
12c6cac
feat: Enhance MediaSessionInstance tests for new call handling
diegolmello Mar 30, 2026
5ae590d
test(voip): add MediaCallEvents tests for cross-server accept pipeline
diegolmello Mar 30, 2026
beb9a7c
refactor(voip): keep MediaCallEvents native handlers private
diegolmello Mar 30, 2026
385991d
chore(voip): dedupe MediaCallEvents tests, clarify nativeAccept docs
diegolmello Mar 30, 2026
4a0e2f7
chore(voip): iOS CallKit call-waiting + drop JS callee busy-reject af…
diegolmello Mar 30, 2026
36ed84f
chore(voip): document iOS busy-call helpers; tighten videoconf guard …
diegolmello Mar 30, 2026
9e71e6c
i18n: add phone state permission strings for supported locales
diegolmello Mar 30, 2026
c7e4f74
fix: resolve ESLint errors in VoIP-related test files
diegolmello Mar 30, 2026
72ab82e
fix voipservice
diegolmello Mar 31, 2026
408cced
feat(voip): auto-hold RC call on OS hold (CallKeep didToggleHoldCallA…
diegolmello Apr 1, 2026
5828949
Fix for phone calls on iOS
diegolmello Apr 1, 2026
ab85a8f
support holding
diegolmello Apr 1, 2026
d7bffcd
fix(voip): localize phone-state OK button; gate FCM before busy check
diegolmello Apr 1, 2026
6bebf15
feat: per-call DDP registry for call waiting
diegolmello Apr 1, 2026
db0bb36
chore: format code and fix lint issues
diegolmello Apr 1, 2026
3bc1473
fix(voip): detect cellular calls in no-permission audio mode fallback
diegolmello Apr 1, 2026
126715d
fix(voip): guard toggleHold against already-resumed state on OS unhold
diegolmello Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ dependencies {

// For SecureKeystore (EncryptedSharedPreferences)
implementation 'androidx.security:security-crypto:1.1.0'

testImplementation 'junit:junit:4.13.2'
}

apply plugin: 'com.google.gms.google-services'
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package chat.rocket.reactnative.voip

/**
* Pure routing for an incoming VoIP FCM push after [VoipPayload.isVoipIncomingCall] is true.
* Stale (invalid or expired lifetime) pushes must not reach busy vs show branching.
*/
internal enum class VoipIncomingPushAction {
STALE,
REJECT_BUSY,
SHOW_INCOMING
}

internal fun decideIncomingVoipPushAction(
isValidForIncomingHandling: Boolean,
hasActiveCall: Boolean
): VoipIncomingPushAction {
if (!isValidForIncomingHandling) {
return VoipIncomingPushAction.STALE
}
return if (hasActiveCall) {
VoipIncomingPushAction.REJECT_BUSY
} else {
VoipIncomingPushAction.SHOW_INCOMING
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package chat.rocket.reactnative.voip

/**
* Per-call DDP client slots: each [callId] maps to at most one [DDPClient] in production.
* Isolates teardown so a busy-call reject (call B) does not disconnect call A's listener.
*/
internal class VoipPerCallDdpRegistry<T : Any>(
private val releaseClient: (T) -> Unit
) {
private val lock = Any()
private val clients = mutableMapOf<String, T>()
private val loggedInCallIds = mutableSetOf<String>()

fun clientFor(callId: String): T? = synchronized(lock) { clients[callId] }

fun isLoggedIn(callId: String): Boolean = synchronized(lock) { loggedInCallIds.contains(callId) }

fun putClient(callId: String, client: T) {
synchronized(lock) {
clients.remove(callId)?.let(releaseClient)
clients[callId] = client
loggedInCallIds.remove(callId)
}
}

fun markLoggedIn(callId: String) {
synchronized(lock) {
loggedInCallIds.add(callId)
}
}

fun stopClient(callId: String) {
synchronized(lock) {
loggedInCallIds.remove(callId)
clients.remove(callId)?.let(releaseClient)
}
}

fun stopAllClients() {
synchronized(lock) {
loggedInCallIds.clear()
clients.values.forEach(releaseClient)
clients.clear()
}
}

fun clientCount(): Int = synchronized(lock) { clients.size }

fun clientIds(): Set<String> = synchronized(lock) { clients.keys.toSet() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package chat.rocket.reactnative.voip

import org.junit.Assert.assertEquals
import org.junit.Test

class VoipIncomingCallDispatchTest {

@Test
fun `stale push with active call does not route to reject busy`() {
assertEquals(
VoipIncomingPushAction.STALE,
decideIncomingVoipPushAction(isValidForIncomingHandling = false, hasActiveCall = true)
)
}

@Test
fun `stale push without active call does not route to show incoming`() {
assertEquals(
VoipIncomingPushAction.STALE,
decideIncomingVoipPushAction(isValidForIncomingHandling = false, hasActiveCall = false)
)
}

@Test
fun `valid push with active call rejects busy`() {
assertEquals(
VoipIncomingPushAction.REJECT_BUSY,
decideIncomingVoipPushAction(isValidForIncomingHandling = true, hasActiveCall = true)
)
}

@Test
fun `valid push without active call shows incoming`() {
assertEquals(
VoipIncomingPushAction.SHOW_INCOMING,
decideIncomingVoipPushAction(isValidForIncomingHandling = true, hasActiveCall = false)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package chat.rocket.reactnative.voip

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

class VoipPerCallDdpRegistryTest {

private fun registry(): Pair<MutableList<String>, VoipPerCallDdpRegistry<String>> {
val released = mutableListOf<String>()
return released to VoipPerCallDdpRegistry { released.add(it) }
}

@Test
fun `stopClient removes only the named callId`() {
val (released, reg) = registry()
reg.putClient("callA", "clientA")
reg.putClient("callB", "clientB")
reg.stopClient("callA")
assertEquals(listOf("clientA"), released)
assertEquals(setOf("callB"), reg.clientIds())
assertEquals("clientB", reg.clientFor("callB"))
assertNull(reg.clientFor("callA"))
}

@Test
fun `stopAllClients disconnects every entry`() {
val (released, reg) = registry()
reg.putClient("callA", "clientA")
reg.putClient("callB", "clientB")
reg.stopAllClients()
assertEquals(2, released.size)
assertTrue(released.containsAll(listOf("clientA", "clientB")))
assertEquals(0, reg.clientCount())
assertTrue(reg.clientIds().isEmpty())
}

@Test
fun `starting a second listener for another callId does not release the first`() {
val (released, reg) = registry()
reg.putClient("callA", "clientA")
reg.putClient("callB", "clientB")
assertTrue(released.isEmpty())
assertEquals(setOf("callA", "callB"), reg.clientIds())
}

@Test
fun `putClient for the same callId releases the previous client`() {
val (released, reg) = registry()
reg.putClient("callA", "first")
reg.putClient("callA", "second")
assertEquals(listOf("first"), released)
assertEquals("second", reg.clientFor("callA"))
}

@Test
fun `loggedIn state is per callId`() {
val (_, reg) = registry()
reg.putClient("callA", "a")
reg.putClient("callB", "b")
assertFalse(reg.isLoggedIn("callA"))
reg.markLoggedIn("callA")
assertTrue(reg.isLoggedIn("callA"))
assertFalse(reg.isLoggedIn("callB"))
}

@Test
fun `stopClient clears loggedIn for that callId`() {
val (_, reg) = registry()
reg.putClient("callA", "a")
reg.markLoggedIn("callA")
reg.stopClient("callA")
assertFalse(reg.isLoggedIn("callA"))
}
}
2 changes: 2 additions & 0 deletions app/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@
"Pause": "توقف مؤقت",
"Permalink_copied_to_clipboard": "تم نسخ الرابط الثابت إلى الحافظة!",
"Phone": "الهاتف",
"Phone_state_permission_message": "يتيح ذلك لـ Rocket.Chat معرفة ما إذا كنت في مكالمة هاتفية أو VoIP حاليًا كي تُعالج المكالمات الواردة بشكل صحيح.",
"Phone_state_permission_title": "السماح بالوصول إلى حالة الهاتف",
"Pin": "ثبت",
"Pinned": "مثبت",
"Play": "لعب",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/bn-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@
"Permalink_copied_to_clipboard": "পারমালিঙ্ক ক্লিপবোর্ডে কপি করা হয়েছে!",
"Person_or_channel": "ব্যক্তি বা চ্যানেল",
"Phone": "ফোন",
"Phone_state_permission_message": "এটি Rocket.Chat-কে আপনি ইতিমধ্যে কোনও ফোন বা VoIP কলে আছেন কিনা তা শনাক্ত করতে দেয়, যাতে ইনকামিং কলগুলি সঠিকভাবে পরিচালনা করা যায়।",
"Phone_state_permission_title": "ফোনের অবস্থা অ্যাক্সেসের অনুমতি দিন",
"Pin": "পিন",
"Pinned": "পিনকরা",
"Pinned_a_message": "একটি বার্তা পিন করা হয়েছে:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@
"Permalink_copied_to_clipboard": "Trvalý odkaz zkopírován do schránky!",
"Person_or_channel": "Osoba nebo kanál",
"Phone": "Telefon",
"Phone_state_permission_message": "Rocket.Chat tak může zjistit, zda již probíhá telefonní nebo hlasový (VoIP) hovor, a správně zpracovat příchozí hovory.",
"Phone_state_permission_title": "Povolit přístup ke stavu telefonu",
"Pin": "Kolík",
"Pinned": "Připnuto",
"Pinned_a_message": "Připnuta zpráva:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,8 @@
"Pause": "Pause",
"Permalink_copied_to_clipboard": "Permalink in die Zwischenablage kopiert!",
"Phone": "Telefon",
"Phone_state_permission_message": "Damit kann Rocket.Chat erkennen, ob Sie bereits in einem Telefon- oder VoIP-Gespräch sind, damit eingehende Anrufe korrekt behandelt werden können.",
"Phone_state_permission_title": "Zugriff auf den Telefonstatus erlauben",
"Pin": "Anheften",
"Pinned": "Angeheftet",
"Place_chat_on_hold": "Chat in die Warteschleife stellen",
Expand Down
3 changes: 3 additions & 0 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@
"Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code.",
"OR": "OR",
"OS": "OS",
"Ok": "Ok",
"Overwrites_the_server_configuration_and_use_room_config": "Overwrites the workspace configuration and use room config",
"Owner": "Owner",
"Parent_channel_or_group": "Parent channel or group",
Expand All @@ -662,6 +663,8 @@
"Permalink_copied_to_clipboard": "Permalink copied to clipboard!",
"Person_or_channel": "Person or channel",
"Phone": "Phone",
"Phone_state_permission_message": "This lets Rocket.Chat detect when you are already on a phone or VoIP call so incoming calls can be handled correctly.",
"Phone_state_permission_title": "Allow phone state access",
"Pin": "Pin",
"Pinned": "Pinned",
"Pinned_a_message": "Pinned a message:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@
"Passwords_do_not_match": "Las contraseñas no coinciden.",
"Pause": "Pausa",
"Permalink_copied_to_clipboard": "¡Enlace permanente copiado al portapapeles!",
"Phone_state_permission_message": "Permite que Rocket.Chat detecte si ya está en una llamada telefónica o VoIP para gestionar correctamente las llamadas entrantes.",
"Phone_state_permission_title": "Permitir acceso al estado del teléfono",
"Pin": "Fijar",
"Pinned": "Fijado",
"Play": "Jugar",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,8 @@
"Pause": "Tauko",
"Permalink_copied_to_clipboard": "Pysyvä linkki kopioitu leikepöydälle!",
"Phone": "Puhelin",
"Phone_state_permission_message": "Tämän avulla Rocket.Chat voi havaita, oletko jo puhelimessa tai VoIP-puhelussa, jotta saapuvat puhelut voidaan käsitellä oikein.",
"Phone_state_permission_title": "Salli puhelimen tilan käyttö",
"Pin": "Kiinnitä",
"Pinned": "Kiinnitetty",
"Place_chat_on_hold": "Aseta keskustelu pitoon",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@
"Pause": "Pause",
"Permalink_copied_to_clipboard": "Lien permanent copié dans le presse-papiers !",
"Phone": "Téléphone",
"Phone_state_permission_message": "Cela permet à Rocket.Chat de détecter si vous êtes déjà en appel téléphonique ou VoIP afin de traiter correctement les appels entrants.",
"Phone_state_permission_title": "Autoriser l’accès à l’état du téléphone",
"Pin": "Épingler",
"Pinned": "Épinglé",
"Place_chat_on_hold": "Mettre le chat en attente",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/hi-IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@
"Permalink_copied_to_clipboard": "पर्मालिंक क्लिपबोर्ड पर कॉपी किया गया!",
"Person_or_channel": "व्यक्ति या चैनल",
"Phone": "फ़ोन",
"Phone_state_permission_message": "इससे Rocket.Chat यह पता लगा सकता है कि क्या आप पहले से किसी फ़ोन या VoIP कॉल पर हैं, ताकि आने वाली कॉल को सही ढंग से संभाला जा सके।",
"Phone_state_permission_title": "फ़ोन स्थिति तक पहुँच की अनुमति दें",
"Pin": "पिन",
"Pinned": "पिन किया गया",
"Pinned_a_message": "एक संदेश को पिन किया गया है:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@
"Permalink_copied_to_clipboard": "Permalink a vágólapra másolva!",
"Person_or_channel": "Személy vagy csatorna",
"Phone": "Telefon",
"Phone_state_permission_message": "Így a Rocket.Chat észlelheti, ha már telefonos vagy VoIP-hívásban van, hogy a bejövő hívások helyesen kezelhetők legyenek.",
"Phone_state_permission_title": "Telefonállapot elérésének engedélyezése",
"Pin": "Pin",
"Pinned": "Kitűzve",
"Pinned_a_message": "Kitűzött üzenet:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@
"Pause": "Pausa",
"Permalink_copied_to_clipboard": "Permalink copiato negli appunti!",
"Phone": "Telefono",
"Phone_state_permission_message": "Consente a Rocket.Chat di rilevare se sei già in una chiamata telefonica o VoIP, così da gestire correttamente le chiamate in arrivo.",
"Phone_state_permission_title": "Consenti l’accesso allo stato del telefono",
"Pin": "Appunta",
"Pinned": "Appuntati",
"Play": "Giocare",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@
"Passwords_do_not_match": "パスワードが一致しません。",
"Pause": "一時停止",
"Permalink_copied_to_clipboard": "リンクをクリップボードにコピーしました!",
"Phone_state_permission_message": "電話またはVoIP通話中かどうかをRocket.Chatが検知し、着信を適切に処理できるようにします。",
"Phone_state_permission_title": "電話状態へのアクセスを許可",
"Pin": "ピン留め",
"Pinned": "ピン留めされました",
"Play": "遊ぶ",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@
"Pause": "Pauze",
"Permalink_copied_to_clipboard": "Permalink gekopiëerd naar klembord!",
"Phone": "Telefoon",
"Phone_state_permission_message": "Hiermee kan Rocket.Chat detecteren of u al in een telefoon- of VoIP-gesprek bent, zodat inkomende oproepen correct worden afgehandeld.",
"Phone_state_permission_title": "Toegang tot telefoonstatus toestaan",
"Pin": "Vastzetten",
"Pinned": "Vastgezet",
"Place_chat_on_hold": "Chat in de wacht zetten",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/nn.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@
"Passwords_do_not_match": "Passorda stemmer ikkje overeins",
"Pause": "Pause",
"Phone": "Telefon",
"Phone_state_permission_message": "Dette lèt Rocket.Chat oppdage om du allereie er i ein telefon- eller VoIP-samtale, slik at innkomande samtalar kan handsamast korrekt.",
"Phone_state_permission_title": "Tillat tilgang til telefonstatus",
"Pin": "Pin",
"Pinned_a_message": "Pinned en melding:",
"Please_add_a_comment": "Vennligst legg til en kommentar",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/no.json
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,8 @@
"Permalink_copied_to_clipboard": "Permalenke kopiert til utklippstavlen!",
"Person_or_channel": "Person eller kanal",
"Phone": "Telefon",
"Phone_state_permission_message": "Dette lar Rocket.Chat oppdage om du allerede er i en telefon- eller VoIP-samtale, slik at innkommende anrop kan håndteres riktig.",
"Phone_state_permission_title": "Tillat tilgang til telefonstatus",
"Pin": "Pin",
"Pinned": "Festet",
"Pinned_a_message": "Festet en melding:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,8 @@
"Permalink_copied_to_clipboard": "Link-permanente copiado para a área de transferência!",
"Person_or_channel": "Pessoa ou canal",
"Phone": "Telefone",
"Phone_state_permission_message": "Isso permite que o Rocket.Chat detecte quando você já está em uma chamada telefônica ou VoIP para que as chamadas recebidas sejam tratadas corretamente.",
"Phone_state_permission_title": "Permitir acesso ao estado do telefone",
"Pin": "Fixar",
"Pinned": "Mensagens Fixadas",
"Pinned_a_message": "Fixou uma mensagem:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/pt-PT.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@
"Pause": "Pausa",
"Permalink_copied_to_clipboard": "Link permanente copiado para a área de transferência!",
"Phone": "Telefone",
"Phone_state_permission_message": "Isto permite ao Rocket.Chat detetar se já está numa chamada telefónica ou VoIP, para que as chamadas recebidas sejam tratadas corretamente.",
"Phone_state_permission_title": "Permitir acesso ao estado do telefone",
"Pin": "Afixar",
"Pinned": "Afixada",
"Pinned_a_message": "Fixou uma mensagem:",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@
"Pause": "Пауза",
"Permalink_copied_to_clipboard": "Постоянная ссылка скопирована в буфер обмена!",
"Phone": "Телефон",
"Phone_state_permission_message": "Это позволяет Rocket.Chat определять, идёт ли у вас уже телефонный или VoIP-звонок, чтобы правильно обрабатывать входящие вызовы.",
"Phone_state_permission_title": "Разрешить доступ к состоянию телефона",
"Pin": "Прикрепить сообщение",
"Pinned": "Прикреплено",
"Place_chat_on_hold": "Поставить чат на удержание",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/sl-SI.json
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@
"Pause": "Premor",
"Permalink_copied_to_clipboard": "Permalink, kopirano v odložišče!",
"Phone": "Telefon",
"Phone_state_permission_message": "To omogoča Rocket.Chatu, da zazna, ali ste že v telefonskem ali VoIP klicu, da se dohodni klici pravilno obdelajo.",
"Phone_state_permission_title": "Dovoli dostop do stanja telefona",
"Pin": "Zatič",
"Pinned": "Pripet",
"Place_chat_on_hold": "Postavite klepet na \"pridržan\"",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@
"Pause": "Paus",
"Permalink_copied_to_clipboard": "Permalänken har kopierats till urklipp.",
"Phone": "Telefonnummer",
"Phone_state_permission_message": "Detta låter Rocket.Chat upptäcka om du redan är i ett telefon- eller VoIP-samtal så att inkommande samtal kan hanteras korrekt.",
"Phone_state_permission_title": "Tillåt åtkomst till telefonstatus",
"Pin": "Fäst",
"Pinned": "Fästa",
"Place_chat_on_hold": "Parkera chatten",
Expand Down
Loading
Loading