fix: enable clickable links in AppleEmojiText for URLs, emails, and phone numbers
This commit is contained in:
@@ -54,7 +54,12 @@ android {
|
|||||||
kotlinOptions { jvmTarget = "11" }
|
kotlinOptions { jvmTarget = "11" }
|
||||||
buildFeatures { compose = true }
|
buildFeatures { compose = true }
|
||||||
composeOptions { kotlinCompilerExtensionVersion = "1.5.4" }
|
composeOptions { kotlinCompilerExtensionVersion = "1.5.4" }
|
||||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
packaging {
|
||||||
|
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -290,6 +290,13 @@ fun MessageBubble(
|
|||||||
else if (isDarkTheme) Color.White else Color(0xFF000000)
|
else if (isDarkTheme) Color.White else Color(0xFF000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔗 Цвет ссылок: для исходящих (синий фон) - светлый, для входящих - стандартный синий
|
||||||
|
val linkColor =
|
||||||
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
|
if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне
|
||||||
|
else Color(0xFF2196F3) // Стандартный Material Blue для входящих
|
||||||
|
}
|
||||||
|
|
||||||
val timeColor =
|
val timeColor =
|
||||||
remember(message.isOutgoing, isDarkTheme) {
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
if (message.isOutgoing) Color.White.copy(alpha = 0.7f)
|
if (message.isOutgoing) Color.White.copy(alpha = 0.7f)
|
||||||
@@ -606,7 +613,8 @@ fun MessageBubble(
|
|||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp,
|
||||||
|
linkColor = linkColor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
@@ -652,7 +660,8 @@ fun MessageBubble(
|
|||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp
|
fontSize = 17.sp,
|
||||||
|
linkColor = linkColor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
@@ -689,7 +698,8 @@ fun MessageBubble(
|
|||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp
|
fontSize = 17.sp,
|
||||||
|
linkColor = linkColor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
package com.rosetta.messenger.ui.components
|
package com.rosetta.messenger.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.TextPaint
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.ImageSpan
|
import android.text.style.ImageSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
|
import android.util.Patterns
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -305,6 +313,7 @@ fun AppleEmojiTextField(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TextView с Apple эмодзи (для отображения, не редактирования)
|
* TextView с Apple эмодзи (для отображения, не редактирования)
|
||||||
|
* 🔥 Поддержка кликабельных ссылок (URL, email, телефоны)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppleEmojiText(
|
fun AppleEmojiText(
|
||||||
@@ -314,7 +323,9 @@ fun AppleEmojiText(
|
|||||||
fontSize: androidx.compose.ui.unit.TextUnit = androidx.compose.ui.unit.TextUnit.Unspecified,
|
fontSize: androidx.compose.ui.unit.TextUnit = androidx.compose.ui.unit.TextUnit.Unspecified,
|
||||||
fontWeight: androidx.compose.ui.text.font.FontWeight? = null,
|
fontWeight: androidx.compose.ui.text.font.FontWeight? = null,
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
overflow: android.text.TextUtils.TruncateAt? = null
|
overflow: android.text.TextUtils.TruncateAt? = null,
|
||||||
|
linkColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color(0xFF54A9EB), // Telegram-style blue
|
||||||
|
enableLinks: Boolean = true // 🔥 Включить кликабельные ссылки
|
||||||
) {
|
) {
|
||||||
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
||||||
else fontSize.value
|
else fontSize.value
|
||||||
@@ -344,6 +355,11 @@ fun AppleEmojiText(
|
|||||||
if (overflow != null) {
|
if (overflow != null) {
|
||||||
ellipsize = overflow
|
ellipsize = overflow
|
||||||
}
|
}
|
||||||
|
// 🔥 Включаем кликабельные ссылки
|
||||||
|
if (enableLinks) {
|
||||||
|
setLinkColor(linkColor.toArgb())
|
||||||
|
enableClickableLinks(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
@@ -355,6 +371,11 @@ fun AppleEmojiText(
|
|||||||
if (overflow != null) {
|
if (overflow != null) {
|
||||||
view.ellipsize = overflow
|
view.ellipsize = overflow
|
||||||
}
|
}
|
||||||
|
// 🔥 Обновляем настройки ссылок
|
||||||
|
if (enableLinks) {
|
||||||
|
view.setLinkColor(linkColor.toArgb())
|
||||||
|
view.enableClickableLinks(true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
@@ -365,6 +386,7 @@ fun AppleEmojiText(
|
|||||||
* 🔥 Поддерживает:
|
* 🔥 Поддерживает:
|
||||||
* - Реальные Unicode эмодзи (😀)
|
* - Реальные Unicode эмодзи (😀)
|
||||||
* - Текстовый формат :emoji_1f600: (как в React Native версии)
|
* - Текстовый формат :emoji_1f600: (как в React Native версии)
|
||||||
|
* - Кликабельные ссылки (URL, email, телефоны)
|
||||||
*/
|
*/
|
||||||
class AppleEmojiTextView @JvmOverloads constructor(
|
class AppleEmojiTextView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -377,13 +399,48 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
// 🔥 Паттерн для :emoji_XXXX: формата (из React Native)
|
// 🔥 Паттерн для :emoji_XXXX: формата (из React Native)
|
||||||
private val EMOJI_CODE_PATTERN = Pattern.compile(":emoji_([a-fA-F0-9_-]+):")
|
private val EMOJI_CODE_PATTERN = Pattern.compile(":emoji_([a-fA-F0-9_-]+):")
|
||||||
private val bitmapCache = LruCache<String, Bitmap>(100)
|
private val bitmapCache = LruCache<String, Bitmap>(100)
|
||||||
|
|
||||||
|
// 🔥 Паттерны для ссылок
|
||||||
|
private val URL_PATTERN = Pattern.compile(
|
||||||
|
"(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)" +
|
||||||
|
"|(www\\.[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)" +
|
||||||
|
"|([\\w\\-]+\\.(com|org|net|ru|io|dev|app|me|co|info|biz|edu|gov|uk|de|fr|jp|cn|br|in|au|ca|it|es|nl|se|no|fi|dk|pl|ua|kz|by)[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]*)"
|
||||||
|
)
|
||||||
|
private val EMAIL_PATTERN = Patterns.EMAIL_ADDRESS
|
||||||
|
private val PHONE_PATTERN = Pattern.compile(
|
||||||
|
"\\+?[0-9][\\s\\-()0-9]{6,}[0-9]"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var linkColorValue: Int = 0xFF54A9EB.toInt() // Default Telegram blue
|
||||||
|
private var linksEnabled: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Отключаем лишние отступы шрифта для корректного отображения emoji
|
// Отключаем лишние отступы шрифта для корректного отображения emoji
|
||||||
includeFontPadding = false
|
includeFontPadding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Установить цвет для ссылок
|
||||||
|
*/
|
||||||
|
fun setLinkColor(color: Int) {
|
||||||
|
linkColorValue = color
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Включить/выключить кликабельные ссылки
|
||||||
|
*/
|
||||||
|
fun enableClickableLinks(enable: Boolean) {
|
||||||
|
linksEnabled = enable
|
||||||
|
if (enable) {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
// Убираем highlight при клике
|
||||||
|
highlightColor = android.graphics.Color.TRANSPARENT
|
||||||
|
} else {
|
||||||
|
movementMethod = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setTextWithEmojis(text: String) {
|
fun setTextWithEmojis(text: String) {
|
||||||
// 🔥 Сначала заменяем :emoji_XXXX: на PNG изображения
|
// 🔥 Сначала заменяем :emoji_XXXX: на PNG изображения
|
||||||
val spannable = SpannableStringBuilder(text)
|
val spannable = SpannableStringBuilder(text)
|
||||||
@@ -449,9 +506,95 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 5. Добавляем кликабельные ссылки после обработки эмодзи
|
||||||
|
if (linksEnabled) {
|
||||||
|
addClickableLinks(spannable)
|
||||||
|
}
|
||||||
|
|
||||||
setText(spannable)
|
setText(spannable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Добавляет кликабельные ссылки (URL, email, телефоны) в spannable
|
||||||
|
*/
|
||||||
|
private enum class LinkType { URL, EMAIL, PHONE }
|
||||||
|
|
||||||
|
private fun addClickableLinks(spannable: SpannableStringBuilder) {
|
||||||
|
val textStr = spannable.toString()
|
||||||
|
|
||||||
|
// Собираем все найденные ссылки
|
||||||
|
data class LinkMatch(val start: Int, val end: Int, val url: String, val type: LinkType)
|
||||||
|
val linkMatches = mutableListOf<LinkMatch>()
|
||||||
|
|
||||||
|
// 1. Ищем URL
|
||||||
|
val urlMatcher = URL_PATTERN.matcher(textStr)
|
||||||
|
while (urlMatcher.find()) {
|
||||||
|
var url = urlMatcher.group()
|
||||||
|
// Добавляем https:// если нет протокола
|
||||||
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
url = "https://$url"
|
||||||
|
}
|
||||||
|
linkMatches.add(LinkMatch(urlMatcher.start(), urlMatcher.end(), url, LinkType.URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ищем Email
|
||||||
|
val emailMatcher = EMAIL_PATTERN.matcher(textStr)
|
||||||
|
while (emailMatcher.find()) {
|
||||||
|
val email = emailMatcher.group()
|
||||||
|
// Проверяем что не перекрывается с URL
|
||||||
|
val overlaps = linkMatches.any {
|
||||||
|
(emailMatcher.start() >= it.start && emailMatcher.start() < it.end) ||
|
||||||
|
(emailMatcher.end() > it.start && emailMatcher.end() <= it.end)
|
||||||
|
}
|
||||||
|
if (!overlaps) {
|
||||||
|
linkMatches.add(LinkMatch(emailMatcher.start(), emailMatcher.end(), "mailto:$email", LinkType.EMAIL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ищем телефоны
|
||||||
|
val phoneMatcher = PHONE_PATTERN.matcher(textStr)
|
||||||
|
while (phoneMatcher.find()) {
|
||||||
|
val phone = phoneMatcher.group().replace("[\\s\\-()]".toRegex(), "")
|
||||||
|
// Проверяем что не перекрывается с другими ссылками
|
||||||
|
val overlaps = linkMatches.any {
|
||||||
|
(phoneMatcher.start() >= it.start && phoneMatcher.start() < it.end) ||
|
||||||
|
(phoneMatcher.end() > it.start && phoneMatcher.end() <= it.end)
|
||||||
|
}
|
||||||
|
if (!overlaps && phone.length >= 7) {
|
||||||
|
linkMatches.add(LinkMatch(phoneMatcher.start(), phoneMatcher.end(), "tel:$phone", LinkType.PHONE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Применяем ClickableSpan для каждой ссылки
|
||||||
|
for (link in linkMatches) {
|
||||||
|
val clickableSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link.url))
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Если не удалось открыть ссылку, игнорируем
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
super.updateDrawState(ds)
|
||||||
|
ds.color = linkColorValue
|
||||||
|
ds.isUnderlineText = false // Убираем подчёркивание (Telegram-style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spannable.setSpan(
|
||||||
|
clickableSpan,
|
||||||
|
link.start,
|
||||||
|
link.end,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadEmojiBitmap(unified: String): Bitmap? {
|
private fun loadEmojiBitmap(unified: String): Bitmap? {
|
||||||
bitmapCache.get(unified)?.let { return it }
|
bitmapCache.get(unified)?.let { return it }
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ private fun computeAvatarState(
|
|||||||
avatarSizeMinPx: Float, // Into notch size (24dp or notch width)
|
avatarSizeMinPx: Float, // Into notch size (24dp or notch width)
|
||||||
hasAvatar: Boolean,
|
hasAvatar: Boolean,
|
||||||
// Notch info
|
// Notch info
|
||||||
|
notchCenterX: Float, // X position of front camera/notch
|
||||||
notchCenterY: Float,
|
notchCenterY: Float,
|
||||||
notchRadiusPx: Float,
|
notchRadiusPx: Float,
|
||||||
// Telegram thresholds in pixels
|
// Telegram thresholds in pixels
|
||||||
@@ -211,9 +212,20 @@ private fun computeAvatarState(
|
|||||||
val isNear = radius <= dp32
|
val isNear = radius <= dp32
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// CENTER X - always screen center
|
// CENTER X - animate towards notch/camera position when collapsing
|
||||||
|
// Normal: screen center, Collapsed: notch center (front camera)
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
val centerX = screenWidthPx / 2f
|
val startX = screenWidthPx / 2f // Normal position = screen center
|
||||||
|
val endX = notchCenterX // Target = front camera position
|
||||||
|
|
||||||
|
val centerX: Float = when {
|
||||||
|
// Pull-down expansion - stay at screen center
|
||||||
|
hasAvatar && expansionProgress > 0f -> screenWidthPx / 2f
|
||||||
|
// Collapsing - animate X towards notch/camera
|
||||||
|
collapseProgress > 0f -> lerpFloat(endX, startX, diff)
|
||||||
|
// Normal state - screen center
|
||||||
|
else -> screenWidthPx / 2f
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// CENTER Y - Telegram: avatarY = lerp(endY, startY, diff)
|
// CENTER Y - Telegram: avatarY = lerp(endY, startY, diff)
|
||||||
@@ -380,7 +392,7 @@ fun ProfileMetaballOverlay(
|
|||||||
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
||||||
// derivedStateOf автоматически отслеживает их как зависимости внутри лямбды
|
// derivedStateOf автоматически отслеживает их как зависимости внутри лямбды
|
||||||
// Только стабильные параметры (размеры экрана, notch info) как ключи remember
|
// Только стабильные параметры (размеры экрана, notch info) как ключи remember
|
||||||
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx, notchCenterY, notchRadiusPx) {
|
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx, notchCenterX, notchCenterY, notchRadiusPx) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
computeAvatarState(
|
computeAvatarState(
|
||||||
collapseProgress = collapseProgress,
|
collapseProgress = collapseProgress,
|
||||||
@@ -391,6 +403,7 @@ fun ProfileMetaballOverlay(
|
|||||||
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
||||||
avatarSizeMinPx = avatarSizeMinPx,
|
avatarSizeMinPx = avatarSizeMinPx,
|
||||||
hasAvatar = hasAvatar,
|
hasAvatar = hasAvatar,
|
||||||
|
notchCenterX = notchCenterX,
|
||||||
notchCenterY = notchCenterY,
|
notchCenterY = notchCenterY,
|
||||||
notchRadiusPx = notchRadiusPx,
|
notchRadiusPx = notchRadiusPx,
|
||||||
dp40 = dp40,
|
dp40 = dp40,
|
||||||
@@ -649,6 +662,7 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
val avatarSizeMinPx = with(density) { ProfileMetaballConstants.AVATAR_SIZE_MIN.toPx() }
|
val avatarSizeMinPx = with(density) { ProfileMetaballConstants.AVATAR_SIZE_MIN.toPx() }
|
||||||
|
|
||||||
// Fallback notch values for compat mode
|
// Fallback notch values for compat mode
|
||||||
|
val notchCenterX = screenWidthPx / 2f // Center of screen for compat mode
|
||||||
val notchCenterY = statusBarHeightPx / 2f
|
val notchCenterY = statusBarHeightPx / 2f
|
||||||
val notchRadiusPx = with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
|
val notchRadiusPx = with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
|
||||||
|
|
||||||
@@ -661,7 +675,7 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
|
|
||||||
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
||||||
// derivedStateOf автоматически отслеживает их как зависимости
|
// derivedStateOf автоматически отслеживает их как зависимости
|
||||||
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx) {
|
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx, notchCenterX) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
computeAvatarState(
|
computeAvatarState(
|
||||||
collapseProgress = collapseProgress,
|
collapseProgress = collapseProgress,
|
||||||
@@ -672,6 +686,7 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
||||||
avatarSizeMinPx = avatarSizeMinPx,
|
avatarSizeMinPx = avatarSizeMinPx,
|
||||||
hasAvatar = hasAvatar,
|
hasAvatar = hasAvatar,
|
||||||
|
notchCenterX = notchCenterX,
|
||||||
notchCenterY = notchCenterY,
|
notchCenterY = notchCenterY,
|
||||||
notchRadiusPx = notchRadiusPx,
|
notchRadiusPx = notchRadiusPx,
|
||||||
dp40 = dp40,
|
dp40 = dp40,
|
||||||
|
|||||||
Reference in New Issue
Block a user