feat: enhance versioning and avatar handling with dynamic properties and improved UI interactions
This commit is contained in:
@@ -5,6 +5,20 @@ plugins {
|
|||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun safeGitOutput(vararg args: String): String? {
|
||||||
|
return runCatching {
|
||||||
|
providers.exec { commandLine("git", *args) }.standardOutput.asText.get().trim().ifBlank { null }
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
val versionBase = providers.gradleProperty("ROSETTA_VERSION_BASE").orElse("1.0")
|
||||||
|
val explicitVersionCode = providers.gradleProperty("ROSETTA_VERSION_CODE").orNull?.toIntOrNull()
|
||||||
|
val explicitVersionName = providers.gradleProperty("ROSETTA_VERSION_NAME").orNull
|
||||||
|
val gitCommitCount = safeGitOutput("rev-list", "--count", "HEAD")?.toIntOrNull()
|
||||||
|
val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
||||||
|
val computedVersionCode = (explicitVersionCode ?: gitCommitCount ?: 1).coerceAtLeast(1)
|
||||||
|
val computedVersionName = explicitVersionName ?: "${versionBase.get()}.$computedVersionCode"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.rosetta.messenger"
|
namespace = "com.rosetta.messenger"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
@@ -13,8 +27,9 @@ android {
|
|||||||
applicationId = "com.rosetta.messenger"
|
applicationId = "com.rosetta.messenger"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = computedVersionCode
|
||||||
versionName = "1.0"
|
versionName = computedVersionName
|
||||||
|
buildConfigField("String", "GIT_SHA", "\"$gitShortSha\"")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables { useSupportLibrary = true }
|
vectorDrawables { useSupportLibrary = true }
|
||||||
@@ -52,7 +67,10 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions { jvmTarget = "11" }
|
kotlinOptions { jvmTarget = "11" }
|
||||||
buildFeatures { compose = true }
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
composeOptions { kotlinCompilerExtensionVersion = "1.5.4" }
|
composeOptions { kotlinCompilerExtensionVersion = "1.5.4" }
|
||||||
packaging {
|
packaging {
|
||||||
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
|
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.rosetta.messenger.ui.auth.AccountInfo
|
|||||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
import com.rosetta.messenger.ui.auth.AuthFlow
|
||||||
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
||||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||||
|
import com.rosetta.messenger.ui.chats.RequestsListScreen
|
||||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
import com.rosetta.messenger.ui.chats.SearchScreen
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||||||
import com.rosetta.messenger.ui.components.SwipeBackContainer
|
import com.rosetta.messenger.ui.components.SwipeBackContainer
|
||||||
@@ -484,6 +485,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
*/
|
*/
|
||||||
sealed class Screen {
|
sealed class Screen {
|
||||||
data object Profile : Screen()
|
data object Profile : Screen()
|
||||||
|
data object Requests : Screen()
|
||||||
data object Search : Screen()
|
data object Search : Screen()
|
||||||
data class ChatDetail(val user: SearchUser) : Screen()
|
data class ChatDetail(val user: SearchUser) : Screen()
|
||||||
data class OtherProfile(val user: SearchUser) : Screen()
|
data class OtherProfile(val user: SearchUser) : Screen()
|
||||||
@@ -564,6 +566,7 @@ fun MainScreen(
|
|||||||
|
|
||||||
// Derived visibility — only triggers recomposition when THIS screen changes
|
// Derived visibility — only triggers recomposition when THIS screen changes
|
||||||
val isProfileVisible by remember { derivedStateOf { navStack.any { it is Screen.Profile } } }
|
val isProfileVisible by remember { derivedStateOf { navStack.any { it is Screen.Profile } } }
|
||||||
|
val isRequestsVisible by remember { derivedStateOf { navStack.any { it is Screen.Requests } } }
|
||||||
val isSearchVisible by remember { derivedStateOf { navStack.any { it is Screen.Search } } }
|
val isSearchVisible by remember { derivedStateOf { navStack.any { it is Screen.Search } } }
|
||||||
val chatDetailScreen by remember {
|
val chatDetailScreen by remember {
|
||||||
derivedStateOf { navStack.filterIsInstance<Screen.ChatDetail>().lastOrNull() }
|
derivedStateOf { navStack.filterIsInstance<Screen.ChatDetail>().lastOrNull() }
|
||||||
@@ -616,6 +619,8 @@ fun MainScreen(
|
|||||||
val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel =
|
val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel =
|
||||||
androidx.lifecycle.viewmodel.compose.viewModel()
|
androidx.lifecycle.viewmodel.compose.viewModel()
|
||||||
val profileState by profileViewModel.state.collectAsState()
|
val profileState by profileViewModel.state.collectAsState()
|
||||||
|
val chatsListViewModel: com.rosetta.messenger.ui.chats.ChatsListViewModel =
|
||||||
|
androidx.lifecycle.viewmodel.compose.viewModel()
|
||||||
|
|
||||||
// Appearance: background blur color preference
|
// Appearance: background blur color preference
|
||||||
val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
||||||
@@ -682,6 +687,7 @@ fun MainScreen(
|
|||||||
// TODO: Share invite link
|
// TODO: Share invite link
|
||||||
},
|
},
|
||||||
onSearchClick = { pushScreen(Screen.Search) },
|
onSearchClick = { pushScreen(Screen.Search) },
|
||||||
|
onRequestsClick = { pushScreen(Screen.Requests) },
|
||||||
onNewChat = {
|
onNewChat = {
|
||||||
// TODO: Show new chat screen
|
// TODO: Show new chat screen
|
||||||
},
|
},
|
||||||
@@ -693,10 +699,29 @@ fun MainScreen(
|
|||||||
onTogglePin = { opponentKey ->
|
onTogglePin = { opponentKey ->
|
||||||
mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) }
|
mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) }
|
||||||
},
|
},
|
||||||
|
chatsViewModel = chatsListViewModel,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
onLogout = onLogout
|
onLogout = onLogout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SwipeBackContainer(
|
||||||
|
isVisible = isRequestsVisible,
|
||||||
|
onBack = { navStack = navStack.filterNot { it is Screen.Requests } },
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
) {
|
||||||
|
RequestsListScreen(
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
chatsViewModel = chatsListViewModel,
|
||||||
|
onBack = { navStack = navStack.filterNot { it is Screen.Requests } },
|
||||||
|
onUserSelect = { selectedRequestUser ->
|
||||||
|
navStack =
|
||||||
|
navStack.filterNot {
|
||||||
|
it is Screen.Requests || it is Screen.ChatDetail
|
||||||
|
} + Screen.ChatDetail(selectedRequestUser)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Profile Screen — MUST be before sub-screens so it stays
|
// Profile Screen — MUST be before sub-screens so it stays
|
||||||
// visible beneath them during swipe-back animation
|
// visible beneath them during swipe-back animation
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.rosetta.messenger.data
|
package com.rosetta.messenger.data
|
||||||
|
|
||||||
|
import com.rosetta.messenger.network.MessageAttachment
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -27,7 +28,8 @@ object ForwardManager {
|
|||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val isOutgoing: Boolean,
|
val isOutgoing: Boolean,
|
||||||
val senderPublicKey: String, // publicKey отправителя сообщения
|
val senderPublicKey: String, // publicKey отправителя сообщения
|
||||||
val originalChatPublicKey: String // publicKey чата откуда пересылается
|
val originalChatPublicKey: String, // publicKey чата откуда пересылается
|
||||||
|
val attachments: List<MessageAttachment> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сообщения для пересылки
|
// Сообщения для пересылки
|
||||||
@@ -120,4 +122,22 @@ object ForwardManager {
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Атомарно получить forward-сообщения для конкретного чата и очистить pending state.
|
||||||
|
* Это повторяет desktop-подход "consume once" после перехода в целевой диалог.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun consumeForwardMessagesForChat(publicKey: String): List<ForwardMessage> {
|
||||||
|
val selectedKey = _selectedChatPublicKey.value
|
||||||
|
val pending = _forwardMessages.value
|
||||||
|
if (selectedKey != publicKey || pending.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
_forwardMessages.value = emptyList()
|
||||||
|
_selectedChatPublicKey.value = null
|
||||||
|
_showChatPicker.value = false
|
||||||
|
return pending
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1389,7 +1389,21 @@ fun ChatDetailScreen(
|
|||||||
else
|
else
|
||||||
user.publicKey,
|
user.publicKey,
|
||||||
originalChatPublicKey =
|
originalChatPublicKey =
|
||||||
user.publicKey
|
user.publicKey,
|
||||||
|
attachments =
|
||||||
|
msg.attachments
|
||||||
|
.filter {
|
||||||
|
it.type !=
|
||||||
|
AttachmentType
|
||||||
|
.MESSAGES
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
attachment ->
|
||||||
|
attachment.copy(
|
||||||
|
localUri =
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ForwardManager
|
ForwardManager
|
||||||
@@ -1797,6 +1811,8 @@ fun ChatDetailScreen(
|
|||||||
message,
|
message,
|
||||||
isDarkTheme =
|
isDarkTheme =
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
|
isSelectionMode =
|
||||||
|
isSelectionMode,
|
||||||
showTail =
|
showTail =
|
||||||
showTail,
|
showTail,
|
||||||
isGroupStart =
|
isGroupStart =
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
|
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
|
||||||
// Это важно для правильного отображения forward сообщений сразу
|
// Это важно для правильного отображения forward сообщений сразу
|
||||||
val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey)
|
val forwardMessages = ForwardManager.consumeForwardMessagesForChat(publicKey)
|
||||||
val hasForward = forwardMessages.isNotEmpty()
|
val hasForward = forwardMessages.isNotEmpty()
|
||||||
if (hasForward) {}
|
if (hasForward) {}
|
||||||
|
|
||||||
@@ -543,12 +543,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
text = fm.text,
|
text = fm.text,
|
||||||
timestamp = fm.timestamp,
|
timestamp = fm.timestamp,
|
||||||
isOutgoing = fm.isOutgoing,
|
isOutgoing = fm.isOutgoing,
|
||||||
publicKey = fm.senderPublicKey
|
publicKey = fm.senderPublicKey,
|
||||||
|
attachments = fm.attachments
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_isForwardMode.value = true
|
_isForwardMode.value = true
|
||||||
// Очищаем ForwardManager после применения
|
|
||||||
ForwardManager.clear()
|
|
||||||
} else {
|
} else {
|
||||||
// Сбрасываем forward state если нет forward сообщений
|
// Сбрасываем forward state если нет forward сообщений
|
||||||
_replyMessages.value = emptyList()
|
_replyMessages.value = emptyList()
|
||||||
@@ -1261,6 +1260,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val attType = AttachmentType.fromInt(attJson.optInt("type", 0))
|
val attType = AttachmentType.fromInt(attJson.optInt("type", 0))
|
||||||
val attPreview = attJson.optString("preview", "")
|
val attPreview = attJson.optString("preview", "")
|
||||||
val attBlob = attJson.optString("blob", "")
|
val attBlob = attJson.optString("blob", "")
|
||||||
|
val attWidth = attJson.optInt("width", 0)
|
||||||
|
val attHeight = attJson.optInt("height", 0)
|
||||||
|
|
||||||
if (attId.isNotEmpty()) {
|
if (attId.isNotEmpty()) {
|
||||||
replyAttachmentsFromJson.add(
|
replyAttachmentsFromJson.add(
|
||||||
@@ -1268,7 +1269,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
id = attId,
|
id = attId,
|
||||||
type = attType,
|
type = attType,
|
||||||
preview = attPreview,
|
preview = attPreview,
|
||||||
blob = attBlob
|
blob = attBlob,
|
||||||
|
width = attWidth,
|
||||||
|
height = attHeight
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1412,7 +1415,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
text = msg.text,
|
text = msg.text,
|
||||||
timestamp = msg.timestamp.time,
|
timestamp = msg.timestamp.time,
|
||||||
isOutgoing = msg.isOutgoing,
|
isOutgoing = msg.isOutgoing,
|
||||||
publicKey = if (msg.isOutgoing) sender else opponent
|
publicKey = if (msg.isOutgoing) sender else opponent,
|
||||||
|
attachments =
|
||||||
|
msg.attachments
|
||||||
|
.filter { it.type != AttachmentType.MESSAGES }
|
||||||
|
.map { it.copy(localUri = "") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_isForwardMode.value = true
|
_isForwardMode.value = true
|
||||||
@@ -1572,6 +1579,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
put("id", att.id)
|
put("id", att.id)
|
||||||
put("type", att.type.value)
|
put("type", att.type.value)
|
||||||
put("preview", att.preview)
|
put("preview", att.preview)
|
||||||
|
put("width", att.width)
|
||||||
|
put("height", att.height)
|
||||||
// Для IMAGE/FILE - blob не включаем (слишком большой)
|
// Для IMAGE/FILE - blob не включаем (слишком большой)
|
||||||
// Для MESSAGES - включаем blob
|
// Для MESSAGES - включаем blob
|
||||||
put(
|
put(
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ fun ChatsListScreen(
|
|||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onInviteFriendsClick: () -> Unit,
|
onInviteFriendsClick: () -> Unit,
|
||||||
onSearchClick: () -> Unit,
|
onSearchClick: () -> Unit,
|
||||||
|
onRequestsClick: () -> Unit = {},
|
||||||
onNewChat: () -> Unit,
|
onNewChat: () -> Unit,
|
||||||
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
|
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
|
||||||
backgroundBlurColorId: String = "avatar",
|
backgroundBlurColorId: String = "avatar",
|
||||||
@@ -429,7 +430,7 @@ fun ChatsListScreen(
|
|||||||
if (showRequestsScreen) return@pointerInput
|
if (showRequestsScreen) return@pointerInput
|
||||||
|
|
||||||
val velocityTracker = VelocityTracker()
|
val velocityTracker = VelocityTracker()
|
||||||
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.45f
|
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.8f
|
||||||
|
|
||||||
awaitEachGesture {
|
awaitEachGesture {
|
||||||
val down =
|
val down =
|
||||||
@@ -1062,10 +1063,31 @@ fun ChatsListScreen(
|
|||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = showRequestsScreen,
|
targetState = showRequestsScreen,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
fadeIn(
|
if (targetState) {
|
||||||
animationSpec = tween(200)
|
// Opening requests: slide in from right
|
||||||
) togetherWith
|
slideInHorizontally(
|
||||||
fadeOut(animationSpec = tween(150))
|
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
) { fullWidth -> fullWidth } + fadeIn(
|
||||||
|
animationSpec = tween(200)
|
||||||
|
) togetherWith
|
||||||
|
slideOutHorizontally(
|
||||||
|
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
) { fullWidth -> -fullWidth / 4 } + fadeOut(
|
||||||
|
animationSpec = tween(150)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Closing requests: slide out to right
|
||||||
|
slideInHorizontally(
|
||||||
|
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
) { fullWidth -> -fullWidth / 4 } + fadeIn(
|
||||||
|
animationSpec = tween(200)
|
||||||
|
) togetherWith
|
||||||
|
slideOutHorizontally(
|
||||||
|
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
) { fullWidth -> fullWidth } + fadeOut(
|
||||||
|
animationSpec = tween(150)
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
label = "RequestsTransition"
|
label = "RequestsTransition"
|
||||||
) { isRequestsScreen ->
|
) { isRequestsScreen ->
|
||||||
@@ -1163,11 +1185,12 @@ fun ChatsListScreen(
|
|||||||
RequestsSection(
|
RequestsSection(
|
||||||
count =
|
count =
|
||||||
requestsCount,
|
requestsCount,
|
||||||
|
requests =
|
||||||
|
requests,
|
||||||
isDarkTheme =
|
isDarkTheme =
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
onClick = {
|
onClick = {
|
||||||
showRequestsScreen =
|
onRequestsClick()
|
||||||
true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Divider(
|
Divider(
|
||||||
@@ -2015,8 +2038,14 @@ fun SwipeableDialogItem(
|
|||||||
}
|
}
|
||||||
?: break
|
?: break
|
||||||
if (change.changedToUpIgnoreConsumed()
|
if (change.changedToUpIgnoreConsumed()
|
||||||
)
|
) {
|
||||||
|
// Tap detected — finger went up before touchSlop
|
||||||
|
if (!passedSlop) {
|
||||||
|
change.consume()
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
val delta = change.positionChange()
|
val delta = change.positionChange()
|
||||||
totalDragX += delta.x
|
totalDragX += delta.x
|
||||||
@@ -2155,7 +2184,7 @@ fun SwipeableDialogItem(
|
|||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
isBlocked = isBlocked,
|
isBlocked = isBlocked,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
onClick = onClick
|
onClick = null // Tap handled by parent pointerInput
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сепаратор внутри контента
|
// Сепаратор внутри контента
|
||||||
@@ -2178,7 +2207,7 @@ fun DialogItemContent(
|
|||||||
isPinned: Boolean = false,
|
isPinned: Boolean = false,
|
||||||
isBlocked: Boolean = false,
|
isBlocked: Boolean = false,
|
||||||
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||||
onClick: () -> Unit
|
onClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
||||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||||
@@ -2257,7 +2286,10 @@ fun DialogItemContent(
|
|||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.then(
|
||||||
|
if (onClick != null) Modifier.clickable(onClick = onClick)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -2656,33 +2688,141 @@ fun TypingIndicatorSmall() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 📬 Секция Requests - кнопка для перехода к списку запросов */
|
/** 📬 Секция Requests — Telegram-style chat item (как Archived Chats) */
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestsSection(count: Int, isDarkTheme: Boolean, onClick: () -> Unit) {
|
fun RequestsSection(
|
||||||
val textColor = if (isDarkTheme) Color(0xFF4DABF7) else Color(0xFF228BE6)
|
count: Int,
|
||||||
val arrowColor = if (isDarkTheme) Color(0xFFC9C9C9) else Color(0xFF228BE6)
|
requests: List<DialogUiModel> = emptyList(),
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||||
|
val secondaryTextColor =
|
||||||
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
|
||||||
|
val iconBgColor =
|
||||||
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFC7C7CC) }
|
||||||
|
|
||||||
|
// Последний запрос — показываем имя отправителя как subtitle
|
||||||
|
val lastRequest = remember(requests) { requests.firstOrNull() }
|
||||||
|
val subtitle = remember(lastRequest) {
|
||||||
|
when {
|
||||||
|
lastRequest == null -> ""
|
||||||
|
lastRequest.opponentTitle.isNotEmpty() &&
|
||||||
|
lastRequest.opponentTitle != lastRequest.opponentKey &&
|
||||||
|
lastRequest.opponentTitle != lastRequest.opponentKey.take(7) ->
|
||||||
|
lastRequest.opponentTitle
|
||||||
|
lastRequest.opponentUsername.isNotEmpty() ->
|
||||||
|
"@${lastRequest.opponentUsername}"
|
||||||
|
else -> lastRequest.opponentKey.take(7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
// Иконка — круглый аватар как в Telegram Archived Chats
|
||||||
text = "Requests +$count",
|
Box(
|
||||||
fontSize = 16.sp,
|
modifier =
|
||||||
fontWeight = FontWeight.SemiBold,
|
Modifier.size(56.dp)
|
||||||
color = textColor
|
.clip(CircleShape)
|
||||||
)
|
.background(iconBgColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.MailForward,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Icon(
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
imageVector = TablerIcons.ChevronRight,
|
|
||||||
contentDescription = "Open requests",
|
// Текст: название + последний отправитель
|
||||||
tint = arrowColor.copy(alpha = 0.6f),
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
modifier = Modifier.size(24.dp)
|
Row(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Requests",
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitle.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Badge с количеством
|
||||||
|
if (count > 0) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||||
|
.clip(RoundedCornerShape(11.dp))
|
||||||
|
.background(iconBgColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (count > 99) "99+" else count.toString(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (count > 0) {
|
||||||
|
// Если нет subtitle но есть count
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||||
|
.clip(RoundedCornerShape(11.dp))
|
||||||
|
.background(iconBgColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (count > 99) "99+" else count.toString(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.rosetta.messenger.ui.chats
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rosetta.messenger.network.SearchUser
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import compose.icons.TablerIcons
|
||||||
|
import compose.icons.tablericons.ArrowLeft
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RequestsListScreen(
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
chatsViewModel: ChatsListViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onUserSelect: (SearchUser) -> Unit
|
||||||
|
) {
|
||||||
|
val chatsState by chatsViewModel.chatsState.collectAsState()
|
||||||
|
val requests = chatsState.requests
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.ArrowLeft,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = PrimaryBlue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Requests",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = backgroundColor,
|
||||||
|
scrolledContainerColor = backgroundColor,
|
||||||
|
navigationIconContentColor = textColor,
|
||||||
|
titleContentColor = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = backgroundColor
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
RequestsScreen(
|
||||||
|
requests = requests,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
onBack = onBack,
|
||||||
|
onRequestClick = { request ->
|
||||||
|
onUserSelect(chatsViewModel.dialogToSearchUser(request))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -240,6 +240,7 @@ fun TypingIndicator(isDarkTheme: Boolean) {
|
|||||||
fun MessageBubble(
|
fun MessageBubble(
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
|
isSelectionMode: Boolean = false,
|
||||||
showTail: Boolean = true,
|
showTail: Boolean = true,
|
||||||
isGroupStart: Boolean = false,
|
isGroupStart: Boolean = false,
|
||||||
isSelected: Boolean = false,
|
isSelected: Boolean = false,
|
||||||
@@ -307,6 +308,8 @@ fun MessageBubble(
|
|||||||
if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне
|
if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне
|
||||||
else Color(0xFF2196F3) // Стандартный Material Blue для входящих
|
else Color(0xFF2196F3) // Стандартный Material Blue для входящих
|
||||||
}
|
}
|
||||||
|
val linksEnabled = !isSelectionMode
|
||||||
|
val textClickHandler: (() -> Unit)? = if (isSelectionMode) onClick else null
|
||||||
|
|
||||||
val timeColor =
|
val timeColor =
|
||||||
remember(message.isOutgoing, isDarkTheme) {
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
@@ -705,6 +708,10 @@ fun MessageBubble(
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
linkColor =
|
linkColor =
|
||||||
linkColor,
|
linkColor,
|
||||||
|
enableLinks =
|
||||||
|
linksEnabled,
|
||||||
|
onClick =
|
||||||
|
textClickHandler,
|
||||||
onLongClick =
|
onLongClick =
|
||||||
onLongClick // 🔥 Long press для selection
|
onLongClick // 🔥 Long press для selection
|
||||||
)
|
)
|
||||||
@@ -786,6 +793,8 @@ fun MessageBubble(
|
|||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
|
enableLinks = linksEnabled,
|
||||||
|
onClick = textClickHandler,
|
||||||
onLongClick =
|
onLongClick =
|
||||||
onLongClick // 🔥
|
onLongClick // 🔥
|
||||||
// Long
|
// Long
|
||||||
@@ -861,6 +870,8 @@ fun MessageBubble(
|
|||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
|
enableLinks = linksEnabled,
|
||||||
|
onClick = textClickHandler,
|
||||||
onLongClick =
|
onLongClick =
|
||||||
onLongClick // 🔥
|
onLongClick // 🔥
|
||||||
// Long
|
// Long
|
||||||
|
|||||||
@@ -399,7 +399,9 @@ fun MessageInputBar(
|
|||||||
ReplyImagePreview(
|
ReplyImagePreview(
|
||||||
attachment = imageAttachment,
|
attachment = imageAttachment,
|
||||||
modifier = Modifier.size(36.dp),
|
modifier = Modifier.size(36.dp),
|
||||||
senderPublicKey = if (msg.isOutgoing) myPublicKey else opponentPublicKey,
|
senderPublicKey = msg.publicKey.ifEmpty {
|
||||||
|
if (msg.isOutgoing) myPublicKey else opponentPublicKey
|
||||||
|
},
|
||||||
recipientPrivateKey = myPrivateKey
|
recipientPrivateKey = myPrivateKey
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ fun AppleEmojiText(
|
|||||||
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
|
linkColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color(0xFF54A9EB), // Telegram-style blue
|
||||||
enableLinks: Boolean = true, // 🔥 Включить кликабельные ссылки
|
enableLinks: Boolean = true, // 🔥 Включить кликабельные ссылки
|
||||||
|
onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble)
|
||||||
onLongClick: (() -> Unit)? = null // 🔥 Callback для long press (selection в MessageBubble)
|
onLongClick: (() -> Unit)? = null // 🔥 Callback для long press (selection в MessageBubble)
|
||||||
) {
|
) {
|
||||||
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
||||||
@@ -370,9 +371,12 @@ fun AppleEmojiText(
|
|||||||
setLinkColor(linkColor.toArgb())
|
setLinkColor(linkColor.toArgb())
|
||||||
enableClickableLinks(true, onLongClick)
|
enableClickableLinks(true, onLongClick)
|
||||||
} else {
|
} else {
|
||||||
// 🔥 Даже без ссылок поддерживаем long press
|
// 🔥 ВАЖНО: в selection mode полностью отключаем LinkMovementMethod,
|
||||||
onLongClickCallback = onLongClick
|
// иначе тапы по тексту могут "съедаться" и не доходить до onClick.
|
||||||
|
enableClickableLinks(false, onLongClick)
|
||||||
}
|
}
|
||||||
|
// 🔥 Поддержка обычного tap (например, для selection mode)
|
||||||
|
setOnClickListener(onClick?.let { click -> View.OnClickListener { click.invoke() } })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
@@ -389,9 +393,12 @@ fun AppleEmojiText(
|
|||||||
view.setLinkColor(linkColor.toArgb())
|
view.setLinkColor(linkColor.toArgb())
|
||||||
view.enableClickableLinks(true, onLongClick)
|
view.enableClickableLinks(true, onLongClick)
|
||||||
} else {
|
} else {
|
||||||
// 🔥 Даже без ссылок поддерживаем long press
|
// 🔥 ВАЖНО: в selection mode полностью отключаем LinkMovementMethod,
|
||||||
view.onLongClickCallback = onLongClick
|
// иначе тапы по тексту могут "съедаться" и не доходить до onClick.
|
||||||
|
view.enableClickableLinks(false, onLongClick)
|
||||||
}
|
}
|
||||||
|
// 🔥 Обновляем tap callback, чтобы не было stale lambda
|
||||||
|
view.setOnClickListener(onClick?.let { click -> View.OnClickListener { click.invoke() } })
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,7 +152,10 @@ private fun isCenteredTopCutout(
|
|||||||
if (notchInfo == null || notchInfo.bounds.width() <= 0f || notchInfo.bounds.height() <= 0f) {
|
if (notchInfo == null || notchInfo.bounds.width() <= 0f || notchInfo.bounds.height() <= 0f) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val tolerancePx = screenWidthPx * 0.20f
|
if (notchInfo.gravity != Gravity.CENTER) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val tolerancePx = screenWidthPx * 0.10f
|
||||||
return abs(notchInfo.bounds.centerX() - screenWidthPx / 2f) <= tolerancePx
|
return abs(notchInfo.bounds.centerX() - screenWidthPx / 2f) <= tolerancePx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +214,6 @@ 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
|
||||||
@@ -265,21 +267,8 @@ private fun computeAvatarState(
|
|||||||
val isDrawing = radius <= dp40
|
val isDrawing = radius <= dp40
|
||||||
val isNear = radius <= dp32
|
val isNear = radius <= dp32
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// Always lock X to screen center to avoid OEM cutout offset issues on some devices.
|
||||||
// CENTER X - animate towards notch/camera position when collapsing
|
val centerX: Float = screenWidthPx / 2f
|
||||||
// Normal: screen center, Collapsed: notch center (front camera)
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
|
||||||
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)
|
||||||
@@ -450,17 +439,8 @@ fun ProfileMetaballOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notch center position - ONLY use if notch is centered (like front camera)
|
// Always use true screen center for X target to keep droplet perfectly centered.
|
||||||
// If notch is off-center (corner notch), use screen center instead
|
val notchCenterX = screenWidthPx / 2f
|
||||||
val notchCenterX = remember(notchInfo, screenWidthPx, hasCenteredNotch) {
|
|
||||||
if (hasCenteredNotch && notchInfo != null) {
|
|
||||||
// Centered notch (like Dynamic Island or punch-hole camera)
|
|
||||||
notchInfo.bounds.centerX()
|
|
||||||
} else {
|
|
||||||
// No notch or off-center notch - always use screen center
|
|
||||||
screenWidthPx / 2f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val notchCenterY = remember(notchInfo, hasCenteredNotch, statusBarHeightPx) {
|
val notchCenterY = remember(notchInfo, hasCenteredNotch, statusBarHeightPx) {
|
||||||
resolveSafeNotchCenterY(
|
resolveSafeNotchCenterY(
|
||||||
@@ -493,7 +473,6 @@ 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,
|
||||||
@@ -596,19 +575,13 @@ fun ProfileMetaballOverlay(
|
|||||||
// Draw target shape at top (notch or black bar fallback)
|
// Draw target shape at top (notch or black bar fallback)
|
||||||
if (showConnector) {
|
if (showConnector) {
|
||||||
blackPaint.alpha = connectorPaintAlpha
|
blackPaint.alpha = connectorPaintAlpha
|
||||||
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
|
if (hasRealNotch && notchInfo != null) {
|
||||||
nativeCanvas.drawCircle(
|
nativeCanvas.drawCircle(
|
||||||
notchCenterX,
|
notchCenterX,
|
||||||
notchCenterY,
|
notchCenterY,
|
||||||
notchRadiusPx,
|
notchRadiusPx,
|
||||||
blackPaint
|
blackPaint
|
||||||
)
|
)
|
||||||
} else if (hasRealNotch && notchInfo != null && notchInfo.isAccurate && notchInfo.path != null) {
|
|
||||||
nativeCanvas.drawPath(notchInfo.path, blackPaint)
|
|
||||||
} else if (hasRealNotch && notchInfo != null) {
|
|
||||||
val bounds = notchInfo.bounds
|
|
||||||
val rad = kotlin.math.max(bounds.width(), bounds.height()) / 2f
|
|
||||||
nativeCanvas.drawRoundRect(bounds, rad, rad, blackPaint)
|
|
||||||
} else {
|
} else {
|
||||||
// No notch fallback: full-width black bar at top
|
// No notch fallback: full-width black bar at top
|
||||||
// Like Telegram's ProfileGooeyView when notchInfo == null
|
// Like Telegram's ProfileGooeyView when notchInfo == null
|
||||||
@@ -798,7 +771,6 @@ 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,
|
||||||
@@ -943,9 +915,8 @@ fun ProfileMetaballOverlayCpu(
|
|||||||
with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
|
with(density) { ProfileMetaballConstants.FALLBACK_CAMERA_SIZE.toPx() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val notchCenterX = remember(notchInfo, screenWidthPx) {
|
// Always use true screen center for X target to keep droplet perfectly centered.
|
||||||
if (hasRealNotch && notchInfo != null) notchInfo.bounds.centerX() else screenWidthPx / 2f
|
val notchCenterX = screenWidthPx / 2f
|
||||||
}
|
|
||||||
val notchCenterY = remember(notchInfo, hasRealNotch, statusBarHeightPx, blackBarHeightPx) {
|
val notchCenterY = remember(notchInfo, hasRealNotch, statusBarHeightPx, blackBarHeightPx) {
|
||||||
resolveSafeNotchCenterY(
|
resolveSafeNotchCenterY(
|
||||||
notchInfo = notchInfo,
|
notchInfo = notchInfo,
|
||||||
@@ -973,7 +944,6 @@ fun ProfileMetaballOverlayCpu(
|
|||||||
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
avatarSizeExpandedPx = avatarSizeExpandedPx,
|
||||||
avatarSizeMinPx = avatarSizeMinPx,
|
avatarSizeMinPx = avatarSizeMinPx,
|
||||||
hasAvatar = hasAvatar,
|
hasAvatar = hasAvatar,
|
||||||
notchCenterX = notchCenterX,
|
|
||||||
notchCenterY = notchCenterY,
|
notchCenterY = notchCenterY,
|
||||||
notchRadiusPx = notchRadiusPx,
|
notchRadiusPx = notchRadiusPx,
|
||||||
dp40 = dp40, dp34 = dp34, dp32 = dp32, dp18 = dp18, dp22 = dp22,
|
dp40 = dp40, dp34 = dp34, dp32 = dp32, dp18 = dp18, dp22 = dp22,
|
||||||
@@ -1081,19 +1051,13 @@ fun ProfileMetaballOverlayCpu(
|
|||||||
// Draw target (notch or black bar)
|
// Draw target (notch or black bar)
|
||||||
if (showConnector) {
|
if (showConnector) {
|
||||||
blackPaint.alpha = connectorPaintAlpha
|
blackPaint.alpha = connectorPaintAlpha
|
||||||
if (hasRealNotch && notchInfo != null && notchInfo.isLikelyCircle) {
|
if (hasRealNotch && notchInfo != null) {
|
||||||
val rad = min(notchInfo.bounds.width(), notchInfo.bounds.height()) / 2f
|
|
||||||
offscreenCanvas.drawCircle(
|
offscreenCanvas.drawCircle(
|
||||||
notchInfo.bounds.centerX(),
|
notchCenterX,
|
||||||
notchInfo.bounds.bottom - notchInfo.bounds.width() / 2f,
|
notchCenterY,
|
||||||
rad, blackPaint
|
notchRadiusPx,
|
||||||
|
blackPaint
|
||||||
)
|
)
|
||||||
} else if (hasRealNotch && notchInfo != null && notchInfo.isAccurate && notchInfo.path != null) {
|
|
||||||
offscreenCanvas.drawPath(notchInfo.path, blackPaint)
|
|
||||||
} else if (hasRealNotch && notchInfo != null) {
|
|
||||||
val bounds = notchInfo.bounds
|
|
||||||
val rad = kotlin.math.max(bounds.width(), bounds.height()) / 2f
|
|
||||||
offscreenCanvas.drawRoundRect(bounds, rad, rad, blackPaint)
|
|
||||||
} else {
|
} else {
|
||||||
// No notch: draw black bar
|
// No notch: draw black bar
|
||||||
offscreenCanvas.drawRect(0f, 0f, screenWidthPx, blackBarHeightPx, blackPaint)
|
offscreenCanvas.drawRect(0f, 0f, screenWidthPx, blackBarHeightPx, blackPaint)
|
||||||
|
|||||||
@@ -10,18 +10,22 @@ import androidx.compose.material3.*
|
|||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rosetta.messenger.BuildConfig
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdatesScreen(
|
fun UpdatesScreen(
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val versionName = remember { BuildConfig.VERSION_NAME }
|
||||||
|
val buildNumber = remember { BuildConfig.VERSION_CODE.toString() }
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||||
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -113,7 +117,7 @@ fun UpdatesScreen(
|
|||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "1.0.0",
|
text = versionName,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = secondaryTextColor
|
color = secondaryTextColor
|
||||||
@@ -132,7 +136,7 @@ fun UpdatesScreen(
|
|||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "100",
|
text = buildNumber,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = secondaryTextColor
|
color = secondaryTextColor
|
||||||
|
|||||||
Reference in New Issue
Block a user