Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 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 }}
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import android.media.RingtoneManager
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.widget.ImageButton
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.FrameLayout
import android.util.Log
import androidx.core.content.ContextCompat
import android.view.ViewOutlineProvider
import com.bumptech.glide.Glide
import chat.rocket.reactnative.MainActivity
import chat.rocket.reactnative.R
import chat.rocket.reactnative.notification.Ejson
import android.graphics.Typeface

/**
* Full-screen Activity displayed when an incoming VoIP call arrives.
Expand Down Expand Up @@ -56,6 +61,7 @@ class IncomingCallActivity : Activity() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

setContentView(R.layout.activity_incoming_call)
applyInterFont()

val voipPayload = VoipPayload.fromBundle(intent.extras)
if (voipPayload == null || !voipPayload.isVoipIncomingCall()) {
Expand All @@ -72,15 +78,85 @@ class IncomingCallActivity : Activity() {
setupButtons(voipPayload)
}

private fun applyInterFont() {
val interRegular = try {
Typeface.createFromAsset(assets, "fonts/Inter-Regular.otf")
} catch (e: Exception) {
Log.e(TAG, "Failed to load Inter-Regular font", e)
return
}
val interBold = try {
Typeface.createFromAsset(assets, "fonts/Inter-Bold.otf")
} catch (e: Exception) {
Log.e(TAG, "Failed to load Inter-Bold font", e)
interRegular
}
listOf(
R.id.header_text,
R.id.host_name,
R.id.incoming_call_reject_label,
R.id.incoming_call_accept_label
).forEach { id ->
findViewById<TextView>(id)?.setTypeface(interRegular)
}
findViewById<TextView>(R.id.caller_name)?.setTypeface(interBold)
}

private fun updateUI(payload: VoipPayload) {
val callerView = findViewById<TextView>(R.id.caller_name)
callerView?.text = payload.caller

// Try to load avatar if available
// TODO: needs username to load avatar
val avatarView = findViewById<ImageView>(R.id.caller_avatar)
// Avatar loading would require additional data - can be enhanced later
// For now, just show a placeholder or default icon
findViewById<TextView>(R.id.caller_name)?.text = payload.caller.ifEmpty { getString(R.string.incoming_call_unknown_caller) }
findViewById<TextView>(R.id.host_name)?.text = payload.hostName.ifEmpty { getString(R.string.incoming_call_unknown_host) }

loadAvatar(payload)
}

private fun loadAvatar(payload: VoipPayload) {
if (payload.host.isBlank() || payload.username.isBlank()) return

val container = findViewById<FrameLayout>(R.id.avatar_container)
val imageView = findViewById<ImageView>(R.id.avatar)
val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480)
val avatarUrl = Ejson.forCallerAvatar(payload.host, payload.username)?.getCallerAvatarUri(sizePx)
?: return
val cornerRadiusPx = (8 * resources.displayMetrics.density).toFloat()

Glide.with(this)
.load(avatarUrl)
.into(object : com.bumptech.glide.request.target.CustomTarget<android.graphics.drawable.Drawable>(sizePx, sizePx) {
override fun onResourceReady(
resource: android.graphics.drawable.Drawable,
transition: com.bumptech.glide.request.transition.Transition<in android.graphics.drawable.Drawable>?
) {
container.visibility = View.VISIBLE
imageView.setImageDrawable(resource)
applyAvatarRoundCorners(imageView, cornerRadiusPx)
}

override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) {
container.visibility = View.GONE
}

override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {
container.visibility = View.GONE
}
})
}

/**
* Applies rounded corners via view-level clipping.
* Works for both PNG (BitmapDrawable) and SVG (vector/PictureDrawable) since
* Glide's RoundedCorners bitmap transform only applies to bitmaps.
*/
private fun applyAvatarRoundCorners(imageView: ImageView, cornerRadiusPx: Float) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return
imageView.post {
val radius = cornerRadiusPx
imageView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: android.graphics.Outline) {
outline.setRoundRect(0, 0, view.width, view.height, radius)
}
}
imageView.clipToOutline = true
}
}

private fun startRingtone() {
Expand All @@ -105,14 +181,11 @@ class IncomingCallActivity : Activity() {
}

private fun setupButtons(payload: VoipPayload) {
val acceptButton = findViewById<ImageButton>(R.id.btn_accept)
val declineButton = findViewById<ImageButton>(R.id.btn_decline)

acceptButton?.setOnClickListener {
findViewById<LinearLayout>(R.id.btn_accept)?.setOnClickListener {
handleAccept(payload)
}

declineButton?.setOnClickListener {
findViewById<LinearLayout>(R.id.btn_decline)?.setOnClickListener {
handleDecline(payload)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import com.google.gson.annotations.SerializedName
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import chat.rocket.reactnative.utils.CallIdUUID
import android.util.Log

data class VoipPayload(
@SerializedName("callId")
val callId: String,

@SerializedName("caller")
val caller: String,

@SerializedName("username")
val username: String,

@SerializedName("host")
val host: String,

@SerializedName("type")
val type: String
val type: String,

@SerializedName("hostName")
val hostName: String,
) {
val notificationId: Int = callId.hashCode()
val callUUID: String = CallIdUUID.generateUUIDv5(callId)
Expand All @@ -31,8 +36,10 @@ data class VoipPayload(
return Bundle().apply {
putString("callId", callId)
putString("caller", caller)
putString("username", username)
putString("host", host)
putString("type", type)
putString("hostName", hostName)
putString("callUUID", callUUID)
putInt("notificationId", notificationId)
// Useful flag for MainActivity to know it's handling a VoIP action
Expand All @@ -44,33 +51,38 @@ data class VoipPayload(
return Arguments.createMap().apply {
putString("callId", callId)
putString("caller", caller)
putString("username", username)
putString("host", host)
putString("type", type)
putString("hostName", hostName)
putString("callUUID", callUUID)
putInt("notificationId", notificationId)
}
}

companion object {
fun fromMap(data: Map<String, String>): VoipPayload? {
Log.d("RocketChat.VoipPayload", "Parsing VoIP payload from map: $data")
val type = data["type"] ?: return null
val callId = data["callId"] ?: return null
val caller = data["caller"] ?: return null
val username = data["username"] ?: return null
val host = data["host"] ?: return null
val hostName = data["hostName"] ?: return null
if (type != "incoming_call") return null

return VoipPayload(callId, caller, host, type)
return VoipPayload(callId, caller, username, host, type, hostName)
}

fun fromBundle(bundle: Bundle?): VoipPayload? {
if (bundle == null) return null
val callId = bundle.getString("callId") ?: return null
val caller = bundle.getString("caller") ?: ""
val host = bundle.getString("host") ?: ""
val type = bundle.getString("type") ?: ""
val caller = bundle.getString("caller") ?: return null
val username = bundle.getString("username") ?: return null
val host = bundle.getString("host") ?: return null
val type = bundle.getString("type") ?: return null
val hostName = bundle.getString("hostName") ?: return null

return VoipPayload(callId, caller, host, type)
return VoipPayload(callId, caller, username, host, type, hostName)
}
}
}
6 changes: 6 additions & 0 deletions android/app/src/main/res/drawable/bg_avatar_incoming_call.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/incoming_call_avatar_bg"/>
<corners android:radius="8dp"/>
</shape>
6 changes: 6 additions & 0 deletions android/app/src/main/res/drawable/bg_btn_accept.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/incoming_call_accept_bg"/>
<corners android:radius="8dp"/>
</shape>
6 changes: 6 additions & 0 deletions android/app/src/main/res/drawable/bg_btn_reject.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/incoming_call_reject_bg"/>
<corners android:radius="8dp"/>
</shape>
9 changes: 9 additions & 0 deletions android/app/src/main/res/drawable/ic_call.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M11.157,6.042C11.936,5.94 12.696,6.304 13.109,6.961L13.186,7.096L15.026,10.606C15.124,10.793 15.192,10.994 15.227,11.202C15.259,11.393 15.263,11.58 15.246,11.751L15.244,11.762C15.2,12.173 15.038,12.535 14.821,12.821L14.693,12.992C14.649,13.053 14.609,13.113 14.574,13.171L14.477,13.342C14.465,13.368 14.455,13.392 14.446,13.413L14.526,13.556C14.587,13.655 14.664,13.764 14.752,13.884L15.049,14.266C15.484,14.802 16.006,15.359 16.348,15.702C16.605,15.959 16.982,16.316 17.381,16.662L17.782,17C18.053,17.219 18.296,17.401 18.492,17.523C18.547,17.558 18.596,17.583 18.636,17.604L18.707,17.572C18.81,17.522 18.931,17.448 19.057,17.357L19.227,17.227L19.228,17.229C19.511,17.014 19.875,16.848 20.293,16.804L20.302,16.803C20.484,16.785 20.631,16.796 20.706,16.803C20.749,16.808 20.787,16.812 20.818,16.817C20.833,16.819 20.848,16.823 20.861,16.825C20.868,16.826 20.875,16.828 20.882,16.829C20.885,16.829 20.887,16.83 20.89,16.831C20.892,16.831 20.894,16.831 20.895,16.832H20.898C21.088,16.869 21.273,16.933 21.444,17.023L24.953,18.863C25.695,19.252 26.116,20.062 26.007,20.892C25.877,21.894 25.464,23.215 24.68,24.263L24.518,24.469C24.049,25.037 23.415,25.559 22.601,25.814L22.444,25.86C21.659,26.067 20.831,25.993 20.004,25.65L19.699,25.518C18.175,24.834 16.567,23.738 15.234,22.714L14.71,22.302C13.514,21.349 12.538,20.462 12.062,19.986L11.563,19.463C10.978,18.83 10.162,17.89 9.336,16.815L8.923,16.267C7.957,14.961 6.985,13.457 6.4,12.046L6.277,11.715C6.042,10.997 6.01,10.285 6.189,9.605L6.234,9.448L6.342,9.15C6.581,8.573 6.955,8.097 7.37,7.715L7.581,7.531C8.657,6.642 10.089,6.181 11.157,6.042ZM11.415,8.025C10.657,8.124 9.693,8.443 8.991,8.966L8.854,9.072C8.494,9.37 8.251,9.7 8.142,10.047C8.051,10.337 8.04,10.679 8.178,11.095L8.248,11.279C8.744,12.476 9.606,13.826 10.525,15.07L10.922,15.595C11.976,16.967 13,18.096 13.477,18.573C13.954,19.05 15.083,20.073 16.454,21.128L16.979,21.523C18.222,22.442 19.573,23.305 20.769,23.802C21.271,24.01 21.671,24.011 22.003,23.907C22.35,23.798 22.679,23.555 22.976,23.195C23.489,22.575 23.826,21.698 23.973,20.948L24.024,20.635L20.514,18.795L20.499,18.794C20.49,18.795 20.479,18.798 20.468,18.802L20.433,18.824L20.245,18.965C20.097,19.073 19.933,19.181 19.76,19.277L19.585,19.368C19.287,19.513 18.878,19.662 18.437,19.621L18.298,19.6C17.974,19.535 17.661,19.36 17.433,19.219C17.285,19.126 17.132,19.021 16.979,18.908L16.522,18.552C15.92,18.064 15.312,17.495 14.934,17.117C14.556,16.739 13.985,16.13 13.496,15.527C13.252,15.227 13.015,14.91 12.831,14.615C12.689,14.388 12.514,14.075 12.449,13.752L12.428,13.612C12.387,13.171 12.535,12.762 12.68,12.465C12.796,12.227 12.94,12.002 13.084,11.804L13.225,11.616C13.243,11.593 13.253,11.568 13.255,11.55C13.256,11.543 13.256,11.537 13.255,11.535L11.415,8.025Z"
android:fillColor="#ffffff"/>
</vector>
9 changes: 9 additions & 0 deletions android/app/src/main/res/drawable/ic_call_end.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M15.999,11L16.723,11.017C17.584,11.05 18.826,11.139 20.17,11.314L20.849,11.41C22.456,11.651 24.207,12.026 25.619,12.61L25.94,12.758C26.613,13.099 27.139,13.58 27.494,14.188L27.573,14.33L27.706,14.617C27.945,15.194 28.018,15.795 27.996,16.359L27.976,16.638C27.844,18.027 27.157,19.366 26.5,20.22C26.021,20.842 25.226,21.123 24.47,20.95L24.32,20.909L20.537,19.729C20.335,19.666 20.145,19.571 19.973,19.449C19.815,19.337 19.682,19.207 19.573,19.074L19.565,19.065C19.305,18.743 19.164,18.372 19.116,18.017L19.115,18.018L19.085,17.806C19.073,17.731 19.059,17.66 19.043,17.595L18.99,17.405C18.98,17.378 18.971,17.354 18.962,17.333L18.804,17.289C18.691,17.263 18.56,17.239 18.413,17.217L17.932,17.156C17.245,17.085 16.483,17.06 15.998,17.06C15.635,17.06 15.116,17.074 14.59,17.111L14.067,17.156C13.72,17.192 13.42,17.236 13.195,17.288L13.196,17.289C13.132,17.304 13.079,17.32 13.036,17.333L13.009,17.405C12.972,17.514 12.938,17.651 12.913,17.806L12.884,18.018L12.882,18.017C12.834,18.369 12.695,18.743 12.43,19.069L12.423,19.077C12.307,19.219 12.197,19.315 12.139,19.362C12.105,19.39 12.075,19.413 12.049,19.432C12.036,19.441 12.024,19.449 12.013,19.457C12.007,19.461 12.002,19.465 11.997,19.469C11.994,19.471 11.991,19.472 11.989,19.474L11.985,19.477L11.983,19.479C11.823,19.586 11.646,19.671 11.461,19.729L7.679,20.909C6.88,21.159 6.009,20.883 5.498,20.22C4.883,19.419 4.24,18.192 4.053,16.897L4.023,16.638C3.953,15.904 4.031,15.087 4.426,14.331L4.505,14.188C4.914,13.487 5.551,12.953 6.379,12.61L6.688,12.488C8.249,11.894 10.161,11.533 11.828,11.314L12.491,11.234C14.01,11.062 15.326,11 15.999,11ZM15.998,13C15.324,13 13.802,13.074 12.087,13.298L11.436,13.39C9.907,13.619 8.342,13.964 7.145,14.459C6.643,14.667 6.36,14.949 6.199,15.257C6.03,15.579 5.969,15.983 6.013,16.448C6.09,17.249 6.471,18.108 6.898,18.742L7.083,19L10.866,17.819L10.877,17.809C10.883,17.802 10.888,17.792 10.893,17.781L10.903,17.741L10.936,17.508C10.965,17.327 11.004,17.135 11.058,16.944L11.118,16.756C11.225,16.443 11.409,16.048 11.75,15.766L11.864,15.682C12.138,15.499 12.483,15.401 12.745,15.341C12.914,15.302 13.097,15.268 13.286,15.239L13.86,15.167C14.631,15.087 15.464,15.06 15.998,15.06C16.533,15.06 17.367,15.087 18.139,15.167C18.524,15.207 18.915,15.262 19.254,15.341C19.515,15.401 19.861,15.499 20.135,15.682L20.249,15.766C20.59,16.048 20.774,16.443 20.881,16.756C20.967,17.006 21.025,17.267 21.063,17.508L21.096,17.741C21.1,17.771 21.111,17.795 21.122,17.809C21.126,17.815 21.131,17.818 21.132,17.819L24.916,19C25.381,18.395 25.837,17.487 25.964,16.621L25.986,16.448C26.03,15.983 25.969,15.579 25.8,15.257C25.659,14.987 25.425,14.737 25.033,14.54L24.854,14.459C23.657,13.964 22.092,13.619 20.563,13.39L19.912,13.298C18.196,13.074 16.673,13 15.998,13Z"
android:fillColor="#ffffff"/>
</vector>
Loading
Loading