From d9453edd056df7fe154a28676e1335071ff256a1 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 31 Jan 2026 03:14:37 +0500 Subject: [PATCH] Implement feature X to enhance user experience and optimize performance --- .../messenger/ui/settings/ProfileScreen.kt | 450 +++++++----------- 1 file changed, 164 insertions(+), 286 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index 3f2e13e..52576da 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -291,203 +291,119 @@ fun ProfileScreen( // ViewModel state val profileState by viewModel.state.collectAsState() - // Scroll state for collapsing header animation + // Scroll state for collapsing header + overscroll avatar expansion val density = LocalDensity.current val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + + // Header heights val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT + statusBarHeight).toPx() } val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() } - - // Track scroll offset with animated state for smooth transitions - // Может быть отрицательным для overscroll (оттягивание вверх) - var scrollOffset by remember { mutableFloatStateOf(0f) } val maxScrollOffset = expandedHeightPx - collapsedHeightPx - val minScrollOffset = -expandedHeightPx * 0.5f // Максимальный overscroll (50% высоты) - // Порог для snap - если overscroll > 50%, то snap к квадрату, иначе к кругу - val snapThreshold = minScrollOffset * 0.5f + // Track scroll offset for collapsing (скролл вверх = collapse) + var scrollOffset by remember { mutableFloatStateOf(0f) } - // Анимированный overscroll offset для плавного snap - val animatedScrollOffset = remember { androidx.compose.animation.core.Animatable(0f) } + // Calculate collapse progress (0 = expanded, 1 = collapsed) + val collapseProgress by remember { + derivedStateOf { (scrollOffset / maxScrollOffset).coerceIn(0f, 1f) } + } - // Отслеживаем активный drag + // Dynamic header height based on scroll + val headerHeight = + with(density) { + (expandedHeightPx - scrollOffset).coerceAtLeast(collapsedHeightPx).toDp() + } + + // Track overscroll offset for avatar expansion (скролл вниз при достижении верха) + var overscrollOffset by remember { mutableFloatStateOf(0f) } + val maxOverscroll = with(density) { 200.dp.toPx() } + val snapThreshold = maxOverscroll * 0.5f + + // Track if user is currently dragging var isDragging by remember { mutableStateOf(false) } - // Отслеживаем направление последнего скролла: true = вверх (к кругу), false = вниз (к квадрату) - var lastScrollDirectionUp by remember { mutableStateOf(false) } - - // Calculate collapse progress (0 = expanded, 1 = collapsed, negative = overscroll) - val collapseProgress by remember { - derivedStateOf { (animatedScrollOffset.value / maxScrollOffset).coerceIn(-1f, 1f) } - } - - // Определяем целевое состояние: true = круг (scrollOffset >= 0), false = квадрат (scrollOffset - // < snapThreshold) - val targetIsCircle by remember { + // Целевое значение для snap: либо 0 (круг), либо maxOverscroll (квадрат) + val snapTarget by remember { derivedStateOf { - // Решение: круг если scrollOffset >= snapThreshold (50% от минимального) - scrollOffset >= snapThreshold + if (!isDragging && overscrollOffset > snapThreshold) maxOverscroll + else if (!isDragging) 0f else overscrollOffset } } - // Текущее отображаемое состояние (для предотвращения мерцания) - var displayedIsCircle by remember { mutableStateOf(true) } - - // Overscroll progress - просто на основе scrollOffset - val overscrollProgress by remember { - derivedStateOf { - // Если мы в режиме "круг", всегда показываем 0 (полный круг) - // Если в режиме "квадрат", показываем 1 (полный квадрат) - val rawProgress = - if (animatedScrollOffset.value < 0f) { - (-animatedScrollOffset.value / (-minScrollOffset)).coerceIn(0f, 1f) - } else { - 0f - } - - // Дискретный прогресс на основе displayedIsCircle - val discreteProgress = if (displayedIsCircle) 0f else 1f - - Log.d( - TAG, - "📊 overscrollProgress: raw=$rawProgress, discrete=$discreteProgress, displayedIsCircle=$displayedIsCircle, targetIsCircle=$targetIsCircle, scrollOffset=${animatedScrollOffset.value}" - ) - discreteProgress - } - } - - // Обновляем displayedIsCircle только при отпускании пальца (snap) - LaunchedEffect(isDragging, targetIsCircle) { - Log.d( - TAG, - "🎯 LaunchedEffect: isDragging=$isDragging, targetIsCircle=$targetIsCircle, displayedIsCircle=$displayedIsCircle, scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp" - ) - - if (!isDragging) { - // Палец отпущен - snap на основе направления скролла - // Если скроллили ВВЕРХ - snap к кругу, если ВНИЗ - snap к квадрату - val newIsCircle = lastScrollDirectionUp || scrollOffset >= 0f - Log.d( - TAG, - "👆 Finger released: scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp, newIsCircle=$newIsCircle" + // Пружинистая анимация для snap-эффекта - быстрая + val animatedOverscroll by + animateFloatAsState( + targetValue = snapTarget, + animationSpec = + spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = 2000f // Очень быстрая анимация + ), + label = "overscroll" ) - if (scrollOffset < 0f) { - // Были в overscroll зоне - val targetOffset = if (newIsCircle) 0f else minScrollOffset - Log.d( - TAG, - "🎯 Snap animation: from $scrollOffset to $targetOffset (circle=$newIsCircle)" - ) - - // Сначала обновляем состояние формы - displayedIsCircle = newIsCircle - - // Потом анимируем позицию - animatedScrollOffset.animateTo( - targetValue = targetOffset, - animationSpec = - spring( - dampingRatio = - androidx.compose.animation.core.Spring - .DampingRatioMediumBouncy, - stiffness = - androidx.compose.animation.core.Spring.StiffnessLow - ) - ) - scrollOffset = targetOffset - } else if (scrollOffset >= 0f) { - // Обычный скролл - всегда круг - displayedIsCircle = true - animatedScrollOffset.snapTo(scrollOffset) - } + // Calculate expansion progress (0 = круг, 1 = квадрат) - только когда header раскрыт + val expansionProgress by remember { + derivedStateOf { + if (collapseProgress > 0.1f) 0f // Не расширяем если header коллапсирован + else (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f) } } - // Синхронизация scrollOffset -> animatedScrollOffset во время drag - LaunchedEffect(scrollOffset, isDragging) { - if (isDragging) { - animatedScrollOffset.snapTo(scrollOffset) - Log.d(TAG, "🔄 Sync during drag: scrollOffset=$scrollOffset -> animatedScrollOffset") - } - } - - // Nested scroll connection for tracking scroll with overscroll support + // Nested scroll connection для collapsing + overscroll val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.y - val newOffset = scrollOffset - delta - - // Отслеживаем, что палец активен - val wasDragging = isDragging isDragging = true - // Отслеживаем направление скролла (только значимые движения) - if (kotlin.math.abs(delta) > 3f) { - // delta < 0 = палец идёт вверх (скролл контента вниз, возврат к кругу) - // delta > 0 = палец идёт вниз (скролл контента вверх, к квадрату) - lastScrollDirectionUp = delta < 0f - Log.d( - TAG, - "🧭 Direction changed: delta=$delta, lastScrollDirectionUp=$lastScrollDirectionUp" - ) - } - - Log.d( - TAG, - "🔄 onPreScroll: delta=$delta, scrollOffset=$scrollOffset, newOffset=$newOffset, wasDragging=$wasDragging, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp" - ) - - // Если скроллим вверх (delta < 0) и были в overscroll зоне - возвращаемся к кругу - if (delta < 0f && scrollOffset < 0f) { - // Плавно уменьшаем overscroll - val newValue = (scrollOffset - delta).coerceAtMost(0f) - scrollOffset = newValue - - // Когда достигли 0, переключаемся на круг - if (scrollOffset >= 0f) { - Log.d(TAG, "⚡ Reached 0 from overscroll - switching to circle") - displayedIsCircle = true - scrollOffset = 0f + // Если тянем вверх (delta < 0) + if (delta < 0) { + // Сначала убираем overscroll если есть + if (overscrollOffset > 0) { + val newOffset = (overscrollOffset + delta).coerceAtLeast(0f) + val consumed = overscrollOffset - newOffset + overscrollOffset = newOffset + return Offset(0f, -consumed) + } + // Затем коллапсируем header + if (scrollOffset < maxScrollOffset) { + val newScrollOffset = (scrollOffset - delta).coerceIn(0f, maxScrollOffset) + val consumed = newScrollOffset - scrollOffset + scrollOffset = newScrollOffset + return Offset(0f, -consumed) } - - Log.d(TAG, "📤 Scroll up in overscroll: scrollOffset=$scrollOffset") - return Offset(0f, -delta) } - val consumed = - when { - // Scroll up (collapse) - delta < 0 = палец идёт вверх - delta < 0 && scrollOffset < maxScrollOffset -> { - val consumed = - (newOffset.coerceIn(minScrollOffset, maxScrollOffset) - - scrollOffset) - scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset) - Log.d(TAG, "📈 Collapse: scrollOffset=$scrollOffset") - -consumed - } - // Scroll down (expand / overscroll) - delta > 0 = палец идёт вниз - delta > 0 && scrollOffset > minScrollOffset -> { - val consumed = - scrollOffset - - newOffset.coerceIn(minScrollOffset, maxScrollOffset) - scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset) - Log.d(TAG, "📉 Expand/Overscroll: scrollOffset=$scrollOffset") - consumed - } - else -> 0f - } - return Offset(0f, consumed) + // Если тянем вниз (delta > 0) и header коллапсирован - сначала раскрываем + if (delta > 0 && scrollOffset > 0) { + val newScrollOffset = (scrollOffset - delta).coerceAtLeast(0f) + val consumed = scrollOffset - newScrollOffset + scrollOffset = newScrollOffset + return Offset(0f, consumed) + } + + return Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + // Если достигли верха (scrollOffset = 0) и тянем вниз - создаем overscroll + if (available.y > 0 && scrollOffset == 0f) { + val resistance = 0.3f + val delta = available.y * resistance + overscrollOffset = (overscrollOffset + delta).coerceAtMost(maxOverscroll) + return Offset(0f, available.y) + } + return Offset.Zero } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - Log.d( - TAG, - "👆 onPostFling: scrollOffset=$scrollOffset, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp" - ) isDragging = false - Log.d(TAG, "👆 onPostFling: drag ended, isDragging=false, triggering snap") - return super.onPostFling(consumed, available) + return Velocity.Zero } } } @@ -541,21 +457,7 @@ fun ProfileScreen( .nestedScroll(nestedScrollConnection) ) { // Scrollable content - LazyColumn( - modifier = - Modifier.fillMaxSize() - .padding( - top = - with(density) { - // Не увеличиваем padding при overscroll - // Используем animatedScrollOffset для плавности - (expandedHeightPx - - animatedScrollOffset.value - .coerceAtLeast(0f)) - .toDp() - } - ) - ) { + LazyColumn(modifier = Modifier.fillMaxSize().padding(top = headerHeight)) { item { Spacer(modifier = Modifier.height(16.dp)) @@ -666,7 +568,7 @@ fun ProfileScreen( } // ═════════════════════════════════════════════════════════════ - // 🎨 COLLAPSING HEADER - Telegram style + // 🎨 COLLAPSING PROFILE HEADER with overscroll expansion // ═════════════════════════════════════════════════════════════ CollapsingProfileHeader( name = editedName.ifBlank { accountPublicKey.take(10) }, @@ -674,7 +576,7 @@ fun ProfileScreen( publicKey = accountPublicKey, avatarColors = avatarColors, collapseProgress = collapseProgress, - overscrollProgress = overscrollProgress, + expansionProgress = expansionProgress, onBack = onBack, hasChanges = hasChanges, onSave = { @@ -803,7 +705,10 @@ fun ProfileScreen( } // ═════════════════════════════════════════════════════════════ -// 🎯 COLLAPSING PROFILE HEADER - Telegram Style Animation +// 🎯 COLLAPSING PROFILE HEADER - Circle by default, Square on overscroll +// По умолчанию: круглая аватарка +// При overscroll вниз (expansionProgress): расширяется до квадрата +// При скролле вверх (collapseProgress): уменьшается и уходит вверх // ═════════════════════════════════════════════════════════════ @Composable private fun CollapsingProfileHeader( @@ -812,7 +717,7 @@ private fun CollapsingProfileHeader( publicKey: String, avatarColors: AvatarColors, collapseProgress: Float, - overscrollProgress: Float, // 0 = normal, 1 = full overscroll (квадратный аватар) + expansionProgress: Float, onBack: () -> Unit, hasChanges: Boolean, onSave: () -> Unit, @@ -831,140 +736,118 @@ private fun CollapsingProfileHeader( // Get actual status bar height val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() - // Header heights - // По умолчанию header = ширина экрана минус отступ для Account, при скролле уменьшается - val expandedHeight = - screenWidthDp - 60.dp // Высота header (меньше чтобы не перекрывать Account) + // ═══════════════════════════════════════════════════════════ + // 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll + // ═══════════════════════════════════════════════════════════ + val expandedHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight val collapsedHeight = COLLAPSED_HEADER_HEIGHT + statusBarHeight - // Animated header height - НЕ увеличивается при overscroll + // Header height меняется только при collapse, НЕ при overscroll val headerHeight = - androidx.compose.ui.unit.lerp( - expandedHeight, - collapsedHeight, - collapseProgress.coerceAtLeast(0f) - ) + androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress) // ═══════════════════════════════════════════════════════════ - // 👤 AVATAR - По умолчанию круглый, при overscroll становится квадратным - // Аватар всегда ограничен размером header + // 👤 AVATAR - По умолчанию КРУГЛАЯ, при overscroll расширяется до прямоугольника + // При collapse - уменьшается и уходит вверх // ═══════════════════════════════════════════════════════════ - - // Размер круглого аватара по умолчанию val circleSize = AVATAR_SIZE_EXPANDED + // Зона аватарки = ВСЯ высота header включая статус бар + val avatarZoneHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight - // При overscroll: от круга до полного размера header - // При collapse: от круга до 0 + // При overscroll расширяем до прямоугольника на всю зону (только если не collapsed) val avatarWidth: Dp val avatarHeight: Dp - val avatarX: Dp - val avatarY: Dp - val cornerRadius: Dp - val collapseOnly = collapseProgress.coerceAtLeast(0f) - - if (overscrollProgress > 0f) { - // OVERSCROLL: размер СРАЗУ полный, закругление плавно уменьшается - avatarWidth = screenWidthDp - avatarHeight = expandedHeight - avatarX = 0.dp - avatarY = 0.dp - // Закругление плавно от круга (circleSize/2) до квадрата (0) - cornerRadius = androidx.compose.ui.unit.lerp(circleSize / 2, 0.dp, overscrollProgress) + if (collapseProgress < 0.1f && expansionProgress > 0f) { + // Overscroll: круг -> прямоугольник на всю зону ВКЛЮЧАЯ статус бар + avatarWidth = androidx.compose.ui.unit.lerp(circleSize, screenWidthDp, expansionProgress) + avatarHeight = + androidx.compose.ui.unit.lerp(circleSize, avatarZoneHeight, expansionProgress) } else { - // NORMAL / COLLAPSE: круглый аватар уменьшается при скролле - avatarWidth = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseOnly) - avatarHeight = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseOnly) - avatarX = (screenWidthDp - avatarWidth) / 2 - - // Позиция Y: по центру header при развернутом, уходит вверх при сворачивании - val avatarCenterY = (expandedHeight - circleSize) / 2 - avatarY = - if (collapseOnly < 0.5f) { - androidx.compose.ui.unit.lerp( - avatarCenterY, - statusBarHeight + 14.dp, - collapseOnly * 2 - ) - } else { - val phase2Progress = (collapseOnly - 0.5f) / 0.5f - androidx.compose.ui.unit.lerp( - statusBarHeight + 14.dp, - statusBarHeight - 60.dp, - phase2Progress - ) - } - // Полностью круглый - cornerRadius = circleSize / 2 + // Collapse: сразу начинаем уменьшаться от круга до 0 + val collapsedSize = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseProgress) + avatarWidth = collapsedSize + avatarHeight = collapsedSize } - // Для cornerRadius используем меньшую сторону - val avatarSize = minOf(avatarWidth, avatarHeight) + val avatarSize = if (avatarWidth < avatarHeight) avatarWidth else avatarHeight - val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseOnly) + // Позиция X: всегда по центру + val avatarX = (screenWidthDp - avatarWidth) / 2 + + // Позиция Y + val availableHeight = avatarZoneHeight - statusBarHeight + val defaultCenterY = statusBarHeight + (availableHeight - avatarHeight) / 2 + val topAvatarY = 0.dp // От самого верха экрана при полном expansion + + val avatarY = + if (collapseProgress < 0.1f && expansionProgress > 0f) { + // При overscroll прижимаемся к самому верху + androidx.compose.ui.unit.lerp(defaultCenterY, topAvatarY, expansionProgress) + } else { + // Collapse: сразу начинаем уходить вверх + androidx.compose.ui.unit.lerp( + defaultCenterY, + statusBarHeight - 80.dp, + collapseProgress + ) + } + + // Закругление: круг по умолчанию, при overscroll становится квадратом + val cornerRadius = + if (collapseProgress < 0.1f && expansionProgress > 0f) { + // Overscroll: круг -> квадрат с небольшим скруглением + androidx.compose.ui.unit.lerp(avatarSize / 2, 12.dp, expansionProgress) + } else { + // Всегда круг + avatarSize / 2 + } + + val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress) // ═══════════════════════════════════════════════════════════ - // 📝 TEXT - always centered, under avatar + // 📝 TEXT - внизу header зоны, внутри блока // ═══════════════════════════════════════════════════════════ - val textX = screenWidthDp / 2 // Always center - - // Позиция Y аватара для расчета текста (используем expandedHeight для стабильности) - val avatarCenterYForText = (expandedHeight - circleSize) / 2 - - // Позиция текста: под аватаром по центру - val avatarBottomY = avatarCenterYForText + circleSize // Низ аватара - val textExpandedY = avatarBottomY + 16.dp // 16dp отступ от аватара + val textDefaultY = expandedHeight - 60.dp // Внизу header блока (чуть ниже) val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2 - val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseOnly) + + // Текст меняет позицию только при collapse, НЕ при overscroll + val textY = androidx.compose.ui.unit.lerp(textDefaultY, textCollapsedY, collapseProgress) // Font sizes - val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseOnly) - val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseOnly) + val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress) + val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress) - Box( - modifier = - Modifier.fillMaxWidth() - .height(headerHeight) - .clip( - RoundedCornerShape(0.dp) - ) // Обрезаем содержимое по границам header - ) { + Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) { // ═══════════════════════════════════════════════════════════ - // 🎨 BLURRED AVATAR BACKGROUND - скрываем при overscroll (аватар сам закрывает фон) + // 🎨 BLURRED AVATAR BACKGROUND - всегда показываем // ═══════════════════════════════════════════════════════════ - if (overscrollProgress < 0.5f) { - BlurredAvatarBackground( - publicKey = publicKey, - avatarRepository = avatarRepository, - fallbackColor = avatarColors.backgroundColor, - blurRadius = 25f, - alpha = 0.3f - ) - } + BlurredAvatarBackground( + publicKey = publicKey, + avatarRepository = avatarRepository, + fallbackColor = avatarColors.backgroundColor, + blurRadius = 25f, + alpha = 0.3f + ) // ═══════════════════════════════════════════════════════════ - // 👤 AVATAR - По умолчанию круглый по центру - // РИСУЕМ ПЕРВЫМ чтобы кнопки были поверх + // 👤 AVATAR - Круг по умолчанию, квадрат при overscroll // ═══════════════════════════════════════════════════════════ if (avatarSize > 1.dp) { Box( modifier = Modifier.offset(x = avatarX, y = avatarY) - .width(avatarWidth) - .height(avatarHeight) + .size(width = avatarWidth, height = avatarHeight) .clip(RoundedCornerShape(cornerRadius)), contentAlignment = Alignment.Center ) { - // Используем AvatarImage если репозиторий доступен if (avatarRepository != null) { - // Всегда используем FullSizeAvatar чтобы избежать мерцания при переключении FullSizeAvatar( publicKey = publicKey, avatarRepository = avatarRepository, isDarkTheme = isDarkTheme ) } else { - // Fallback: цветной placeholder с инициалами Box( modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor), @@ -984,7 +867,7 @@ private fun CollapsingProfileHeader( } // ═══════════════════════════════════════════════════════════ - // 🔙 BACK BUTTON (поверх аватара) + // 🔙 BACK BUTTON // ═══════════════════════════════════════════════════════════ Box( modifier = @@ -1004,8 +887,7 @@ private fun CollapsingProfileHeader( } // ═══════════════════════════════════════════════════════════ - // ⋮ MENU BUTTON / 💾 SAVE BUTTON (top right corner) - // Показываем Save если есть изменения, иначе три точки меню + // ⋮ MENU BUTTON / 💾 SAVE BUTTON // ═══════════════════════════════════════════════════════════ Box( modifier = @@ -1014,7 +896,6 @@ private fun CollapsingProfileHeader( .padding(end = 4.dp, top = 4.dp), contentAlignment = Alignment.Center ) { - // Save button (when has changes) AnimatedVisibility(visible = hasChanges, enter = fadeIn(), exit = fadeOut()) { TextButton(onClick = onSave) { Text( @@ -1025,7 +906,6 @@ private fun CollapsingProfileHeader( } } - // Menu button (when no changes) AnimatedVisibility(visible = !hasChanges, enter = fadeIn(), exit = fadeOut()) { IconButton( onClick = { onAvatarMenuChange(true) }, @@ -1034,13 +914,12 @@ private fun CollapsingProfileHeader( Icon( imageVector = TablerIcons.DotsVertical, contentDescription = "Profile menu", - tint = Color.White, // Всегда белые - на фоне аватара + tint = Color.White, modifier = Modifier.size(24.dp) ) } } - // Меню для установки фото профиля com.rosetta.messenger.ui.chats.components.ProfilePhotoMenu( expanded = showAvatarMenu, onDismiss = { onAvatarMenuChange(false) }, @@ -1058,7 +937,7 @@ private fun CollapsingProfileHeader( } // ═══════════════════════════════════════════════════════════ - // 📝 TEXT BLOCK - Name + Online, always centered + // 📝 TEXT BLOCK - Name + Online // ═══════════════════════════════════════════════════════════ Column( modifier = @@ -1090,7 +969,6 @@ private fun CollapsingProfileHeader( Spacer(modifier = Modifier.height(2.dp)) - // Online text - always centered Text(text = "online", fontSize = onlineFontSize, color = Color(0xFF4CAF50)) } }