Hoist globalfeed state out of Screen component

This commit is contained in:
maxmoney21m 2023-03-13 19:03:15 +08:00
parent e4cec20d1a
commit 85d722f96d
6 changed files with 67 additions and 49 deletions

View File

@ -83,9 +83,9 @@ fun keyboardAsState(): State<Keyboard> {
@Composable
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
val currentRoute = currentRoute(navController)?.substringBefore("?")
val currentRoute = currentRoute(navController)
val currentRouteBase = currentRoute?.substringBefore("?")
val coroutineScope = rememberCoroutineScope()
val isKeyboardOpen by keyboardAsState()
if (isKeyboardOpen == Keyboard.Closed) {
@ -99,13 +99,15 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
backgroundColor = MaterialTheme.colors.background
) {
bottomNavigationItems.forEach { item ->
val selected = currentRouteBase == item.base
BottomNavigationItem(
icon = { NotifiableIcon(item, currentRoute, accountViewModel) },
selected = currentRoute == item.base,
icon = { NotifiableIcon(item, selected, accountViewModel) },
selected = selected,
onClick = {
coroutineScope.launch {
if (currentRoute != item.base) {
navController.navigate(item.route) {
if (currentRouteBase != item.base) {
navController.navigate(item.base) {
navController.graph.startDestinationRoute?.let { start ->
popUpTo(start)
restoreState = true
@ -114,7 +116,8 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
restoreState = true
}
} else {
navController.navigate("${item.base}?forceRefresh=${true}") {
val route = currentRoute.replace("{scrollToTop}", "true")
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { start ->
popUpTo(start) { inclusive = item.route == Route.Home.route }
restoreState = true
@ -134,13 +137,13 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
}
@Composable
private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) {
Box(Modifier.size(if ("Home" == item.base) 25.dp else 23.dp)) {
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
Icon(
painter = painterResource(id = item.icon),
painter = painterResource(id = route.icon),
null,
modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp),
tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified
modifier = Modifier.size(if ("Home" == route.base) 24.dp else 20.dp),
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
)
val accountState by accountViewModel.accountLiveData.observeAsState()
@ -159,13 +162,13 @@ private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel:
LaunchedEffect(key1 = notif) {
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
hasNewItems = route.hasNewItems(account, notif.cache, context)
}
}
LaunchedEffect(key1 = db) {
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
hasNewItems = route.hasNewItems(account, notif.cache, context)
}
}

View File

@ -1,11 +1,17 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
@Composable
fun AppNavigation(
@ -14,9 +20,28 @@ fun AppNavigation(
accountStateViewModel: AccountStateViewModel,
nextPage: String? = null
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
GlobalFeedFilter.account = account
val globalFeedViewModel: NostrGlobalFeedViewModel = viewModel()
NavHost(navController, startDestination = Route.Home.route) {
Route.Search.let { route ->
composable(route.route, route.arguments, content = {
SearchScreen(
accountViewModel = accountViewModel,
feedViewModel = globalFeedViewModel,
navController = navController,
scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
)
})
}
Routes.forEach {
composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, accountStateViewModel, navController))
it.buildScreen?.let { fn ->
composable(it.route, it.arguments, content = fn(accountViewModel, accountStateViewModel, navController))
}
}
}

View File

@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
sealed class Route(
@ -33,32 +32,25 @@ sealed class Route(
val icon: Int,
val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false },
val arguments: List<NamedNavArgument> = emptyList(),
val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit
val buildScreen: ((AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit)? = null
) {
val base: String
get() = route.substringBefore("?")
object Home : Route(
"Home?forceRefresh={forceRefresh}",
"Home?scrollToTop={scrollToTop}",
R.drawable.ic_home,
arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }),
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) },
buildScreen = { accountViewModel, _, navController ->
{ backStackEntry ->
HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false)
}
{ HomeScreen(accountViewModel, navController, it.arguments?.getBoolean("scrollToTop", false) ?: false) }
}
)
object Search : Route(
"Search?forceRefresh={forceRefresh}",
R.drawable.ic_globe,
arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }),
buildScreen = { acc, _, navController ->
{ backStackEntry ->
SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false)
}
}
route = "Search?scrollToTop={scrollToTop}",
icon = R.drawable.ic_globe,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false })
)
object Notification : Route(
@ -154,7 +146,7 @@ val Routes = listOf(
// * Functions below only exist because we have not broken the datasource classes into backend and frontend.
// **
@Composable
public fun currentRoute(navController: NavHostController): String? {
fun currentRoute(navController: NavHostController): String? {
val navBackStackEntry by navController.currentBackStackEntryAsState()
return navBackStackEntry?.destination?.route
}

View File

@ -42,11 +42,11 @@ fun FeedView(
navController: NavController,
routeForLastRead: String?,
scrollStateKey: String? = null,
forceRefresh: Boolean = false
scrollToTop: Boolean = false
) {
val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(forceRefresh) }
var refreshing by remember { mutableStateOf(false) }
val refresh = { refreshing = true; viewModel.refresh(); refreshing = false }
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
@ -77,7 +77,7 @@ fun FeedView(
accountViewModel,
navController,
scrollStateKey,
forceRefresh
scrollToTop
)
}
@ -99,7 +99,7 @@ private fun FeedLoaded(
accountViewModel: AccountViewModel,
navController: NavController,
scrollStateKey: String?,
forceRefresh: Boolean = false
scrollToTop: Boolean = false
) {
val listState = if (scrollStateKey != null) {
rememberForeverLazyListState(scrollStateKey)
@ -107,9 +107,9 @@ private fun FeedLoaded(
rememberLazyListState()
}
if (forceRefresh) {
if (scrollToTop) {
LaunchedEffect(Unit) {
listState.animateScrollToItem(0)
listState.scrollToItem(index = 0)
}
}

View File

@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) {
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, scrollToTop: Boolean = false) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
@ -107,7 +107,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController,
}
HorizontalPager(count = 2, state = pagerState) {
when (pagerState.currentPage) {
0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, forceRefresh)
0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop)
1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
}
}

View File

@ -47,7 +47,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
@ -58,14 +57,13 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
import com.vitorpamplona.amethyst.ui.note.ChannelName
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
@ -79,12 +77,12 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.channels.Channel as CoroutineChannel
@Composable
fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
GlobalFeedFilter.account = account
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
fun SearchScreen(
accountViewModel: AccountViewModel,
feedViewModel: FeedViewModel,
navController: NavController,
scrollToTop: Boolean = false
) {
val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(accountViewModel) {
@ -115,7 +113,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
modifier = Modifier.padding(vertical = 0.dp)
) {
SearchBar(accountViewModel, navController)
FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh)
FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop)
}
}
}