mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Implement full predictive back support and replace tabs with HorizontalPager
This commit is contained in:
@@ -47,6 +47,10 @@ import ru.fromchat.app_name
|
|||||||
import ru.fromchat.back
|
import ru.fromchat.back
|
||||||
import ru.fromchat.ui.branding.FromChatBrandTitle
|
import ru.fromchat.ui.branding.FromChatBrandTitle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* About is a root [androidx.navigation.NavHost] destination; system and predictive back (see
|
||||||
|
* manifest `enableOnBackInvokedCallback`) pop via the same stack as [LocalNavController.navigateUp].
|
||||||
|
*/
|
||||||
private const val URL_TELEGRAM = "https://t.me/fromchat_ch"
|
private const val URL_TELEGRAM = "https://t.me/fromchat_ch"
|
||||||
private const val URL_MAX = "https://maxgate.io/fromchat_ch"
|
private const val URL_MAX = "https://maxgate.io/fromchat_ch"
|
||||||
private const val URL_WEBSITE = "https://fromchat.ru"
|
private const val URL_WEBSITE = "https://fromchat.ru"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.
|
|||||||
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
|
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
|
||||||
import androidx.compose.animation.SharedTransitionLayout
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.compose.setSingletonImageLoaderFactory
|
import coil3.compose.setSingletonImageLoaderFactory
|
||||||
import coil3.svg.SvgDecoder
|
import coil3.svg.SvgDecoder
|
||||||
@@ -38,7 +40,9 @@ import ru.fromchat.ui.auth.LoginScreen
|
|||||||
import ru.fromchat.ui.auth.RegisterScreen
|
import ru.fromchat.ui.auth.RegisterScreen
|
||||||
import ru.fromchat.ui.chat.PublicChatScreen
|
import ru.fromchat.ui.chat.PublicChatScreen
|
||||||
import ru.fromchat.ui.debug.DebugApiScreen
|
import ru.fromchat.ui.debug.DebugApiScreen
|
||||||
import ru.fromchat.ui.dm.DmContainerScreen
|
import ru.fromchat.ui.dm.DmChatRoute
|
||||||
|
import ru.fromchat.ui.dm.DmNav
|
||||||
|
import ru.fromchat.ui.dm.DmProfileRoute
|
||||||
import ru.fromchat.ui.main.MainScreen
|
import ru.fromchat.ui.main.MainScreen
|
||||||
import ru.fromchat.ui.profile.ProfileScreen
|
import ru.fromchat.ui.profile.ProfileScreen
|
||||||
import ru.fromchat.ui.setup.ServerConfigScreen
|
import ru.fromchat.ui.setup.ServerConfigScreen
|
||||||
@@ -237,7 +241,7 @@ fun App(scrollToMessageId: Int? = null, startAtPublicChat: Boolean = false) {
|
|||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
onBack = { navController.navigateUp() },
|
onBack = { navController.navigateUp() },
|
||||||
onChat = { navController.navigate("dm/$it") },
|
onChat = { navController.navigate(DmNav.chatRoute(it)) },
|
||||||
sharedTransitionScope = this@SharedTransitionLayout,
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
animatedVisibilityScope = this@composable,
|
animatedVisibilityScope = this@composable,
|
||||||
useSharedElementFromNavigation = useSharedElement,
|
useSharedElementFromNavigation = useSharedElement,
|
||||||
@@ -245,11 +249,61 @@ fun App(scrollToMessageId: Int? = null, startAtPublicChat: Boolean = false) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("dm/{otherUserId}") { backStackEntry ->
|
val dmChatProfileFade = tween<Float>(durationMillis = 280)
|
||||||
val otherUserId = backStackEntry.savedStateHandle.get<String>("otherUserId")?.toIntOrNull() ?: 0
|
|
||||||
DmContainerScreen(
|
composable(
|
||||||
|
route = DmNav.CHAT_ROUTE,
|
||||||
|
arguments = listOf(navArgument("otherUserId") { type = NavType.StringType }),
|
||||||
|
enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
DmNav.PROFILE_ROUTE -> fadeIn(animationSpec = dmChatProfileFade)
|
||||||
|
else -> slideIntoContainer(Start, animationSpec = animationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
when (targetState.destination.route) {
|
||||||
|
DmNav.PROFILE_ROUTE -> fadeOut(animationSpec = dmChatProfileFade)
|
||||||
|
else -> slideOutOfContainer(Start, animationSpec = animationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
DmNav.PROFILE_ROUTE -> fadeIn(animationSpec = dmChatProfileFade)
|
||||||
|
else -> slideIntoContainer(End, animationSpec = animationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
when (targetState.destination.route) {
|
||||||
|
DmNav.PROFILE_ROUTE -> fadeOut(animationSpec = dmChatProfileFade)
|
||||||
|
else -> slideOutOfContainer(End, animationSpec = animationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { entry ->
|
||||||
|
val otherUserId = entry.savedStateHandle.get<String>("otherUserId")?.toIntOrNull() ?: 0
|
||||||
|
if (otherUserId <= 0) return@composable
|
||||||
|
DmChatRoute(
|
||||||
otherUserId = otherUserId,
|
otherUserId = otherUserId,
|
||||||
onBack = { navController.navigateUp() }
|
navController = navController,
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedVisibilityScope = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = DmNav.PROFILE_ROUTE,
|
||||||
|
arguments = listOf(navArgument("otherUserId") { type = NavType.StringType }),
|
||||||
|
enterTransition = { fadeIn(animationSpec = dmChatProfileFade) },
|
||||||
|
exitTransition = { fadeOut(animationSpec = dmChatProfileFade) },
|
||||||
|
popEnterTransition = { fadeIn(animationSpec = dmChatProfileFade) },
|
||||||
|
popExitTransition = { fadeOut(animationSpec = dmChatProfileFade) },
|
||||||
|
) { entry ->
|
||||||
|
val otherUserId = entry.savedStateHandle.get<String>("otherUserId")?.toIntOrNull() ?: 0
|
||||||
|
if (otherUserId <= 0) return@composable
|
||||||
|
DmProfileRoute(
|
||||||
|
otherUserId = otherUserId,
|
||||||
|
navController = navController,
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedVisibilityScope = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import ru.fromchat.crypto.IdentityKeyManager
|
|||||||
import ru.fromchat.crypto.decryptEnvelope
|
import ru.fromchat.crypto.decryptEnvelope
|
||||||
import ru.fromchat.crypto.transport.TransportCrypto
|
import ru.fromchat.crypto.transport.TransportCrypto
|
||||||
import ru.fromchat.ui.LocalNavController
|
import ru.fromchat.ui.LocalNavController
|
||||||
|
import ru.fromchat.ui.dm.DmNav
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* English-only copy: the Debug API screen is not for end users; strings are not in compose
|
* English-only copy: the Debug API screen is not for end users; strings are not in compose
|
||||||
@@ -170,7 +171,7 @@ fun DebugApiScreenContent() {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.navigate("dm/2") },
|
onClick = { navController.navigate(DmNav.chatRoute(2)) },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(text = DebugScreenText.OpenAction)
|
Text(text = DebugScreenText.OpenAction)
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package ru.fromchat.ui.dm
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.SharedTransitionLayout
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import ru.fromchat.ui.BackHandler
|
|
||||||
import ru.fromchat.ui.HapticFeedbackEvent
|
|
||||||
import ru.fromchat.ui.rememberHapticFeedback
|
|
||||||
import ru.fromchat.ui.profile.ProfileScreen
|
|
||||||
|
|
||||||
private const val TRANSITION_MS = 320
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DmContainerScreen(
|
|
||||||
otherUserId: Int,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showProfile by remember { mutableStateOf(false) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val haptic = rememberHapticFeedback()
|
|
||||||
val panel = remember(otherUserId) {
|
|
||||||
DmPanelCache.getOrCreate(otherUserId, scope)
|
|
||||||
}
|
|
||||||
var panelState by remember(panel) { mutableStateOf(panel.getState()) }
|
|
||||||
|
|
||||||
LaunchedEffect(panel) {
|
|
||||||
panel.setOnStateChange { panelState = it }
|
|
||||||
panelState = panel.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
val initialDisplayName = panelState.titleAvatar?.displayName?.takeIf { it.isNotBlank() }
|
|
||||||
?: panelState.title.takeIf { it.isNotBlank() }
|
|
||||||
|
|
||||||
BackHandler(enabled = showProfile) {
|
|
||||||
haptic(HapticFeedbackEvent.ProfileClosed)
|
|
||||||
showProfile = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// One stable key per DM to match the same avatar in chat + profile.
|
|
||||||
val sharedAvatarKey = remember(otherUserId) { "dm-avatar-$otherUserId" }
|
|
||||||
|
|
||||||
SharedTransitionLayout(modifier = modifier.fillMaxSize()) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = showProfile,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(animationSpec = tween(TRANSITION_MS)) togetherWith
|
|
||||||
fadeOut(animationSpec = tween(TRANSITION_MS))
|
|
||||||
},
|
|
||||||
label = "dm_profile"
|
|
||||||
) { targetState ->
|
|
||||||
if (!targetState) {
|
|
||||||
DmScreen(
|
|
||||||
panel = panel,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
onTitleClick = {
|
|
||||||
haptic(HapticFeedbackEvent.ProfileOpened)
|
|
||||||
showProfile = true
|
|
||||||
},
|
|
||||||
sharedTransitionScope = this@SharedTransitionLayout,
|
|
||||||
animatedVisibilityScope = this@AnimatedContent,
|
|
||||||
sharedAvatarKey = sharedAvatarKey
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ProfileScreen(
|
|
||||||
userId = otherUserId,
|
|
||||||
onBack = {
|
|
||||||
haptic(HapticFeedbackEvent.ProfileClosed)
|
|
||||||
showProfile = false
|
|
||||||
},
|
|
||||||
onChat = { _ ->
|
|
||||||
haptic(HapticFeedbackEvent.ProfileClosed)
|
|
||||||
showProfile = false
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
sharedTransitionScope = this@SharedTransitionLayout,
|
|
||||||
animatedVisibilityScope = this@AnimatedContent,
|
|
||||||
sharedAvatarKey = sharedAvatarKey,
|
|
||||||
initialDisplayName = initialDisplayName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package ru.fromchat.ui.dm
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import ru.fromchat.ui.HapticFeedbackEvent
|
||||||
|
import ru.fromchat.ui.profile.ProfileScreen
|
||||||
|
import ru.fromchat.ui.rememberHapticFeedback
|
||||||
|
|
||||||
|
/** Route patterns and builders for DM chat + in-DM profile (stacked for predictive / system back). */
|
||||||
|
object DmNav {
|
||||||
|
const val CHAT_ROUTE = "dm/{otherUserId}/chat"
|
||||||
|
const val PROFILE_ROUTE = "dm/{otherUserId}/profile"
|
||||||
|
|
||||||
|
fun chatRoute(otherUserId: Int) = "dm/$otherUserId/chat"
|
||||||
|
|
||||||
|
fun profileRoute(otherUserId: Int) = "dm/$otherUserId/profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DM_AVATAR_KEY_PREFIX = "dm-avatar-"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DmChatRoute(
|
||||||
|
otherUserId: Int,
|
||||||
|
navController: NavController,
|
||||||
|
sharedTransitionScope: SharedTransitionScope,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val panel = remember(otherUserId, scope) { DmPanelCache.getOrCreate(otherUserId, scope) }
|
||||||
|
val haptic = rememberHapticFeedback()
|
||||||
|
val sharedAvatarKey = remember(otherUserId) { "$DM_AVATAR_KEY_PREFIX$otherUserId" }
|
||||||
|
|
||||||
|
DmScreen(
|
||||||
|
panel = panel,
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
onTitleClick = {
|
||||||
|
haptic(HapticFeedbackEvent.ProfileOpened)
|
||||||
|
navController.navigate(DmNav.profileRoute(otherUserId))
|
||||||
|
},
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedAvatarKey = sharedAvatarKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DmProfileRoute(
|
||||||
|
otherUserId: Int,
|
||||||
|
navController: NavController,
|
||||||
|
sharedTransitionScope: SharedTransitionScope,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val panel = remember(otherUserId, scope) { DmPanelCache.getOrCreate(otherUserId, scope) }
|
||||||
|
val haptic = rememberHapticFeedback()
|
||||||
|
val sharedAvatarKey = remember(otherUserId) { "$DM_AVATAR_KEY_PREFIX$otherUserId" }
|
||||||
|
val stateSnapshot = panel.getState()
|
||||||
|
val initialDisplayName = stateSnapshot.titleAvatar?.displayName?.takeIf { it.isNotBlank() }
|
||||||
|
?: stateSnapshot.title.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
ProfileScreen(
|
||||||
|
userId = otherUserId,
|
||||||
|
onBack = {
|
||||||
|
haptic(HapticFeedbackEvent.ProfileClosed)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onChat = {
|
||||||
|
haptic(HapticFeedbackEvent.ProfileClosed)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedAvatarKey = sharedAvatarKey,
|
||||||
|
initialDisplayName = initialDisplayName
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ import ru.fromchat.api.db.MessageCacheStore
|
|||||||
import ru.fromchat.net.NetworkConnectivity
|
import ru.fromchat.net.NetworkConnectivity
|
||||||
import ru.fromchat.ui.ConnectingEllipsis
|
import ru.fromchat.ui.ConnectingEllipsis
|
||||||
import ru.fromchat.ui.LocalNavController
|
import ru.fromchat.ui.LocalNavController
|
||||||
|
import ru.fromchat.ui.dm.DmNav
|
||||||
import ru.fromchat.ui.branding.FromChatBrandTitle
|
import ru.fromchat.ui.branding.FromChatBrandTitle
|
||||||
import ru.fromchat.ui.chat.Avatar
|
import ru.fromchat.ui.chat.Avatar
|
||||||
|
|
||||||
@@ -287,7 +288,7 @@ fun ChatsTab() {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (conv.otherUserId != 0) {
|
if (conv.otherUserId != 0) {
|
||||||
navController.navigate("dm/${conv.otherUserId}")
|
navController.navigate(DmNav.chatRoute(conv.otherUserId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package ru.fromchat.ui.main
|
package ru.fromchat.ui.main
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||||
import androidx.compose.material.icons.filled.Contacts
|
import androidx.compose.material.icons.filled.Contacts
|
||||||
@@ -18,11 +19,9 @@ import androidx.compose.material3.NavigationBarItem
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.*
|
import ru.fromchat.*
|
||||||
@@ -30,35 +29,56 @@ import ru.fromchat.api.ApiClient
|
|||||||
import ru.fromchat.utils.exclude
|
import ru.fromchat.utils.exclude
|
||||||
import ru.fromchat.ui.profile.ProfileScreen
|
import ru.fromchat.ui.profile.ProfileScreen
|
||||||
|
|
||||||
|
private const val PAGE_CHATS = 0
|
||||||
|
private const val PAGE_CONTACTS = 1
|
||||||
|
private const val PAGE_SETTINGS = 2
|
||||||
|
private const val PAGE_PROFILE = 3
|
||||||
|
private const val PAGE_COUNT = 4
|
||||||
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(onLogout: () -> Unit = {}) {
|
fun MainScreen(onLogout: () -> Unit = {}) {
|
||||||
var selectedTab by rememberSaveable { mutableStateOf("chats") }
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = PAGE_CHATS,
|
||||||
|
pageCount = { PAGE_COUNT },
|
||||||
|
)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Pager is the single source of truth; tabs only call animateScrollToPage (no write/read loop).
|
||||||
|
val selectedPage = pagerState.currentPage
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == "chats",
|
selected = selectedPage == PAGE_CHATS,
|
||||||
onClick = { selectedTab = "chats" },
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_CHATS) }
|
||||||
|
},
|
||||||
label = { Text(stringResource(Res.string.chats)) },
|
label = { Text(stringResource(Res.string.chats)) },
|
||||||
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
|
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == "contacts",
|
selected = selectedPage == PAGE_CONTACTS,
|
||||||
onClick = { selectedTab = "contacts" },
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_CONTACTS) }
|
||||||
|
},
|
||||||
label = { Text(stringResource(Res.string.contacts)) },
|
label = { Text(stringResource(Res.string.contacts)) },
|
||||||
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
|
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == "settings",
|
selected = selectedPage == PAGE_SETTINGS,
|
||||||
onClick = { selectedTab = "settings" },
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_SETTINGS) }
|
||||||
|
},
|
||||||
label = { Text(stringResource(Res.string.settings)) },
|
label = { Text(stringResource(Res.string.settings)) },
|
||||||
icon = { Icon(Icons.Filled.Settings, contentDescription = null) }
|
icon = { Icon(Icons.Filled.Settings, contentDescription = null) }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == "profile",
|
selected = selectedPage == PAGE_PROFILE,
|
||||||
onClick = { selectedTab = "profile" },
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_PROFILE) }
|
||||||
|
},
|
||||||
label = { Text(stringResource(Res.string.profile)) },
|
label = { Text(stringResource(Res.string.profile)) },
|
||||||
icon = { Icon(Icons.Filled.Person, contentDescription = null) }
|
icon = { Icon(Icons.Filled.Person, contentDescription = null) }
|
||||||
)
|
)
|
||||||
@@ -67,29 +87,30 @@ fun MainScreen(onLogout: () -> Unit = {}) {
|
|||||||
contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top),
|
contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top),
|
||||||
modifier = Modifier.imePadding()
|
modifier = Modifier.imePadding()
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
HorizontalPager(
|
||||||
Modifier
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding),
|
||||||
) {
|
beyondViewportPageCount = 1,
|
||||||
when (selectedTab) {
|
) { page ->
|
||||||
"chats" -> ChatsTab()
|
when (page) {
|
||||||
"contacts" -> {
|
PAGE_CHATS -> ChatsTab()
|
||||||
Text(stringResource(Res.string.coming_soon))
|
PAGE_CONTACTS -> Text(stringResource(Res.string.coming_soon))
|
||||||
}
|
PAGE_SETTINGS -> SettingsTab(onLogout = onLogout)
|
||||||
"settings" -> {
|
PAGE_PROFILE -> {
|
||||||
SettingsTab(onLogout = onLogout)
|
|
||||||
}
|
|
||||||
"profile" -> {
|
|
||||||
val currentUserId = ApiClient.user?.id
|
val currentUserId = ApiClient.user?.id
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
userId = currentUserId,
|
userId = currentUserId,
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onChat = { _ -> },
|
onChat = { _ -> },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
onOpenSettings = { selectedTab = "settings" }
|
onOpenSettings = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_SETTINGS) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user