From 0d598372bc6514da80b36d73a6b06aaa5bfce3f7 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 11 Feb 2026 10:43:56 -0300 Subject: [PATCH 1/9] Base of full screen incoming call on Android --- android/app/build.gradle | 1 + .../reactnative/voip/IncomingCallActivity.kt | 98 ++++++-- .../rocket/reactnative/voip/VoipPayload.kt | 13 +- .../res/drawable/bg_avatar_incoming_call.xml | 6 + .../src/main/res/drawable/bg_btn_accept.xml | 6 + .../src/main/res/drawable/bg_btn_reject.xml | 6 + android/app/src/main/res/drawable/ic_call.xml | 9 + .../app/src/main/res/drawable/ic_call_end.xml | 9 + .../res/layout/activity_incoming_call.xml | 229 +++++++++++++----- .../res/values-night/colors_incoming_call.xml | 15 ++ .../main/res/values/colors_incoming_call.xml | 15 ++ .../main/res/values/strings_incoming_call.xml | 6 + .../main/res/values/styles_incoming_call.xml | 2 +- 13 files changed, 340 insertions(+), 75 deletions(-) create mode 100644 android/app/src/main/res/drawable/bg_avatar_incoming_call.xml create mode 100644 android/app/src/main/res/drawable/bg_btn_accept.xml create mode 100644 android/app/src/main/res/drawable/bg_btn_reject.xml create mode 100644 android/app/src/main/res/drawable/ic_call.xml create mode 100644 android/app/src/main/res/drawable/ic_call_end.xml create mode 100644 android/app/src/main/res/values-night/colors_incoming_call.xml create mode 100644 android/app/src/main/res/values/colors_incoming_call.xml create mode 100644 android/app/src/main/res/values/strings_incoming_call.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index f0f06675f4c..f47eee7bc95 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index 82da05741c3..e28ef9a8867 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -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.util.Log -import androidx.core.content.ContextCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.R +import android.graphics.Typeface /** * Full-screen Activity displayed when an incoming VoIP call arrives. @@ -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()) { @@ -72,15 +78,80 @@ 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.caller_avatar_initial, + R.id.host_name, + R.id.incoming_call_reject_label, + R.id.incoming_call_accept_label + ).forEach { id -> + findViewById(id)?.setTypeface(interRegular) + } + findViewById(R.id.caller_name)?.setTypeface(interBold) + } + private fun updateUI(payload: VoipPayload) { - val callerView = findViewById(R.id.caller_name) - callerView?.text = payload.caller - - // Try to load avatar if available - // TODO: needs username to load avatar - val avatarView = findViewById(R.id.caller_avatar) - // Avatar loading would require additional data - can be enhanced later - // For now, just show a placeholder or default icon + findViewById(R.id.caller_name)?.text = payload.caller.ifEmpty { "" } + findViewById(R.id.host_name)?.text = payload.hostName.ifEmpty { "" } + + // Avatar: initial as fallback, load from host/avatar/username when available + val initialView = findViewById(R.id.caller_avatar_initial) + val avatarImageView = findViewById(R.id.caller_avatar_image) + val initial = payload.caller.firstOrNull()?.uppercaseChar()?.toString() ?: "?" + initialView?.text = initial + + // loadAvatar(payload, avatarImageView, initialView) + } + + /** + * Loads avatar from ${host}/avatar/username. + * Falls back to initial letter if load fails or URL is invalid. + * A proper avatar URL function can replace this later. + */ + private fun loadAvatar(payload: VoipPayload, imageView: ImageView?, initialView: TextView?) { + if (imageView == null || initialView == null) return + if (payload.host.isBlank() || payload.caller.isBlank()) return + + val baseUrl = payload.host.trim().removeSuffix("/") + val url = if (baseUrl.startsWith("http")) baseUrl else "https://$baseUrl" + val username = payload.caller.trim().let { if (it.startsWith("@")) it else "@$it" } + val avatarUrl = "$url/avatar/$username?format=png&size=240" + + val cornerRadiusPx = (8 * resources.displayMetrics.density).toInt() + val requestOptions = RequestOptions() + .transform(CenterCrop(), RoundedCorners(cornerRadiusPx)) + + Glide.with(this) + .load(avatarUrl) + .apply(requestOptions) + .into(object : com.bumptech.glide.request.target.CustomTarget(360, 360) { + override fun onResourceReady( + resource: android.graphics.drawable.Drawable, + transition: com.bumptech.glide.request.transition.Transition? + ) { + initialView.visibility = View.GONE + imageView.visibility = View.VISIBLE + imageView.setImageDrawable(resource) + } + + override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) { + initialView.visibility = View.VISIBLE + imageView.visibility = View.GONE + } + }) } private fun startRingtone() { @@ -105,14 +176,11 @@ class IncomingCallActivity : Activity() { } private fun setupButtons(payload: VoipPayload) { - val acceptButton = findViewById(R.id.btn_accept) - val declineButton = findViewById(R.id.btn_decline) - - acceptButton?.setOnClickListener { + findViewById(R.id.btn_accept)?.setOnClickListener { handleAccept(payload) } - declineButton?.setOnClickListener { + findViewById(R.id.btn_decline)?.setOnClickListener { handleDecline(payload) } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt index d1ffa2e53d7..678debddea7 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt @@ -18,7 +18,10 @@ data class VoipPayload( 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) @@ -33,6 +36,7 @@ data class VoipPayload( putString("caller", caller) 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 @@ -46,6 +50,7 @@ data class VoipPayload( putString("caller", caller) putString("host", host) putString("type", type) + putString("hostName", hostName) putString("callUUID", callUUID) putInt("notificationId", notificationId) } @@ -58,9 +63,10 @@ data class VoipPayload( val callId = data["callId"] ?: return null val caller = data["caller"] ?: return null val host = data["host"] ?: return null + val hostName = data["hostName"] ?: "" if (type != "incoming_call") return null - return VoipPayload(callId, caller, host, type) + return VoipPayload(callId, caller, host, type, hostName) } fun fromBundle(bundle: Bundle?): VoipPayload? { @@ -69,8 +75,9 @@ data class VoipPayload( val caller = bundle.getString("caller") ?: "" val host = bundle.getString("host") ?: "" val type = bundle.getString("type") ?: "" + val hostName = bundle.getString("hostName") ?: "" - return VoipPayload(callId, caller, host, type) + return VoipPayload(callId, caller, host, type, hostName) } } } \ No newline at end of file diff --git a/android/app/src/main/res/drawable/bg_avatar_incoming_call.xml b/android/app/src/main/res/drawable/bg_avatar_incoming_call.xml new file mode 100644 index 00000000000..15b9de09786 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_avatar_incoming_call.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/drawable/bg_btn_accept.xml b/android/app/src/main/res/drawable/bg_btn_accept.xml new file mode 100644 index 00000000000..e6f8ea9891c --- /dev/null +++ b/android/app/src/main/res/drawable/bg_btn_accept.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/drawable/bg_btn_reject.xml b/android/app/src/main/res/drawable/bg_btn_reject.xml new file mode 100644 index 00000000000..8f9083b8279 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_btn_reject.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_call.xml b/android/app/src/main/res/drawable/ic_call.xml new file mode 100644 index 00000000000..6d6192d7ca1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_call.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_call_end.xml b/android/app/src/main/res/drawable/ic_call_end.xml new file mode 100644 index 00000000000..443ec5c0aeb --- /dev/null +++ b/android/app/src/main/res/drawable/ic_call_end.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/layout/activity_incoming_call.xml b/android/app/src/main/res/layout/activity_incoming_call.xml index d025d991c16..7eb7fb9e894 100644 --- a/android/app/src/main/res/layout/activity_incoming_call.xml +++ b/android/app/src/main/res/layout/activity_incoming_call.xml @@ -1,72 +1,189 @@ - - - - - - - - - + android:background="@color/incoming_call_background" + android:orientation="vertical"> - + + + + + + + + + + + android:orientation="vertical"> + + + + + + + + + + + + + + + + + - - + + + + - - - + + + + + + + + + + + + + + + + + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="44dp" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center" + android:orientation="vertical" + android:padding="16dp"> + + + + + + + + + + diff --git a/android/app/src/main/res/values-night/colors_incoming_call.xml b/android/app/src/main/res/values-night/colors_incoming_call.xml new file mode 100644 index 00000000000..f68d6ba2d4d --- /dev/null +++ b/android/app/src/main/res/values-night/colors_incoming_call.xml @@ -0,0 +1,15 @@ + + + + #1F2329 + #9EA2A8 + #9EA2A8 + #F2F3F5 + #9EA2A8 + #5F1477 + #FFFFFF + #BB3E4E + #1D7256 + #F2F3F5 + #FFFFFF + diff --git a/android/app/src/main/res/values/colors_incoming_call.xml b/android/app/src/main/res/values/colors_incoming_call.xml new file mode 100644 index 00000000000..14984e230a4 --- /dev/null +++ b/android/app/src/main/res/values/colors_incoming_call.xml @@ -0,0 +1,15 @@ + + + + #FFFFFF + #6C727A + #6C727A + #1F2329 + #6C727A + #5F1477 + #FFFFFF + #EC0D2A + #158D65 + #1F2329 + #FFFFFF + diff --git a/android/app/src/main/res/values/strings_incoming_call.xml b/android/app/src/main/res/values/strings_incoming_call.xml new file mode 100644 index 00000000000..d2dbe067bcd --- /dev/null +++ b/android/app/src/main/res/values/strings_incoming_call.xml @@ -0,0 +1,6 @@ + + + Incoming call… + Reject + Accept + diff --git a/android/app/src/main/res/values/styles_incoming_call.xml b/android/app/src/main/res/values/styles_incoming_call.xml index a5a7484c14d..bee85d25875 100644 --- a/android/app/src/main/res/values/styles_incoming_call.xml +++ b/android/app/src/main/res/values/styles_incoming_call.xml @@ -4,7 +4,7 @@ true true false - @android:color/black + @color/incoming_call_background true @android:color/transparent @android:color/transparent From 5ad2eae8970b4e1931ab56b42dc6ab43e883a8da Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 11 Feb 2026 13:19:34 -0300 Subject: [PATCH 2/9] Refactor IncomingCallActivity and VoipPayload: Remove initial avatar display, streamline avatar loading logic, and add username field to VoipPayload for improved avatar handling. Update layout to reflect changes in avatar ImageView ID. --- .../reactnative/voip/IncomingCallActivity.kt | 52 +++++++++---------- .../rocket/reactnative/voip/VoipPayload.kt | 12 ++++- .../res/layout/activity_incoming_call.xml | 14 +---- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index e28ef9a8867..10b3f752ed7 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -10,14 +10,13 @@ import android.os.Build import android.os.Bundle import android.view.WindowManager import android.view.View +import android.view.ViewOutlineProvider +import android.graphics.Outline import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import android.util.Log import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.CenterCrop -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.R import android.graphics.Typeface @@ -93,7 +92,6 @@ class IncomingCallActivity : Activity() { } listOf( R.id.header_text, - R.id.caller_avatar_initial, R.id.host_name, R.id.incoming_call_reject_label, R.id.incoming_call_accept_label @@ -107,48 +105,50 @@ class IncomingCallActivity : Activity() { findViewById(R.id.caller_name)?.text = payload.caller.ifEmpty { "" } findViewById(R.id.host_name)?.text = payload.hostName.ifEmpty { "" } - // Avatar: initial as fallback, load from host/avatar/username when available - val initialView = findViewById(R.id.caller_avatar_initial) - val avatarImageView = findViewById(R.id.caller_avatar_image) - val initial = payload.caller.firstOrNull()?.uppercaseChar()?.toString() ?: "?" - initialView?.text = initial - - // loadAvatar(payload, avatarImageView, initialView) + loadAvatar(payload) } /** * Loads avatar from ${host}/avatar/username. - * Falls back to initial letter if load fails or URL is invalid. - * A proper avatar URL function can replace this later. */ - private fun loadAvatar(payload: VoipPayload, imageView: ImageView?, initialView: TextView?) { - if (imageView == null || initialView == null) return - if (payload.host.isBlank() || payload.caller.isBlank()) return + private fun loadAvatar(payload: VoipPayload) { + if (payload.host.isBlank() || payload.username.isBlank()) return + val imageView = findViewById(R.id.avatar) val baseUrl = payload.host.trim().removeSuffix("/") val url = if (baseUrl.startsWith("http")) baseUrl else "https://$baseUrl" - val username = payload.caller.trim().let { if (it.startsWith("@")) it else "@$it" } - val avatarUrl = "$url/avatar/$username?format=png&size=240" - - val cornerRadiusPx = (8 * resources.displayMetrics.density).toInt() - val requestOptions = RequestOptions() - .transform(CenterCrop(), RoundedCorners(cornerRadiusPx)) + val username = payload.username.trim() + val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480) + val avatarUrl = "$url/avatar/$username?format=png&size=$sizePx" + val cornerRadiusPx = 8 * resources.displayMetrics.density Glide.with(this) .load(avatarUrl) - .apply(requestOptions) - .into(object : com.bumptech.glide.request.target.CustomTarget(360, 360) { + .into(object : com.bumptech.glide.request.target.CustomTarget(sizePx, sizePx) { override fun onResourceReady( resource: android.graphics.drawable.Drawable, transition: com.bumptech.glide.request.transition.Transition? ) { - initialView.visibility = View.GONE imageView.visibility = View.VISIBLE imageView.setImageDrawable(resource) + // View-level clipping works for both PNG and SVG (bitmap transforms don't apply to SVG) + imageView.post { + imageView.outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx) + } + } + imageView.clipToOutline = true + } + } + + override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) { + // Hide the image view if the load fails (URL error, timeout, etc.) + imageView.visibility = View.GONE } override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) { - initialView.visibility = View.VISIBLE + // Clean up when the view is destroyed or recycled imageView.visibility = View.GONE } }) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt index 678debddea7..a9cf33988bf 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt @@ -13,6 +13,9 @@ data class VoipPayload( @SerializedName("caller") val caller: String, + + @SerializedName("username") + val username: String = "", @SerializedName("host") val host: String, @@ -34,6 +37,7 @@ data class VoipPayload( return Bundle().apply { putString("callId", callId) putString("caller", caller) + putString("username", username) putString("host", host) putString("type", type) putString("hostName", hostName) @@ -48,6 +52,7 @@ 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) @@ -62,22 +67,25 @@ data class VoipPayload( 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"] ?: "" if (type != "incoming_call") return null - return VoipPayload(callId, caller, host, type, hostName) + 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 username = bundle.getString("username") ?: "" val host = bundle.getString("host") ?: "" val type = bundle.getString("type") ?: "" val hostName = bundle.getString("hostName") ?: "" - return VoipPayload(callId, caller, host, type, hostName) + return VoipPayload(callId, caller, username, host, type, hostName) } } } \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_incoming_call.xml b/android/app/src/main/res/layout/activity_incoming_call.xml index 7eb7fb9e894..5f36075114a 100644 --- a/android/app/src/main/res/layout/activity_incoming_call.xml +++ b/android/app/src/main/res/layout/activity_incoming_call.xml @@ -49,20 +49,8 @@ android:layout_marginTop="-60dp" android:layout_marginBottom="24dp"> - - Date: Wed, 11 Feb 2026 13:32:32 -0300 Subject: [PATCH 3/9] Fix corner radius --- .../reactnative/voip/IncomingCallActivity.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index 10b3f752ed7..520363e8205 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -10,12 +10,11 @@ import android.os.Build import android.os.Bundle import android.view.WindowManager import android.view.View -import android.view.ViewOutlineProvider -import android.graphics.Outline import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import android.util.Log +import android.view.ViewOutlineProvider import com.bumptech.glide.Glide import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.R @@ -120,7 +119,7 @@ class IncomingCallActivity : Activity() { val username = payload.username.trim() val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480) val avatarUrl = "$url/avatar/$username?format=png&size=$sizePx" - val cornerRadiusPx = 8 * resources.displayMetrics.density + val cornerRadiusPx = (8 * resources.displayMetrics.density).toFloat() Glide.with(this) .load(avatarUrl) @@ -131,15 +130,7 @@ class IncomingCallActivity : Activity() { ) { imageView.visibility = View.VISIBLE imageView.setImageDrawable(resource) - // View-level clipping works for both PNG and SVG (bitmap transforms don't apply to SVG) - imageView.post { - imageView.outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx) - } - } - imageView.clipToOutline = true - } + applyAvatarRoundCorners(imageView, cornerRadiusPx) } override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) { @@ -154,6 +145,21 @@ class IncomingCallActivity : Activity() { }) } + /** + * 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 { + imageView.outlineProvider = ViewOutlineProvider { outline -> + outline.setRoundRect(0, 0, imageView.width, imageView.height, cornerRadiusPx) + } + imageView.clipToOutline = true + } + } + private fun startRingtone() { try { val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) From 9f5779e5ef9d3dfe35964bb19fabe300aa8f721a Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 11 Feb 2026 13:41:32 -0300 Subject: [PATCH 4/9] Enhance avatar URI generation: Update Ejson class to support customizable avatar sizes and refactor IncomingCallActivity to utilize new avatar URI method for improved avatar loading logic. --- .../reactnative/notification/Ejson.java | 34 +++++++++++++++---- .../reactnative/voip/IncomingCallActivity.kt | 17 +++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java index e9998ff8231..43489b5dd2f 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java @@ -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"); @@ -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; } @@ -102,15 +102,37 @@ 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; @@ -118,7 +140,7 @@ public String getCallerAvatarUri() { 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; diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index 520363e8205..04915da3f8d 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -18,6 +18,7 @@ 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 /** @@ -107,18 +108,13 @@ class IncomingCallActivity : Activity() { loadAvatar(payload) } - /** - * Loads avatar from ${host}/avatar/username. - */ private fun loadAvatar(payload: VoipPayload) { if (payload.host.isBlank() || payload.username.isBlank()) return val imageView = findViewById(R.id.avatar) - val baseUrl = payload.host.trim().removeSuffix("/") - val url = if (baseUrl.startsWith("http")) baseUrl else "https://$baseUrl" - val username = payload.username.trim() val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480) - val avatarUrl = "$url/avatar/$username?format=png&size=$sizePx" + val avatarUrl = Ejson.forCallerAvatar(payload.host, payload.username)?.getCallerAvatarUri(sizePx) + ?: return val cornerRadiusPx = (8 * resources.displayMetrics.density).toFloat() Glide.with(this) @@ -153,8 +149,11 @@ class IncomingCallActivity : Activity() { private fun applyAvatarRoundCorners(imageView: ImageView, cornerRadiusPx: Float) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return imageView.post { - imageView.outlineProvider = ViewOutlineProvider { outline -> - outline.setRoundRect(0, 0, imageView.width, imageView.height, cornerRadiusPx) + 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 } From 930b852a22346753f6cbca6cb1159c20c03bc2f2 Mon Sep 17 00:00:00 2001 From: diegolmello Date: Wed, 11 Feb 2026 16:50:33 +0000 Subject: [PATCH 5/9] chore: format code and fix lint issues [skip ci] --- app/sagas/deepLinking.js | 2 +- app/sagas/login.js | 10 +++++----- index.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index d57f47e7fa1..6b0aca2235a 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -46,7 +46,7 @@ const waitForNavigation = () => { if (Navigation.navigationRef.current) { return Promise.resolve(); } - return new Promise(resolve => { + return new Promise((resolve) => { const listener = () => { emitter.off('navigationReady', listener); resolve(); diff --git a/app/sagas/login.js b/app/sagas/login.js index 6e7737a854c..d3c52a6b9a8 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -60,7 +60,7 @@ const showSupportedVersionsWarning = function* showSupportedVersionsWarning(serv const serversDB = database.servers; yield serversDB.write(async () => { - await serverRecord.update(r => { + await serverRecord.update((r) => { r.supportedVersionsWarningAt = new Date(); }); }); @@ -107,7 +107,7 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE } // this is updating on every login just to save `updated_at` // keeping this server as the most recent on autocomplete order - await serverHistoryRecord.update(s => { + await serverHistoryRecord.update((s) => { s.username = result.username; if (iconURL) { s.iconURL = iconURL; @@ -279,12 +279,12 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield serversDB.write(async () => { try { const userRecord = await usersCollection.find(user.id); - await userRecord.update(record => { + await userRecord.update((record) => { record._raw = sanitizedRaw({ id: user.id, ...record._raw }, usersCollection.schema); Object.assign(record, u); }); } catch (e) { - await usersCollection.create(record => { + await usersCollection.create((record) => { record._raw = sanitizedRaw({ id: user.id }, usersCollection.schema); Object.assign(record, u); }); @@ -361,7 +361,7 @@ const handleSetUser = function* handleSetUser({ user }) { yield serversDB.write(async () => { try { const record = await userCollections.find(userId); - await record.update(userRecord => { + await record.update((userRecord) => { if ('avatarETag' in user) { userRecord.avatarETag = user.avatarETag; } diff --git a/index.js b/index.js index a59c08135ce..a8ca1d5a6a0 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ if (process.env.USE_STORYBOOK) { console.log('RNCallKeep setup successful'); RNCallKeep.canMakeMultipleCalls(false); }) - .catch(error => { + .catch((error) => { console.error('Error setting up RNCallKeep:', error); }); } From ea248313ce73674964e679916934cf726b8baf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio=20Stasiak?= <91474186+OtavioStasiak@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:07:46 -0300 Subject: [PATCH 6/9] chore: remove skip ci (#6950) --- .github/workflows/prettier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 3e5a93039c8..c49c60afe60 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -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 }} \ No newline at end of file From d04f155d8199f2afdb7a6bc1329e4f62138a67d6 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 11 Feb 2026 15:11:58 -0300 Subject: [PATCH 7/9] fix coderabbit issues --- .../rocket/reactnative/voip/VoipPayload.kt | 19 ++++++++----------- .../res/layout/activity_incoming_call.xml | 9 ++++----- .../res/values-night/colors_incoming_call.xml | 6 +++--- .../main/res/values/strings_incoming_call.xml | 1 + 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt index a9cf33988bf..e9971eca91a 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt @@ -5,7 +5,6 @@ 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") @@ -15,7 +14,7 @@ data class VoipPayload( val caller: String, @SerializedName("username") - val username: String = "", + val username: String, @SerializedName("host") val host: String, @@ -24,7 +23,7 @@ data class VoipPayload( val type: String, @SerializedName("hostName") - val hostName: String = "" + val hostName: String, ) { val notificationId: Int = callId.hashCode() val callUUID: String = CallIdUUID.generateUUIDv5(callId) @@ -63,14 +62,12 @@ data class VoipPayload( companion object { fun fromMap(data: Map): 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"] ?: "" + val hostName = data["hostName"] ?: return null if (type != "incoming_call") return null return VoipPayload(callId, caller, username, host, type, hostName) @@ -79,11 +76,11 @@ data class VoipPayload( fun fromBundle(bundle: Bundle?): VoipPayload? { if (bundle == null) return null val callId = bundle.getString("callId") ?: return null - val caller = bundle.getString("caller") ?: "" - val username = bundle.getString("username") ?: "" - val host = bundle.getString("host") ?: "" - val type = bundle.getString("type") ?: "" - val hostName = bundle.getString("hostName") ?: "" + 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, username, host, type, hostName) } diff --git a/android/app/src/main/res/layout/activity_incoming_call.xml b/android/app/src/main/res/layout/activity_incoming_call.xml index 5f36075114a..5a0866fe761 100644 --- a/android/app/src/main/res/layout/activity_incoming_call.xml +++ b/android/app/src/main/res/layout/activity_incoming_call.xml @@ -19,7 +19,6 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_marginEnd="8dp" - android:contentDescription="@string/incoming_call_status" android:src="@drawable/ic_notification" android:tint="@color/incoming_call_header_icon" /> @@ -53,7 +52,7 @@ android:id="@+id/avatar" android:layout_width="match_parent" android:layout_height="match_parent" - android:contentDescription="Caller avatar" + android:contentDescription="@string/incoming_call_avatar_description" android:scaleType="centerCrop" android:visibility="gone" /> @@ -65,17 +64,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - android:text="Unknown caller" android:textColor="@color/incoming_call_name" android:textSize="22sp" - android:textStyle="bold" /> + android:textStyle="bold" + tools:text="Unknown caller" /> diff --git a/android/app/src/main/res/values-night/colors_incoming_call.xml b/android/app/src/main/res/values-night/colors_incoming_call.xml index f68d6ba2d4d..4e1b5e4533f 100644 --- a/android/app/src/main/res/values-night/colors_incoming_call.xml +++ b/android/app/src/main/res/values-night/colors_incoming_call.xml @@ -2,10 +2,10 @@ #1F2329 - #9EA2A8 - #9EA2A8 + #9EA2A8 + #9EA2A8 #F2F3F5 - #9EA2A8 + #9EA2A8 #5F1477 #FFFFFF #BB3E4E diff --git a/android/app/src/main/res/values/strings_incoming_call.xml b/android/app/src/main/res/values/strings_incoming_call.xml index d2dbe067bcd..68ad9c5f72d 100644 --- a/android/app/src/main/res/values/strings_incoming_call.xml +++ b/android/app/src/main/res/values/strings_incoming_call.xml @@ -3,4 +3,5 @@ Incoming call… Reject Accept + Caller avatar From 6c879b84721e379bbbb8a32b0a4537948e43c58c Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 11 Feb 2026 15:38:24 -0300 Subject: [PATCH 8/9] Move caller section to the top --- .../rocket/reactnative/voip/IncomingCallActivity.kt | 10 +++++----- .../src/main/res/layout/activity_incoming_call.xml | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index 04915da3f8d..f3618c22ac8 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -13,6 +13,7 @@ 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 android.view.ViewOutlineProvider import com.bumptech.glide.Glide @@ -111,6 +112,7 @@ class IncomingCallActivity : Activity() { private fun loadAvatar(payload: VoipPayload) { if (payload.host.isBlank() || payload.username.isBlank()) return + val container = findViewById(R.id.avatar_container) val imageView = findViewById(R.id.avatar) val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480) val avatarUrl = Ejson.forCallerAvatar(payload.host, payload.username)?.getCallerAvatarUri(sizePx) @@ -124,19 +126,17 @@ class IncomingCallActivity : Activity() { resource: android.graphics.drawable.Drawable, transition: com.bumptech.glide.request.transition.Transition? ) { - imageView.visibility = View.VISIBLE + container.visibility = View.VISIBLE imageView.setImageDrawable(resource) applyAvatarRoundCorners(imageView, cornerRadiusPx) } override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) { - // Hide the image view if the load fails (URL error, timeout, etc.) - imageView.visibility = View.GONE + container.visibility = View.GONE } override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) { - // Clean up when the view is destroyed or recycled - imageView.visibility = View.GONE + container.visibility = View.GONE } }) } diff --git a/android/app/src/main/res/layout/activity_incoming_call.xml b/android/app/src/main/res/layout/activity_incoming_call.xml index 5a0866fe761..ec176db430f 100644 --- a/android/app/src/main/res/layout/activity_incoming_call.xml +++ b/android/app/src/main/res/layout/activity_incoming_call.xml @@ -33,19 +33,19 @@ - + - + android:scaleType="centerCrop" /> @@ -69,7 +68,7 @@ android:textStyle="bold" tools:text="Unknown caller" /> - + Date: Wed, 11 Feb 2026 16:51:10 -0300 Subject: [PATCH 9/9] Unknown caller if empty --- .../java/chat/rocket/reactnative/voip/IncomingCallActivity.kt | 4 ++-- android/app/src/main/res/values/strings_incoming_call.xml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index f3618c22ac8..fa157c8951b 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -103,8 +103,8 @@ class IncomingCallActivity : Activity() { } private fun updateUI(payload: VoipPayload) { - findViewById(R.id.caller_name)?.text = payload.caller.ifEmpty { "" } - findViewById(R.id.host_name)?.text = payload.hostName.ifEmpty { "" } + findViewById(R.id.caller_name)?.text = payload.caller.ifEmpty { getString(R.string.incoming_call_unknown_caller) } + findViewById(R.id.host_name)?.text = payload.hostName.ifEmpty { getString(R.string.incoming_call_unknown_host) } loadAvatar(payload) } diff --git a/android/app/src/main/res/values/strings_incoming_call.xml b/android/app/src/main/res/values/strings_incoming_call.xml index 68ad9c5f72d..bb4905d72da 100644 --- a/android/app/src/main/res/values/strings_incoming_call.xml +++ b/android/app/src/main/res/values/strings_incoming_call.xml @@ -4,4 +4,6 @@ Reject Accept Caller avatar + Unknown caller + Unknown host