Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
4702ee7
merge feat.voip-lib
diegolmello Jan 12, 2026
e532171
feat(voip): enhance call handling with UUID mapping and event listeners
diegolmello Jan 12, 2026
1299b0e
Base call UI
diegolmello Jan 12, 2026
d2cef3d
feat(voip): integrate Zustand for call state management and enhance C…
diegolmello Jan 13, 2026
b0b78cd
feat(voip): add simulateCall function for mock call handling in UI de…
diegolmello Jan 13, 2026
0d18314
refactor(CallView): update button handlers and improve UI responsiveness
diegolmello Jan 13, 2026
4b33a79
Add pause-shape-unfilled icon
diegolmello Jan 13, 2026
ece7e27
Base CallHeader
diegolmello Jan 14, 2026
c3dd2ae
toggleFocus
diegolmello Jan 14, 2026
1df1b29
collapse buttons
diegolmello Jan 14, 2026
8f9129e
Header components
diegolmello Jan 14, 2026
8a5de04
Hide header when no call
diegolmello Jan 14, 2026
a9ec70d
Timer
diegolmello Jan 14, 2026
d6229d9
Add use memo
diegolmello Jan 14, 2026
e718561
Add voice call item on sidebar
diegolmello Jan 14, 2026
26502cb
cleanup
diegolmello Jan 14, 2026
db29a47
Temp use @rocket.chat/media-signaling from .tgz
diegolmello Jan 14, 2026
2b16f4b
cleanup
diegolmello Jan 15, 2026
eae9137
Check module and permissions to enable voip
diegolmello Jan 15, 2026
bb2a8bb
Refactor stop method to use optional chaining for media signal listeners
diegolmello Jan 15, 2026
10593d6
voip push first test
diegolmello Jan 16, 2026
b6766f3
Add VoIP call handling with pending call management
diegolmello Jan 16, 2026
ac85af8
Remove pending store and create getInitialEvents on app/index
diegolmello Jan 20, 2026
9b28770
Attempt to make iOS calls work from cold state
diegolmello Jan 20, 2026
5c5e2be
lint and format
diegolmello Jan 20, 2026
01e42e2
Patch callkeep ios
diegolmello Jan 20, 2026
aa3ca88
Temp send iOS voip push token on gcm
diegolmello Jan 20, 2026
548e855
Temp fix require cycle
diegolmello Jan 20, 2026
abbb072
chore: format code and fix lint issues [skip ci]
diegolmello Jan 20, 2026
77cb36e
CallIDUUID module on android and voip push
diegolmello Jan 21, 2026
59f25eb
Add setCallUUID on useCallStore to persist calls accepted on native A…
diegolmello Jan 22, 2026
cd74d43
remove callkeep from notification
diegolmello Jan 23, 2026
9b71cf9
Android Incoming Call UI POC
diegolmello Jan 27, 2026
b1f81f4
Refactor VoIP handling: Migrate VoIP-related classes to a new package…
diegolmello Jan 28, 2026
089f91b
Remove VoipForegroundService
diegolmello Jan 28, 2026
6ac2f76
cleanup and use caller instead of callerName
diegolmello Jan 28, 2026
d81e67e
Cleanup and make iOS build again
diegolmello Jan 28, 2026
2b3e96a
Refactor VoIP handling: Remove unused event emissions for call answer…
diegolmello Jan 29, 2026
ad89658
Refactor VoIP handling: Introduce a new VoipPayload class to encapsul…
diegolmello Jan 29, 2026
c936ae9
Migrate react-native-voip-push-notifications to VoipModule
diegolmello Jan 30, 2026
3914562
Refactor VoIP module: Update package structure by moving VoipTurboPac…
diegolmello Feb 4, 2026
7986af4
Unify emitters
diegolmello Feb 10, 2026
7467381
Move CallKeep listeners from MediaSessionInstance to getInitialEvents
diegolmello Feb 10, 2026
e72c0f9
Clear callkeep on endcall
diegolmello Feb 10, 2026
0b87d35
Unify getInitialEvents logic
diegolmello Feb 10, 2026
2cbd7ec
getInitialEvents -> MediaCallEvents
diegolmello Feb 10, 2026
0d40944
chore: format code and fix lint issues [skip ci]
diegolmello Feb 10, 2026
18b0b0a
feat(Android): Add full screen incoming call (#6977)
diegolmello Feb 17, 2026
fc9aca6
feat: Update call UI (#6990)
diegolmello Feb 18, 2026
7f5efb1
feat: Handle audio routing, e.g., Bluetooth headset vs. internal spea…
diegolmello Feb 18, 2026
9abbdd5
fix: empty space when not on call (#6993)
diegolmello Feb 18, 2026
11809f1
feat: Dialpad (#7000)
diegolmello Feb 23, 2026
104471c
action: organized translations
diegolmello Feb 23, 2026
10a8b19
feat: start call (#7024)
diegolmello Mar 3, 2026
b1e1a80
chore: format code and fix lint issues
diegolmello Mar 3, 2026
6fcf804
feat: Pre flight (#7038)
diegolmello Mar 9, 2026
0715e89
action: organized translations
diegolmello Mar 9, 2026
8274083
feat: Receive voip push notifications from backend (#7045)
diegolmello Mar 13, 2026
306f8cc
feat: Refactor media session handling and improve disconnect logic (#…
diegolmello Mar 24, 2026
106cbd7
feat: Control incoming call from native (#7066)
diegolmello Mar 25, 2026
e3a7a78
feat: Voice message blocks (#7057)
diegolmello Mar 24, 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
1 change: 1 addition & 0 deletions .cursor/skills/agent-skills
Submodule agent-skills added at a4f602
2 changes: 1 addition & 1 deletion .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ jobs:
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add .
git commit -m "chore: format code and fix lint issues [skip ci]"
git commit -m "chore: format code and fix lint issues"
git push origin ${{ github.ref_name }}
10 changes: 10 additions & 0 deletions __mocks__/react-native-callkeep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default {
setup: jest.fn(),
canMakeMultipleCalls: jest.fn(),
displayIncomingCall: jest.fn(),
endCall: jest.fn(),
setCurrentCallActive: jest.fn(),
addEventListener: jest.fn((event, callback) => ({
remove: jest.fn()
}))
};
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ dependencies {

implementation "com.google.code.gson:gson:2.8.9"
implementation "com.tencent:mmkv-static:1.2.10"
implementation "com.github.bumptech.glide:glide:${rootProject.ext.glideVersion}"
implementation 'com.facebook.soloader:soloader:0.10.4'

// For SecureKeystore (EncryptedSharedPreferences)
Expand Down
41 changes: 41 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
<!-- permissions related to jitsi call -->
<uses-permission android:name="android.permission.BLUETOOTH" />

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />
Comment on lines +25 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add android:required="false" to avoid filtering devices.

Without required="false", Play Store will filter out devices that lack these hardware features. Most VoIP apps should work on devices without dedicated audio output or microphone hardware (e.g., tablets using Bluetooth).

Proposed fix
-    <uses-feature android:name="android.hardware.audio.output" />
-    <uses-feature android:name="android.hardware.microphone" />
+    <uses-feature android:name="android.hardware.audio.output" android:required="false" />
+    <uses-feature android:name="android.hardware.microphone" android:required="false" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />
<uses-feature android:name="android.hardware.audio.output" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
🤖 Prompt for AI Agents
In `@android/app/src/main/AndroidManifest.xml` around lines 25 - 26, Update the
two <uses-feature> entries for android.hardware.audio.output and
android.hardware.microphone so they don't cause Play Store device filtering:
modify the <uses-feature android:name="android.hardware.audio.output" /> and
<uses-feature android:name="android.hardware.microphone" /> elements to include
android:required="false" (i.e., set the required attribute to false for both
features) so devices without those hardware features are not excluded.


<!-- android 13 notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Expand Down Expand Up @@ -104,6 +116,35 @@
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${BugsnagAPIKey}" />

<activity
android:name="chat.rocket.reactnative.voip.IncomingCallActivity"
android:exported="false"
android:launchMode="singleInstance"
android:showOnLockScreen="true"
android:turnScreenOn="true"
android:showWhenLocked="true"
android:theme="@style/Theme.IncomingCall"
android:excludeFromRecents="true"
android:taskAffinity="chat.rocket.reactnative.voip" />

<receiver
android:name="chat.rocket.reactnative.voip.VoipNotification$DeclineReceiver"
android:enabled="true"
android:exported="false" />

<service android:name="io.wazo.callkeep.VoiceConnectionService"
android:label="Wazo"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true"
android:foregroundServiceType="microphone"
>
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>

<service android:name="io.wazo.callkeep.RNCallKeepBackgroundMessagingService" />
</application>

<queries>
Expand Down
Binary file modified android/app/src/main/assets/fonts/custom.ttf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
RNBootSplash.init(this, R.style.BootTheme)
super.onCreate(null)

// Handle notification intents
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
}

public override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)

// Handle notification intents when activity is already running
NotificationIntentHandler.handleIntent(this, intent)
}

override fun invokeDefaultOnBackPressed() {
moveTaskToBack(true)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import chat.rocket.reactnative.storage.MMKVKeyManager;
import chat.rocket.reactnative.storage.SecureStoragePackage;
import chat.rocket.reactnative.notification.VideoConfTurboPackage
import chat.rocket.reactnative.notification.PushNotificationTurboPackage
import chat.rocket.reactnative.VoipTurboPackage

/**
* Main Application class.
Expand All @@ -43,6 +44,7 @@ open class MainApplication : Application(), ReactApplication {
add(WatermelonDBJSIPackage())
add(VideoConfTurboPackage())
add(PushNotificationTurboPackage())
add(VoipTurboPackage())
add(SecureStoragePackage())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class Ejson {
private static final String TAG = "RocketChat.Ejson";
private static final String TOKEN_KEY = "reactnativemeteor_usertoken-";

String host;
public String host;
String rid;
String type;
Sender sender;
Expand Down Expand Up @@ -57,7 +57,7 @@ private MMKV getMMKV() {
* Helper method to build avatar URI from avatar path.
* Validates server URL and credentials, then constructs the full URI.
*/
private String buildAvatarUri(String avatarPath, String errorContext) {
private String buildAvatarUri(String avatarPath, String errorContext, int sizePx) {
String server = serverURL();
if (server == null || server.isEmpty()) {
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
Expand All @@ -67,7 +67,7 @@ private String buildAvatarUri(String avatarPath, String errorContext) {
String userToken = token();
String uid = userId();

String finalUri = server + avatarPath + "?format=png&size=100";
String finalUri = server + avatarPath + "?format=png&size=" + sizePx;
if (!userToken.isEmpty() && !uid.isEmpty()) {
finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid;
}
Expand Down Expand Up @@ -102,23 +102,45 @@ public String getAvatarUri() {
}
}

return buildAvatarUri(avatarPath, "");
return buildAvatarUri(avatarPath, "", 100);
}

/**
* Generates avatar URI for video conference caller.
* Factory for building caller avatar URIs from host + username (e.g. VoIP payload).
* Caller is package-private, so this is the only way to get avatar URI from outside the package.
*/
public static Ejson forCallerAvatar(String host, String username) {
if (host == null || host.isEmpty() || username == null || username.isEmpty()) {
return null;
}
Ejson ejson = new Ejson();
ejson.host = host;
ejson.caller = new Caller();
ejson.caller.username = username;
return ejson;
}

/**
* Generates avatar URI for video conference caller (default size 100).
* Returns null if caller username is not available (username is required for avatar endpoint).
*/
public String getCallerAvatarUri() {
// Check if caller exists and has username (required - /avatar/{userId} endpoint doesn't exist)
return getCallerAvatarUri(100);
}

/**
* Generates avatar URI for video conference caller with custom size.
* Returns null if caller username is not available.
*/
public String getCallerAvatarUri(int sizePx) {
if (caller == null || caller.username == null || caller.username.isEmpty()) {
Log.w(TAG, "Cannot generate caller avatar URI: caller or username is null");
return null;
}

try {
String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8");
return buildAvatarUri(avatarPath, "caller");
return buildAvatarUri(avatarPath, "caller", sizePx);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to encode caller username", e);
return null;
Expand Down Expand Up @@ -242,4 +264,4 @@ static class Content {
String kid;
String iv;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import com.google.gson.GsonBuilder
import chat.rocket.reactnative.voip.VoipNotification

/**
* Handles notification Intent processing from MainActivity.
Expand All @@ -17,11 +18,15 @@ class NotificationIntentHandler {

/**
* Handles a notification Intent from MainActivity.
* Processes both video conf and regular notification intents.
* Processes VoIP, video conf, and regular notification intents.
*/
@JvmStatic
fun handleIntent(context: Context, intent: Intent) {
// Handle video conf action first
if (VoipNotification.handleMainActivityVoipIntent(context, intent)) {
return
}

// Handle video conf action
if (handleVideoConfIntent(context, intent)) {
return
}
Expand All @@ -45,7 +50,7 @@ class NotificationIntentHandler {

val rid = intent.getStringExtra("rid") ?: ""
val callerId = intent.getStringExtra("callerId") ?: ""
val callerName = intent.getStringExtra("callerName") ?: ""
val caller = intent.getStringExtra("caller") ?: ""
val host = intent.getStringExtra("host") ?: ""
val callId = intent.getStringExtra("callId") ?: ""

Expand All @@ -63,7 +68,7 @@ class NotificationIntentHandler {
"callId" to callId,
"caller" to mapOf(
"_id" to callerId,
"name" to callerName
"name" to caller
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import android.os.Bundle
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import chat.rocket.reactnative.voip.VoipNotification
import chat.rocket.reactnative.voip.VoipPayload

/**
* Custom Firebase Messaging Service for Rocket.Chat.
*
* Handles incoming FCM messages and routes them to CustomPushNotification
* for advanced processing (E2E decryption, MessagingStyle, direct reply, etc.)
* Handles incoming FCM messages and routes them to the appropriate handler:
* - VoipNotification for VoIP calls (notificationType: "voip")
* - CustomPushNotification for regular messages and video conferences
*/
class RCFirebaseMessagingService : FirebaseMessagingService() {

Expand All @@ -18,23 +21,29 @@ class RCFirebaseMessagingService : FirebaseMessagingService() {
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "FCM message received from: ${remoteMessage.from}")
// TODO: remove data
Log.d(TAG, "FCM message received from: ${remoteMessage.from} data: ${remoteMessage.data}")

val data = remoteMessage.data
if (data.isEmpty()) {
Log.w(TAG, "FCM message has no data payload, ignoring")
return
}

// Convert FCM data to Bundle for processing
val bundle = Bundle().apply {
data.forEach { (key, value) ->
putString(key, value)
}
val voipPayload = VoipPayload.fromMap(data)
if (voipPayload != null) {
Log.d(TAG, "Detected VoIP payload of type ${voipPayload.type}, routing to VoipNotification handler")
VoipNotification(this).onMessageReceived(voipPayload)
return
}

// Process the notification
// Process regular notifications via CustomPushNotification
try {
val bundle = Bundle().apply {
data.forEach { (key, value) ->
putString(key, value)
}
}
val notification = CustomPushNotification(this, bundle)
notification.onReceived()
} catch (e: Exception) {
Expand Down
Loading
Loading