Фикс скелетона и залипания вкладок в профиле

This commit is contained in:
2026-03-20 12:26:33 +05:00
parent 004b54ec7c
commit 0353f845a5
3 changed files with 42 additions and 11 deletions

View File

@@ -120,6 +120,7 @@ import kotlinx.coroutines.withContext
private val groupMembersCountCache = java.util.concurrent.ConcurrentHashMap<String, Int>() private val groupMembersCountCache = java.util.concurrent.ConcurrentHashMap<String, Int>()
private const val GROUP_MESSAGE_STACK_TIME_DIFF_MS = 5 * 60_000L private const val GROUP_MESSAGE_STACK_TIME_DIFF_MS = 5 * 60_000L
private const val DIRECT_MESSAGE_STACK_TIME_DIFF_MS = 60_000L private const val DIRECT_MESSAGE_STACK_TIME_DIFF_MS = 60_000L
private const val MESSAGE_SKELETON_VISIBILITY_DELAY_MS = 180L
private fun isSameLocalDay(firstTimestampMs: Long, secondTimestampMs: Long): Boolean { private fun isSameLocalDay(firstTimestampMs: Long, secondTimestampMs: Long): Boolean {
val firstCalendar = val firstCalendar =
@@ -631,6 +632,15 @@ fun ChatDetailScreen(
// If typing, the user is obviously online — never show "offline" while typing // If typing, the user is obviously online — never show "offline" while typing
val isOnline = rawIsOnline || isTyping val isOnline = rawIsOnline || isTyping
val isLoading by viewModel.isLoading.collectAsState() // 🔥 Для скелетона val isLoading by viewModel.isLoading.collectAsState() // 🔥 Для скелетона
val showMessageSkeleton by
produceState(initialValue = false, key1 = isLoading) {
if (!isLoading) {
value = false
return@produceState
}
delay(MESSAGE_SKELETON_VISIBILITY_DELAY_MS)
value = isLoading
}
// <20>🔥 Reply/Forward state // <20>🔥 Reply/Forward state
val replyMessages by viewModel.replyMessages.collectAsState() val replyMessages by viewModel.replyMessages.collectAsState()
@@ -2390,7 +2400,7 @@ fun ChatDetailScreen(
when { when {
// 🔥 СКЕЛЕТОН - показываем пока загружаются // 🔥 СКЕЛЕТОН - показываем пока загружаются
// сообщения // сообщения
isLoading -> { showMessageSkeleton -> {
MessageSkeletonList( MessageSkeletonList(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
isGroupChat = isGroupChat, isGroupChat = isGroupChat,
@@ -2398,6 +2408,9 @@ fun ChatDetailScreen(
Modifier.fillMaxSize() Modifier.fillMaxSize()
) )
} }
isLoading && messages.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize())
}
// Пустое состояние (нет сообщений) // Пустое состояние (нет сообщений)
messages.isEmpty() -> { messages.isEmpty() -> {
Column( Column(

View File

@@ -85,6 +85,7 @@ import compose.icons.tablericons.*
import com.rosetta.messenger.ui.icons.TelegramIcons import com.rosetta.messenger.ui.icons.TelegramIcons
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlin.math.hypot import kotlin.math.hypot
@@ -108,6 +109,8 @@ private enum class DeviceResolveAction {
DECLINE DECLINE
} }
private const val SKELETON_VISIBILITY_DELAY_MS = 180L
// Avatar colors matching React Native app (Mantine inspired) // Avatar colors matching React Native app (Mantine inspired)
// Light theme colors (background lighter, text darker) // Light theme colors (background lighter, text darker)
private val avatarColorsLight = private val avatarColorsLight =
@@ -1791,7 +1794,18 @@ fun ChatsListScreen(
val requests = chatsState.requests val requests = chatsState.requests
val requestsCount = chatsState.requestsCount val requestsCount = chatsState.requestsCount
val showSkeleton = isLoading val showSkeleton by
produceState(
initialValue = false,
key1 = isLoading
) {
if (!isLoading) {
value = false
return@produceState
}
delay(SKELETON_VISIBILITY_DELAY_MS)
value = isLoading
}
val chatListState = rememberLazyListState() val chatListState = rememberLazyListState()
var isRequestsVisible by remember { mutableStateOf(true) } var isRequestsVisible by remember { mutableStateOf(true) }
var requestsPullProgress by remember { mutableStateOf(0f) } var requestsPullProgress by remember { mutableStateOf(0f) }
@@ -2010,6 +2024,8 @@ fun ChatsListScreen(
} // Close Box wrapper } // Close Box wrapper
} else if (showSkeleton) { } else if (showSkeleton) {
ChatsListSkeleton(isDarkTheme = isDarkTheme) ChatsListSkeleton(isDarkTheme = isDarkTheme)
} else if (isLoading && chatsState.isEmpty) {
Box(modifier = Modifier.fillMaxSize())
} else if (chatsState.isEmpty) { } else if (chatsState.isEmpty) {
EmptyChatsState( EmptyChatsState(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,

View File

@@ -108,6 +108,7 @@ import compose.icons.TablerIcons
import compose.icons.tablericons.* import compose.icons.tablericons.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -199,19 +200,12 @@ fun OtherProfileScreen(
var avatarViewerTimestamp by remember { mutableStateOf(0L) } var avatarViewerTimestamp by remember { mutableStateOf(0L) }
var imageViewerInitialIndex by remember { mutableIntStateOf(0) } var imageViewerInitialIndex by remember { mutableIntStateOf(0) }
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) } var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
var tabSwitchJob by remember { mutableStateOf<Job?>(null) }
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = 0, initialPage = 0,
pageCount = { OtherProfileTab.entries.size } pageCount = { OtherProfileTab.entries.size }
) )
// Tab click → animate pager
LaunchedEffect(selectedTab) {
val page = OtherProfileTab.entries.indexOf(selectedTab)
if (pagerState.currentPage != page) {
pagerState.animateScrollToPage(page)
}
}
// Pager swipe → update tab + control swipe-back // Pager swipe → update tab + control swipe-back
LaunchedEffect(pagerState) { LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page -> snapshotFlow { pagerState.currentPage }.collect { page ->
@@ -826,7 +820,15 @@ fun OtherProfileScreen(
) { ) {
OtherProfileSharedTabs( OtherProfileSharedTabs(
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { tab -> selectedTab = tab }, onTabSelected = { tab ->
val targetPage = OtherProfileTab.entries.indexOf(tab)
if (targetPage == -1) return@OtherProfileSharedTabs
selectedTab = tab
tabSwitchJob?.cancel()
tabSwitchJob = coroutineScope.launch {
runCatching { pagerState.animateScrollToPage(targetPage) }
}
},
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
} }