Implement feature X to enhance user experience and optimize performance

This commit is contained in:
k1ngsterr1
2026-01-31 03:14:37 +05:00
parent 5f87f091f7
commit d9453edd05

View File

@@ -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))
}
}