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 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.layout.* 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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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.outlined.Block import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rosetta.messenger.network.SearchUser 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 @Composable
fun OtherProfileScreen( fun OtherProfileScreen(
user: SearchUser, user: SearchUser,
@@ -31,182 +52,313 @@ fun OtherProfileScreen(
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
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 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 // Handle back gesture
BackHandler { onBack() } BackHandler { onBack() }
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(backgroundColor) .background(backgroundColor)
.verticalScroll(rememberScrollState()) .nestedScroll(nestedScrollConnection)
) { ) {
// Profile Card with avatar // Scrollable content
ProfileCard( LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() })
) {
item {
Spacer(modifier = Modifier.height(16.dp))
// ═══════════════════════════════════════════════════════════
// 📋 INFO SECTION
// ═══════════════════════════════════════════════════════════
if (user.username.isNotBlank()) {
TelegramSectionTitle(title = "Info", isDarkTheme = isDarkTheme)
TelegramCopyField(
value = "@${user.username}",
fullValue = user.username,
label = "Username",
isDarkTheme = isDarkTheme
)
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" }, name = user.title.ifEmpty { "Unknown User" },
username = user.username.ifEmpty { "" }, username = user.username,
publicKey = user.publicKey, publicKey = user.publicKey,
isDarkTheme = isDarkTheme, avatarColors = avatarColors,
collapseProgress = collapseProgress,
onBack = onBack, onBack = onBack,
hasChanges = false, isDarkTheme = isDarkTheme
onSave = {} )
}
}
// ═══════════════════════════════════════════════════════════
// 🎯 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)
) )
Spacer(modifier = Modifier.height(16.dp)) // Avatar
if (avatarSize > 0.dp) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) { Box(
// Username Section (if available) modifier = Modifier
if (user.username.isNotBlank()) { .offset {
Text( IntOffset(
text = "Username", with(density) { avatarCenterX.toPx().roundToInt() },
fontSize = 14.sp, with(density) { avatarY.toPx().roundToInt() }
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)
)
}
} }
} .size(avatarSize)
.clip(CircleShape)
Text( .background(avatarColors.backgroundColor),
text = "Username for search user or send message.", contentAlignment = Alignment.Center
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
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( if (avatarFontSize.value > 0) {
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = user.publicKey.take(20) + "..." + user.publicKey.takeLast(20), text = getInitials(name),
fontSize = 12.sp, fontSize = avatarFontSize,
color = textColor, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f) color = avatarColors.textColor
) )
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
} }
} }
}
Text( // Name and username
text = "This is user public key. If user haven't set a @username yet, you can send message using public key.", Box(
fontSize = 12.sp, modifier = Modifier
color = secondaryTextColor, .offset {
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp), IntOffset(
lineHeight = 16.sp with(density) { textX.toPx().roundToInt() },
) with(density) { textY.toPx().roundToInt() }
)
Spacer(modifier = Modifier.height(24.dp)) }
.graphicsLayer {
// Block/Unblock Section transformOrigin = androidx.compose.ui.graphics.TransformOrigin(
Surface( pivotFractionX = if (collapseProgress > 0.5f) 0f else 0.5f,
modifier = Modifier.fillMaxWidth(), pivotFractionY = 0.5f
color = surfaceColor, )
shape = RoundedCornerShape(16.dp) }
) {
Column(
horizontalAlignment = if (collapseProgress > 0.5f) Alignment.Start else Alignment.CenterHorizontally
) { ) {
Column { Text(
if (isBlocked) { text = name,
ProfileNavigationItem( fontSize = nameFontSize,
icon = Icons.Outlined.Block, fontWeight = FontWeight.Bold,
iconBackground = Color(0xFF2E7D32), // green color = if (isDarkTheme) Color.White else Color.Black,
title = "Unblock User", maxLines = 1,
subtitle = "Allow this user to message you", overflow = TextOverflow.Ellipsis
onClick = { )
isBlocked = false
// TODO: Implement actual unblock logic if (username.isNotEmpty() && collapseProgress < 0.9f) {
}, Spacer(modifier = Modifier.height(4.dp))
isDarkTheme = isDarkTheme, Text(
hideChevron = true, text = "@$username",
textColor = Color(0xFF2E7D32) fontSize = usernameFontSize,
) color = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
} else { maxLines = 1,
ProfileNavigationItem( overflow = TextOverflow.Ellipsis,
icon = Icons.Outlined.Block, modifier = Modifier.graphicsLayer {
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444), alpha = 1f - (collapseProgress * 2f).coerceIn(0f, 1f)
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)
)
}
} }
} }
}
Text( // Back button
text = if (isBlocked) { IconButton(
"If you want the user to be able to send you messages again, you can unblock them. You can block them later." onClick = onBack,
} else { modifier = Modifier
"The person will no longer be able to message you if you block them. You can unblock them later." .padding(top = statusBarHeight)
}, .padding(start = 4.dp, top = 4.dp)
fontSize = 12.sp, ) {
color = secondaryTextColor, Icon(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp), imageVector = Icons.Filled.ArrowBack,
lineHeight = 16.sp contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
)
}
}
}
// ═══════════════════════════════════════════════════════════
// 🚫 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
) )
Spacer(modifier = Modifier.height(32.dp))
} }
} }
} }