feat: Implement collapsing header and block/unblock functionality in OtherProfileScreen

This commit is contained in:
k1ngsterr1
2026-01-22 16:30:24 +05:00
parent 68130948ba
commit 5161b5343a

View File

@@ -1,25 +1,46 @@
package com.rosetta.messenger.ui.settings
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.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.network.SearchUser
import kotlin.math.roundToInt
// Collapsing header constants
private val EXPANDED_HEADER_HEIGHT_OTHER = 280.dp
private val COLLAPSED_HEADER_HEIGHT_OTHER = 64.dp
private val AVATAR_SIZE_EXPANDED_OTHER = 120.dp
private val AVATAR_SIZE_COLLAPSED_OTHER = 36.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OtherProfileScreen(
user: SearchUser,
@@ -31,182 +52,313 @@ fun OtherProfileScreen(
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
val context = LocalContext.current
// Scroll state for collapsing header animation
val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() }
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight).toPx() }
var scrollOffset by remember { mutableFloatStateOf(0f) }
val maxScrollOffset = expandedHeightPx - collapsedHeightPx
val collapseProgress by remember {
derivedStateOf {
(scrollOffset / maxScrollOffset).coerceIn(0f, 1f)
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = scrollOffset - delta
val consumed = when {
delta < 0 && scrollOffset < maxScrollOffset -> {
val consumed = (newOffset.coerceIn(0f, maxScrollOffset) - scrollOffset)
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
-consumed
}
delta > 0 && scrollOffset > 0 -> {
val consumed = scrollOffset - newOffset.coerceIn(0f, maxScrollOffset)
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
consumed
}
else -> 0f
}
return Offset(0f, consumed)
}
}
}
// Handle back gesture
BackHandler { onBack() }
Column(
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.verticalScroll(rememberScrollState())
.nestedScroll(nestedScrollConnection)
) {
// Profile Card with avatar
ProfileCard(
name = user.title.ifEmpty { "Unknown User" },
username = user.username.ifEmpty { "" },
publicKey = user.publicKey,
isDarkTheme = isDarkTheme,
onBack = onBack,
hasChanges = false,
onSave = {}
)
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
// Username Section (if available)
if (user.username.isNotBlank()) {
Text(
text = "Username",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "@${user.username}",
fontSize = 16.sp,
color = textColor,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
}
Text(
text = "Username for search user or send message.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
// Scrollable content
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() })
) {
item {
Spacer(modifier = Modifier.height(16.dp))
}
// Public Key Section
Text(
text = "Public Key",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = user.publicKey.take(20) + "..." + user.publicKey.takeLast(20),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.weight(1f)
// ═══════════════════════════════════════════════════════════
// 📋 INFO SECTION
// ═══════════════════════════════════════════════════════════
if (user.username.isNotBlank()) {
TelegramSectionTitle(title = "Info", isDarkTheme = isDarkTheme)
TelegramCopyField(
value = "@${user.username}",
fullValue = user.username,
label = "Username",
isDarkTheme = isDarkTheme
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
Spacer(modifier = Modifier.height(24.dp))
}
// Public Key Section
TelegramSectionTitle(title = "Public Key", isDarkTheme = isDarkTheme)
TelegramCopyField(
value = user.publicKey.take(16) + "..." + user.publicKey.takeLast(6),
fullValue = user.publicKey,
label = "Public Key",
isDarkTheme = isDarkTheme
)
Spacer(modifier = Modifier.height(24.dp))
// ═══════════════════════════════════════════════════════════
// 🚫 BLOCK SECTION
// ═══════════════════════════════════════════════════════════
TelegramSectionTitle(title = "Privacy", isDarkTheme = isDarkTheme)
TelegramBlockItem(
isBlocked = isBlocked,
onToggle = { isBlocked = !isBlocked },
isDarkTheme = isDarkTheme
)
Spacer(modifier = Modifier.height(32.dp))
}
}
// ═══════════════════════════════════════════════════════════
// 🎨 COLLAPSING HEADER
// ═══════════════════════════════════════════════════════════
CollapsingOtherProfileHeader(
name = user.title.ifEmpty { "Unknown User" },
username = user.username,
publicKey = user.publicKey,
avatarColors = avatarColors,
collapseProgress = collapseProgress,
onBack = onBack,
isDarkTheme = isDarkTheme
)
}
}
// ═══════════════════════════════════════════════════════════
// 🎯 COLLAPSING HEADER FOR OTHER PROFILE
// ═══════════════════════════════════════════════════════════
@Composable
private fun CollapsingOtherProfileHeader(
name: String,
username: String,
publicKey: String,
avatarColors: AvatarColors,
collapseProgress: Float,
onBack: () -> Unit,
isDarkTheme: Boolean
) {
val density = LocalDensity.current
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp.dp
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val expandedHeight = EXPANDED_HEADER_HEIGHT_OTHER + statusBarHeight
val collapsedHeight = COLLAPSED_HEADER_HEIGHT_OTHER + statusBarHeight
val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
// Avatar animation
val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED_OTHER) / 2
val avatarStartY = statusBarHeight + 32.dp
val avatarEndY = statusBarHeight - 60.dp
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 avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
// Text animation
val textExpandedX = screenWidthDp / 2
val textCollapsedX = 48.dp + 12.dp
val textX = androidx.compose.ui.unit.lerp(textExpandedX, textCollapsedX, collapseProgress)
val textExpandedY = statusBarHeight + 32.dp + AVATAR_SIZE_EXPANDED_OTHER + 48.dp
val textCollapsedY = statusBarHeight + (COLLAPSED_HEADER_HEIGHT_OTHER - 40.dp) / 2
val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseProgress)
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 17.sp, collapseProgress)
val usernameFontSize = androidx.compose.ui.unit.lerp(15.sp, 13.sp, collapseProgress)
val headerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
Box(
modifier = Modifier
.fillMaxWidth()
.height(headerHeight)
.background(headerColor)
) {
// Background color
Box(
modifier = Modifier
.fillMaxSize()
.background(headerColor)
)
// Avatar
if (avatarSize > 0.dp) {
Box(
modifier = Modifier
.offset {
IntOffset(
with(density) { avatarCenterX.toPx().roundToInt() },
with(density) { avatarY.toPx().roundToInt() }
)
}
}
}
Text(
text = "This is user public key. If user haven't set a @username yet, you can send message using public key.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Block/Unblock Section
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
.size(avatarSize)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Column {
if (isBlocked) {
ProfileNavigationItem(
icon = Icons.Outlined.Block,
iconBackground = Color(0xFF2E7D32), // green
title = "Unblock User",
subtitle = "Allow this user to message you",
onClick = {
isBlocked = false
// TODO: Implement actual unblock logic
},
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFF2E7D32)
)
} else {
ProfileNavigationItem(
icon = Icons.Outlined.Block,
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
title = "Block this user",
subtitle = "Prevent this user from messaging you",
onClick = {
isBlocked = true
// TODO: Implement actual block logic
},
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
)
}
if (avatarFontSize.value > 0) {
Text(
text = getInitials(name),
fontSize = avatarFontSize,
fontWeight = FontWeight.Bold,
color = avatarColors.textColor
)
}
}
Text(
text = if (isBlocked) {
"If you want the user to be able to send you messages again, you can unblock them. You can block them later."
} else {
"The person will no longer be able to message you if you block them. You can unblock them later."
},
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
}
// Name and username
Box(
modifier = Modifier
.offset {
IntOffset(
with(density) { textX.toPx().roundToInt() },
with(density) { textY.toPx().roundToInt() }
)
}
.graphicsLayer {
transformOrigin = androidx.compose.ui.graphics.TransformOrigin(
pivotFractionX = if (collapseProgress > 0.5f) 0f else 0.5f,
pivotFractionY = 0.5f
)
}
) {
Column(
horizontalAlignment = if (collapseProgress > 0.5f) Alignment.Start else Alignment.CenterHorizontally
) {
Text(
text = name,
fontSize = nameFontSize,
fontWeight = FontWeight.Bold,
color = if (isDarkTheme) Color.White else Color.Black,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (username.isNotEmpty() && collapseProgress < 0.9f) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "@$username",
fontSize = usernameFontSize,
color = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.graphicsLayer {
alpha = 1f - (collapseProgress * 2f).coerceIn(0f, 1f)
}
)
}
}
}
// Back button
IconButton(
onClick = onBack,
modifier = Modifier
.padding(top = statusBarHeight)
.padding(start = 4.dp, top = 4.dp)
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}
// ═══════════════════════════════════════════════════════════
// 🚫 BLOCK/UNBLOCK ITEM
// ═══════════════════════════════════════════════════════════
@Composable
private fun TelegramBlockItem(
isBlocked: Boolean,
onToggle: () -> Unit,
isDarkTheme: Boolean
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val iconColor = if (isBlocked) {
if (isDarkTheme) Color(0xFF66BB6A) else Color(0xFF4CAF50)
} else {
if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onToggle)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Block,
contentDescription = null,
tint = iconColor,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(20.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (isBlocked) "Unblock User" else "Block User",
fontSize = 16.sp,
color = iconColor,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = if (isBlocked) "Allow this user to message you" else "Prevent this user from messaging you",
fontSize = 13.sp,
color = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
lineHeight = 16.sp
)
}
}
}