diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt index 1c0529b..358fd58 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt @@ -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 + ) + } + } +} \ No newline at end of file