refactor: Clean up imports and improve code formatting in OtherProfileScreen

This commit is contained in:
k1ngsterr1
2026-01-31 04:09:28 +05:00
parent 3ed4986393
commit c6f1998dc9

View File

@@ -1,32 +1,21 @@
package com.rosetta.messenger.ui.settings package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Block
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.ui.chats.ChatViewModel
import com.rosetta.messenger.ui.chats.ChatsListViewModel
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.components.VerifiedBadge
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
@@ -41,17 +30,22 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
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 androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.ChatViewModel
import com.rosetta.messenger.ui.chats.ChatsListViewModel
import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.BlurredAvatarBackground import com.rosetta.messenger.ui.components.BlurredAvatarBackground
import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
// Collapsing header constants // Collapsing header constants
private val EXPANDED_HEADER_HEIGHT_OTHER = 280.dp private val EXPANDED_HEADER_HEIGHT_OTHER = 280.dp
@@ -62,10 +56,10 @@ private val AVATAR_SIZE_COLLAPSED_OTHER = 36.dp
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun OtherProfileScreen( fun OtherProfileScreen(
user: SearchUser, user: SearchUser,
isDarkTheme: Boolean, isDarkTheme: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
avatarRepository: AvatarRepository? = null avatarRepository: AvatarRepository? = null
) { ) {
var isBlocked by remember { mutableStateOf(false) } var isBlocked by remember { mutableStateOf(false) }
var showAvatarMenu by remember { mutableStateOf(false) } var showAvatarMenu by remember { mutableStateOf(false) }
@@ -75,91 +69,98 @@ fun OtherProfileScreen(
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme) val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
val context = LocalContext.current val context = LocalContext.current
// 🔥 Получаем тот же ChatViewModel что и в ChatDetailScreen для очистки истории // 🔥 Получаем тот же ChatViewModel что и в ChatDetailScreen для очистки истории
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}") val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
// 🔥 ChatsListViewModel для блокировки/разблокировки // 🔥 ChatsListViewModel для блокировки/разблокировки
val chatsListViewModel: ChatsListViewModel = viewModel() val chatsListViewModel: ChatsListViewModel = viewModel()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
// 🔥 Загружаем статус блокировки при открытии экрана // 🔥 Загружаем статус блокировки при открытии экрана
LaunchedEffect(user.publicKey) { LaunchedEffect(user.publicKey) { isBlocked = chatsListViewModel.isUserBlocked(user.publicKey) }
isBlocked = chatsListViewModel.isUserBlocked(user.publicKey)
}
// 🎹 Для закрытия клавиатуры // 🎹 Для закрытия клавиатуры
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
// 🗑️ Для удаления диалога // 🗑️ Для удаления диалога
val database = remember { RosettaDatabase.getDatabase(context) } val database = remember { RosettaDatabase.getDatabase(context) }
// 🔥 Закрываем клавиатуру при открытии экрана // 🔥 Закрываем клавиатуру при открытии экрана
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
} }
// <20>🟢 Наблюдаем за онлайн статусом пользователя в реальном времени // <20>🟢 Наблюдаем за онлайн статусом пользователя в реальном времени
val messageRepository = remember { MessageRepository.getInstance(context) } val messageRepository = remember { MessageRepository.getInstance(context) }
val onlineStatus by messageRepository.observeUserOnlineStatus(user.publicKey) val onlineStatus by
.collectAsState(initial = false to 0L) messageRepository
.observeUserOnlineStatus(user.publicKey)
.collectAsState(initial = false to 0L)
val isOnline = onlineStatus.first val isOnline = onlineStatus.first
val lastSeen = onlineStatus.second val lastSeen = onlineStatus.second
// Scroll state for collapsing header animation // Scroll state for collapsing header animation
val density = LocalDensity.current val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() } val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() }
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() } val collapsedHeightPx =
with(density) { (COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() }
var scrollOffset by remember { mutableFloatStateOf(0f) } var scrollOffset by remember { mutableFloatStateOf(0f) }
val maxScrollOffset = expandedHeightPx - collapsedHeightPx val maxScrollOffset = expandedHeightPx - collapsedHeightPx
val collapseProgress by remember { val collapseProgress by remember {
derivedStateOf { derivedStateOf { (scrollOffset / maxScrollOffset).coerceIn(0f, 1f) }
(scrollOffset / maxScrollOffset).coerceIn(0f, 1f)
}
} }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y val delta = available.y
val newOffset = scrollOffset - delta val newOffset = scrollOffset - delta
val consumed = when { val consumed =
delta < 0 && scrollOffset < maxScrollOffset -> { when {
val consumed = (newOffset.coerceIn(0f, maxScrollOffset) - scrollOffset) delta < 0 && scrollOffset < maxScrollOffset -> {
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset) val consumed =
-consumed (newOffset.coerceIn(0f, maxScrollOffset) - scrollOffset)
} scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
delta > 0 && scrollOffset > 0 -> { -consumed
val consumed = scrollOffset - newOffset.coerceIn(0f, maxScrollOffset) }
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset) delta > 0 && scrollOffset > 0 -> {
consumed val consumed =
} scrollOffset - newOffset.coerceIn(0f, maxScrollOffset)
else -> 0f scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
} consumed
}
else -> 0f
}
return Offset(0f, consumed) return Offset(0f, consumed)
} }
} }
} }
// Handle back gesture // Handle back gesture
BackHandler { onBack() } BackHandler { onBack() }
Box( Box(
modifier = Modifier modifier =
.fillMaxSize() Modifier.fillMaxSize()
.background(backgroundColor) .background(backgroundColor)
.nestedScroll(nestedScrollConnection) .nestedScroll(nestedScrollConnection)
) { ) {
// Scrollable content // Scrollable content
LazyColumn( LazyColumn(
modifier = Modifier modifier =
.fillMaxSize() Modifier.fillMaxSize()
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() }) .padding(
top =
with(density) {
(expandedHeightPx - scrollOffset).toDp()
}
)
) { ) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -169,25 +170,25 @@ fun OtherProfileScreen(
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (user.username.isNotBlank()) { if (user.username.isNotBlank()) {
TelegramSectionTitle(title = "Info", isDarkTheme = isDarkTheme) TelegramSectionTitle(title = "Info", isDarkTheme = isDarkTheme)
TelegramCopyField( TelegramCopyField(
value = "@${user.username}", value = "@${user.username}",
fullValue = user.username, fullValue = user.username,
label = "Username", label = "Username",
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
} }
// Public Key Section // Public Key Section
TelegramSectionTitle(title = "Public Key", isDarkTheme = isDarkTheme) TelegramSectionTitle(title = "Public Key", isDarkTheme = isDarkTheme)
TelegramCopyField( TelegramCopyField(
value = user.publicKey.take(16) + "..." + user.publicKey.takeLast(6), value = user.publicKey.take(16) + "..." + user.publicKey.takeLast(6),
fullValue = user.publicKey, fullValue = user.publicKey,
label = "Public Key", label = "Public Key",
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@@ -198,43 +199,43 @@ fun OtherProfileScreen(
// 🎨 COLLAPSING HEADER // 🎨 COLLAPSING HEADER
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
CollapsingOtherProfileHeader( CollapsingOtherProfileHeader(
name = user.title.ifEmpty { "Unknown User" }, name = user.title.ifEmpty { "Unknown User" },
username = user.username, username = user.username,
publicKey = user.publicKey, publicKey = user.publicKey,
verified = user.verified, verified = user.verified,
isOnline = isOnline, isOnline = isOnline,
lastSeen = lastSeen, lastSeen = lastSeen,
avatarColors = avatarColors, avatarColors = avatarColors,
collapseProgress = collapseProgress, collapseProgress = collapseProgress,
onBack = onBack, onBack = onBack,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
showAvatarMenu = showAvatarMenu, showAvatarMenu = showAvatarMenu,
onAvatarMenuChange = { showAvatarMenu = it }, onAvatarMenuChange = { showAvatarMenu = it },
isBlocked = isBlocked, isBlocked = isBlocked,
onBlockToggle = { onBlockToggle = {
coroutineScope.launch { coroutineScope.launch {
if (isBlocked) { if (isBlocked) {
chatsListViewModel.unblockUser(user.publicKey) chatsListViewModel.unblockUser(user.publicKey)
} else { } else {
chatsListViewModel.blockUser(user.publicKey) chatsListViewModel.blockUser(user.publicKey)
}
// Обновляем локальное состояние
isBlocked = !isBlocked
} }
// Обновляем локальное состояние },
isBlocked = !isBlocked avatarRepository = avatarRepository,
} onClearChat = {
}, viewModel.clearChatHistory()
avatarRepository = avatarRepository, // 🗑️ Удаляем диалог из списка после очистки истории
onClearChat = { CoroutineScope(Dispatchers.IO).launch {
viewModel.clearChatHistory() try {
// 🗑️ Удаляем диалог из списка после очистки истории val account = viewModel.myPublicKey ?: return@launch
CoroutineScope(Dispatchers.IO).launch { database.dialogDao().deleteDialog(account, user.publicKey)
try { } catch (e: Exception) {
val account = viewModel.myPublicKey ?: return@launch android.util.Log.e("OtherProfileScreen", "Failed to delete dialog", e)
database.dialogDao().deleteDialog(account, user.publicKey) }
} catch (e: Exception) {
android.util.Log.e("OtherProfileScreen", "Failed to delete dialog", e)
} }
} }
}
) )
} }
} }
@@ -243,205 +244,207 @@ fun OtherProfileScreen(
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
@Composable @Composable
private fun CollapsingOtherProfileHeader( private fun CollapsingOtherProfileHeader(
name: String, name: String,
username: String, username: String,
publicKey: String, publicKey: String,
verified: Int, verified: Int,
isOnline: Boolean, isOnline: Boolean,
lastSeen: Long, lastSeen: Long,
avatarColors: AvatarColors, avatarColors: AvatarColors,
collapseProgress: Float, collapseProgress: Float,
onBack: () -> Unit, onBack: () -> Unit,
isDarkTheme: Boolean, isDarkTheme: Boolean,
showAvatarMenu: Boolean, showAvatarMenu: Boolean,
onAvatarMenuChange: (Boolean) -> Unit, onAvatarMenuChange: (Boolean) -> Unit,
isBlocked: Boolean, isBlocked: Boolean,
onBlockToggle: () -> Unit, onBlockToggle: () -> Unit,
avatarRepository: AvatarRepository? = null, avatarRepository: AvatarRepository? = null,
onClearChat: () -> Unit onClearChat: () -> Unit
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp.dp val screenWidthDp = configuration.screenWidthDp.dp
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val expandedHeight = EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight val expandedHeight = EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight
val collapsedHeight = COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight val collapsedHeight = COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight
val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress) val headerHeight =
androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
// Avatar animation // Avatar animation
val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED_OTHER) / 2 val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED_OTHER) / 2
val avatarStartY = statusBarHeight + 32.dp val avatarStartY = statusBarHeight + 32.dp
val avatarEndY = statusBarHeight - 60.dp val avatarEndY = statusBarHeight - 60.dp
val avatarY = androidx.compose.ui.unit.lerp(avatarStartY, avatarEndY, collapseProgress) val avatarY = androidx.compose.ui.unit.lerp(avatarStartY, avatarEndY, collapseProgress)
val avatarSize = androidx.compose.ui.unit.lerp(AVATAR_SIZE_EXPANDED_OTHER, 0.dp, collapseProgress) val avatarSize =
androidx.compose.ui.unit.lerp(AVATAR_SIZE_EXPANDED_OTHER, 0.dp, collapseProgress)
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress) val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
// Text animation - always centered // Text animation - always centered
val textExpandedY = statusBarHeight + 32.dp + AVATAR_SIZE_EXPANDED_OTHER + 48.dp val textExpandedY = statusBarHeight + 32.dp + AVATAR_SIZE_EXPANDED_OTHER + 48.dp
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT_OTHER / 2 val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT_OTHER / 2
val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseProgress) val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseProgress)
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress) val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress) val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
Box( Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
modifier = Modifier
.fillMaxWidth()
.height(headerHeight)
) {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🎨 BLURRED AVATAR BACKGROUND // 🎨 BLURRED AVATAR BACKGROUND
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
BlurredAvatarBackground( BlurredAvatarBackground(
publicKey = publicKey, publicKey = publicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
fallbackColor = avatarColors.backgroundColor, fallbackColor = avatarColors.backgroundColor,
blurRadius = 25f, blurRadius = 25f,
alpha = 0.3f alpha = 0.3f
) )
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🔙 BACK BUTTON // 🔙 BACK BUTTON
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Box( Box(
modifier = Modifier modifier =
.padding(top = statusBarHeight) Modifier.padding(top = statusBarHeight)
.padding(start = 4.dp, top = 4.dp) .padding(start = 4.dp, top = 4.dp)
.size(48.dp), .size(48.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
IconButton( IconButton(onClick = onBack, modifier = Modifier.size(48.dp)) {
onClick = onBack,
modifier = Modifier.size(48.dp)
) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
tint = if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White, tint = if (isDarkTheme) Color.White else Color(0xFF007AFF),
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// ⋮ MENU BUTTON (top right corner) // ⋮ MENU BUTTON (top right corner)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Box( Box(
modifier = Modifier modifier =
.align(Alignment.TopEnd) Modifier.align(Alignment.TopEnd)
.padding(top = statusBarHeight) .padding(top = statusBarHeight)
.padding(end = 4.dp, top = 4.dp) .padding(end = 4.dp, top = 4.dp)
.size(48.dp), .size(48.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
IconButton( IconButton(onClick = { onAvatarMenuChange(true) }, modifier = Modifier.size(48.dp)) {
onClick = { onAvatarMenuChange(true) },
modifier = Modifier.size(48.dp)
) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "Profile menu", contentDescription = "Profile menu",
tint = if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White, tint =
modifier = Modifier.size(24.dp) if (isColorLight(avatarColors.backgroundColor)) Color.Black
else Color.White,
modifier = Modifier.size(24.dp)
) )
} }
// Меню с блокировкой пользователя // Меню с блокировкой пользователя
com.rosetta.messenger.ui.chats.components.OtherProfileMenu( com.rosetta.messenger.ui.chats.components.OtherProfileMenu(
expanded = showAvatarMenu, expanded = showAvatarMenu,
onDismiss = { onAvatarMenuChange(false) }, onDismiss = { onAvatarMenuChange(false) },
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
isBlocked = isBlocked, isBlocked = isBlocked,
onBlockClick = { onBlockClick = {
onAvatarMenuChange(false) onAvatarMenuChange(false)
onBlockToggle() onBlockToggle()
}, },
onClearChatClick = { onClearChatClick = {
onAvatarMenuChange(false) onAvatarMenuChange(false)
onClearChat() onClearChat()
} }
) )
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 👤 AVATAR - shrinks and moves up // 👤 AVATAR - shrinks and moves up
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (avatarSize > 1.dp) { if (avatarSize > 1.dp) {
Box( Box(
modifier = Modifier modifier =
.offset( Modifier.offset(
x = avatarCenterX + (AVATAR_SIZE_EXPANDED_OTHER - avatarSize) / 2, x =
y = avatarY avatarCenterX +
) (AVATAR_SIZE_EXPANDED_OTHER -
.size(avatarSize) avatarSize) / 2,
.clip(CircleShape) y = avatarY
.background(Color.White.copy(alpha = 0.15f)) )
.padding(2.dp) .size(avatarSize)
.clip(CircleShape), .clip(CircleShape)
contentAlignment = Alignment.Center .background(Color.White.copy(alpha = 0.15f))
.padding(2.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center
) { ) {
AvatarImage( AvatarImage(
publicKey = publicKey, publicKey = publicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
size = avatarSize - 4.dp, size = avatarSize - 4.dp,
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
} }
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 📝 TEXT BLOCK - Name + Verified + Online, always centered // 📝 TEXT BLOCK - Name + Verified + Online, always centered
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Column( Column(
modifier = Modifier modifier =
.align(Alignment.TopCenter) Modifier.align(Alignment.TopCenter).offset(y = textY).graphicsLayer {
.offset(y = textY) val centerOffsetY =
.graphicsLayer { with(density) {
val centerOffsetY = with(density) { androidx.compose
androidx.compose.ui.unit.lerp(24.dp, 18.dp, collapseProgress).toPx() .ui
} .unit
translationY = -centerOffsetY .lerp(24.dp, 18.dp, collapseProgress)
}, .toPx()
horizontalAlignment = Alignment.CenterHorizontally }
translationY = -centerOffsetY
},
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Name + Verified Badge // Name + Verified Badge
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = name, text = name,
fontSize = nameFontSize, fontSize = nameFontSize,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White, color =
maxLines = 1, if (isColorLight(avatarColors.backgroundColor)) Color.Black
overflow = TextOverflow.Ellipsis, else Color.White,
textAlign = TextAlign.Center maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
) )
if (verified > 0) { if (verified > 0) {
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
VerifiedBadge( VerifiedBadge(verified = verified, size = (nameFontSize.value * 0.8f).toInt())
verified = verified,
size = (nameFontSize.value * 0.8f).toInt()
)
} }
} }
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
// Online/Offline status // Online/Offline status
Text( Text(
text = if (isOnline) "online" else "offline", text = if (isOnline) "online" else "offline",
fontSize = onlineFontSize, fontSize = onlineFontSize,
color = if (isOnline) { color =
Color(0xFF4CAF50) if (isOnline) {
} else { Color(0xFF4CAF50)
if (isColorLight(avatarColors.backgroundColor)) Color.Black.copy(alpha = 0.6f) else Color.White.copy(alpha = 0.6f) } else {
} if (isColorLight(avatarColors.backgroundColor))
Color.Black.copy(alpha = 0.6f)
else Color.White.copy(alpha = 0.6f)
}
) )
} }
} }
@@ -451,44 +454,42 @@ private fun CollapsingOtherProfileHeader(
// 🚫 BLOCK/UNBLOCK ITEM // 🚫 BLOCK/UNBLOCK ITEM
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
@Composable @Composable
private fun TelegramBlockItem( private fun TelegramBlockItem(isBlocked: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) {
isBlocked: Boolean,
onToggle: () -> Unit,
isDarkTheme: Boolean
) {
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
val iconColor = PrimaryBlue val iconColor = PrimaryBlue
Row( Row(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.clickable(onClick = onToggle) .clickable(onClick = onToggle)
.padding(horizontal = 16.dp, vertical = 14.dp), .padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Block, imageVector = Icons.Outlined.Block,
contentDescription = null, contentDescription = null,
tint = iconColor, tint = iconColor,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = if (isBlocked) "Unblock User" else "Block User", text = if (isBlocked) "Unblock User" else "Block User",
fontSize = 16.sp, fontSize = 16.sp,
color = iconColor, color = iconColor,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = if (isBlocked) "Allow this user to message you" else "Prevent this user from messaging you", text =
fontSize = 13.sp, if (isBlocked) "Allow this user to message you"
color = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), else "Prevent this user from messaging you",
lineHeight = 16.sp fontSize = 13.sp,
color = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
lineHeight = 16.sp
) )
} }
} }
} }