feat: Implement collapsing header and block/unblock functionality in OtherProfileScreen
This commit is contained in:
@@ -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(
|
||||
// Scrollable content
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.fillMaxSize()
|
||||
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() })
|
||||
) {
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
Text(
|
||||
text = "Public Key",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
TelegramSectionTitle(title = "Public Key", isDarkTheme = isDarkTheme)
|
||||
|
||||
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)
|
||||
)
|
||||
IconButton(
|
||||
onClick = { /* TODO: Copy to clipboard */ }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
tint = secondaryTextColor,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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/Unblock Section
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = surfaceColor,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚫 BLOCK SECTION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
TelegramSectionTitle(title = "Privacy", isDarkTheme = isDarkTheme)
|
||||
|
||||
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
|
||||
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() }
|
||||
)
|
||||
}
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (avatarFontSize.value > 0) {
|
||||
Text(
|
||||
text = getInitials(name),
|
||||
fontSize = avatarFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚫 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user