Hoist globalfeed state out of Screen component
This commit is contained in:
@@ -83,9 +83,9 @@ fun keyboardAsState(): State<Keyboard> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
|
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
|
||||||
val currentRoute = currentRoute(navController)?.substringBefore("?")
|
val currentRoute = currentRoute(navController)
|
||||||
|
val currentRouteBase = currentRoute?.substringBefore("?")
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val isKeyboardOpen by keyboardAsState()
|
val isKeyboardOpen by keyboardAsState()
|
||||||
|
|
||||||
if (isKeyboardOpen == Keyboard.Closed) {
|
if (isKeyboardOpen == Keyboard.Closed) {
|
||||||
@@ -99,13 +99,15 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
backgroundColor = MaterialTheme.colors.background
|
backgroundColor = MaterialTheme.colors.background
|
||||||
) {
|
) {
|
||||||
bottomNavigationItems.forEach { item ->
|
bottomNavigationItems.forEach { item ->
|
||||||
|
val selected = currentRouteBase == item.base
|
||||||
|
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
icon = { NotifiableIcon(item, currentRoute, accountViewModel) },
|
icon = { NotifiableIcon(item, selected, accountViewModel) },
|
||||||
selected = currentRoute == item.base,
|
selected = selected,
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (currentRoute != item.base) {
|
if (currentRouteBase != item.base) {
|
||||||
navController.navigate(item.route) {
|
navController.navigate(item.base) {
|
||||||
navController.graph.startDestinationRoute?.let { start ->
|
navController.graph.startDestinationRoute?.let { start ->
|
||||||
popUpTo(start)
|
popUpTo(start)
|
||||||
restoreState = true
|
restoreState = true
|
||||||
@@ -114,7 +116,8 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
restoreState = true
|
restoreState = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navController.navigate("${item.base}?forceRefresh=${true}") {
|
val route = currentRoute.replace("{scrollToTop}", "true")
|
||||||
|
navController.navigate(route) {
|
||||||
navController.graph.startDestinationRoute?.let { start ->
|
navController.graph.startDestinationRoute?.let { start ->
|
||||||
popUpTo(start) { inclusive = item.route == Route.Home.route }
|
popUpTo(start) { inclusive = item.route == Route.Home.route }
|
||||||
restoreState = true
|
restoreState = true
|
||||||
@@ -134,13 +137,13 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) {
|
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
|
||||||
Box(Modifier.size(if ("Home" == item.base) 25.dp else 23.dp)) {
|
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = item.icon),
|
painter = painterResource(id = route.icon),
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp),
|
modifier = Modifier.size(if ("Home" == route.base) 24.dp else 20.dp),
|
||||||
tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified
|
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
|
||||||
)
|
)
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
@@ -159,13 +162,13 @@ private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel:
|
|||||||
|
|
||||||
LaunchedEffect(key1 = notif) {
|
LaunchedEffect(key1 = notif) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
hasNewItems = item.hasNewItems(account, notif.cache, context)
|
hasNewItems = route.hasNewItems(account, notif.cache, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
LaunchedEffect(key1 = db) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
hasNewItems = item.hasNewItems(account, notif.cache, context)
|
hasNewItems = route.hasNewItems(account, notif.cache, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
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.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
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.AccountStateViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
@@ -14,9 +20,28 @@ fun AppNavigation(
|
|||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
nextPage: String? = null
|
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) {
|
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 {
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.HomeScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
||||||
|
|
||||||
sealed class Route(
|
sealed class Route(
|
||||||
@@ -33,32 +32,25 @@ sealed class Route(
|
|||||||
val icon: Int,
|
val icon: Int,
|
||||||
val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false },
|
val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false },
|
||||||
val arguments: List<NamedNavArgument> = emptyList(),
|
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
|
val base: String
|
||||||
get() = route.substringBefore("?")
|
get() = route.substringBefore("?")
|
||||||
|
|
||||||
object Home : Route(
|
object Home : Route(
|
||||||
"Home?forceRefresh={forceRefresh}",
|
"Home?scrollToTop={scrollToTop}",
|
||||||
R.drawable.ic_home,
|
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) },
|
hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) },
|
||||||
buildScreen = { accountViewModel, _, navController ->
|
buildScreen = { accountViewModel, _, navController ->
|
||||||
{ backStackEntry ->
|
{ HomeScreen(accountViewModel, navController, it.arguments?.getBoolean("scrollToTop", false) ?: false) }
|
||||||
HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
object Search : Route(
|
object Search : Route(
|
||||||
"Search?forceRefresh={forceRefresh}",
|
route = "Search?scrollToTop={scrollToTop}",
|
||||||
R.drawable.ic_globe,
|
icon = R.drawable.ic_globe,
|
||||||
arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }),
|
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false })
|
||||||
buildScreen = { acc, _, navController ->
|
|
||||||
{ backStackEntry ->
|
|
||||||
SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
object Notification : Route(
|
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.
|
// * Functions below only exist because we have not broken the datasource classes into backend and frontend.
|
||||||
// **
|
// **
|
||||||
@Composable
|
@Composable
|
||||||
public fun currentRoute(navController: NavHostController): String? {
|
fun currentRoute(navController: NavHostController): String? {
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
return navBackStackEntry?.destination?.route
|
return navBackStackEntry?.destination?.route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ fun FeedView(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
routeForLastRead: String?,
|
routeForLastRead: String?,
|
||||||
scrollStateKey: String? = null,
|
scrollStateKey: String? = null,
|
||||||
forceRefresh: Boolean = false
|
scrollToTop: Boolean = false
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsState()
|
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 refresh = { refreshing = true; viewModel.refresh(); refreshing = false }
|
||||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ fun FeedView(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
navController,
|
navController,
|
||||||
scrollStateKey,
|
scrollStateKey,
|
||||||
forceRefresh
|
scrollToTop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ private fun FeedLoaded(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
scrollStateKey: String?,
|
scrollStateKey: String?,
|
||||||
forceRefresh: Boolean = false
|
scrollToTop: Boolean = false
|
||||||
) {
|
) {
|
||||||
val listState = if (scrollStateKey != null) {
|
val listState = if (scrollStateKey != null) {
|
||||||
rememberForeverLazyListState(scrollStateKey)
|
rememberForeverLazyListState(scrollStateKey)
|
||||||
@@ -107,9 +107,9 @@ private fun FeedLoaded(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceRefresh) {
|
if (scrollToTop) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
listState.animateScrollToItem(0)
|
listState.scrollToItem(index = 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@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 accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController,
|
|||||||
}
|
}
|
||||||
HorizontalPager(count = 2, state = pagerState) {
|
HorizontalPager(count = 2, state = pagerState) {
|
||||||
when (pagerState.currentPage) {
|
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)
|
1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.RoboHashCache
|
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.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
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.ChannelName
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
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 com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -79,12 +77,12 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.coroutines.channels.Channel as CoroutineChannel
|
import kotlinx.coroutines.channels.Channel as CoroutineChannel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) {
|
fun SearchScreen(
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
accountViewModel: AccountViewModel,
|
||||||
val account = accountState?.account ?: return
|
feedViewModel: FeedViewModel,
|
||||||
|
navController: NavController,
|
||||||
GlobalFeedFilter.account = account
|
scrollToTop: Boolean = false
|
||||||
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
|
) {
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel) {
|
||||||
@@ -115,7 +113,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
|
|||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
SearchBar(accountViewModel, navController)
|
SearchBar(accountViewModel, navController)
|
||||||
FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh)
|
FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user