Фикс скелетона и залипания вкладок в профиле
This commit is contained in:
@@ -120,6 +120,7 @@ import kotlinx.coroutines.withContext
|
||||
private val groupMembersCountCache = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
||||
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 MESSAGE_SKELETON_VISIBILITY_DELAY_MS = 180L
|
||||
|
||||
private fun isSameLocalDay(firstTimestampMs: Long, secondTimestampMs: Long): Boolean {
|
||||
val firstCalendar =
|
||||
@@ -631,6 +632,15 @@ fun ChatDetailScreen(
|
||||
// If typing, the user is obviously online — never show "offline" while typing
|
||||
val isOnline = rawIsOnline || isTyping
|
||||
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
|
||||
val replyMessages by viewModel.replyMessages.collectAsState()
|
||||
@@ -2390,7 +2400,7 @@ fun ChatDetailScreen(
|
||||
when {
|
||||
// 🔥 СКЕЛЕТОН - показываем пока загружаются
|
||||
// сообщения
|
||||
isLoading -> {
|
||||
showMessageSkeleton -> {
|
||||
MessageSkeletonList(
|
||||
isDarkTheme = isDarkTheme,
|
||||
isGroupChat = isGroupChat,
|
||||
@@ -2398,6 +2408,9 @@ fun ChatDetailScreen(
|
||||
Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
isLoading && messages.isEmpty() -> {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
// Пустое состояние (нет сообщений)
|
||||
messages.isEmpty() -> {
|
||||
Column(
|
||||
|
||||
@@ -85,6 +85,7 @@ import compose.icons.tablericons.*
|
||||
import com.rosetta.messenger.ui.icons.TelegramIcons
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.math.hypot
|
||||
@@ -108,6 +109,8 @@ private enum class DeviceResolveAction {
|
||||
DECLINE
|
||||
}
|
||||
|
||||
private const val SKELETON_VISIBILITY_DELAY_MS = 180L
|
||||
|
||||
// Avatar colors matching React Native app (Mantine inspired)
|
||||
// Light theme colors (background lighter, text darker)
|
||||
private val avatarColorsLight =
|
||||
@@ -1791,7 +1794,18 @@ fun ChatsListScreen(
|
||||
val requests = chatsState.requests
|
||||
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()
|
||||
var isRequestsVisible by remember { mutableStateOf(true) }
|
||||
var requestsPullProgress by remember { mutableStateOf(0f) }
|
||||
@@ -2010,6 +2024,8 @@ fun ChatsListScreen(
|
||||
} // Close Box wrapper
|
||||
} else if (showSkeleton) {
|
||||
ChatsListSkeleton(isDarkTheme = isDarkTheme)
|
||||
} else if (isLoading && chatsState.isEmpty) {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
} else if (chatsState.isEmpty) {
|
||||
EmptyChatsState(
|
||||
isDarkTheme = isDarkTheme,
|
||||
|
||||
@@ -108,6 +108,7 @@ import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -199,19 +200,12 @@ fun OtherProfileScreen(
|
||||
var avatarViewerTimestamp by remember { mutableStateOf(0L) }
|
||||
var imageViewerInitialIndex by remember { mutableIntStateOf(0) }
|
||||
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
|
||||
var tabSwitchJob by remember { mutableStateOf<Job?>(null) }
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
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
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||
@@ -826,7 +820,15 @@ fun OtherProfileScreen(
|
||||
) {
|
||||
OtherProfileSharedTabs(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user