mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-06 21:53:20 +02:00
Allow tapping nav icon to refresh & scroll to top
This commit is contained in:
@@ -83,7 +83,7 @@ fun keyboardAsState(): State<Keyboard> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
|
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
|
||||||
val currentRoute = currentRoute(navController)
|
val currentRoute = currentRoute(navController)?.substringBefore("?")
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val isKeyboardOpen by keyboardAsState()
|
val isKeyboardOpen by keyboardAsState()
|
||||||
@@ -101,10 +101,10 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
bottomNavigationItems.forEach { item ->
|
bottomNavigationItems.forEach { item ->
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
icon = { NotifiableIcon(item, currentRoute, accountViewModel) },
|
icon = { NotifiableIcon(item, currentRoute, accountViewModel) },
|
||||||
selected = currentRoute == item.route,
|
selected = currentRoute == item.base,
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (currentRoute != item.route) {
|
if (currentRoute != item.base) {
|
||||||
navController.navigate(item.route) {
|
navController.navigate(item.route) {
|
||||||
navController.graph.startDestinationRoute?.let { start ->
|
navController.graph.startDestinationRoute?.let { start ->
|
||||||
popUpTo(start)
|
popUpTo(start)
|
||||||
@@ -114,8 +114,7 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
restoreState = true
|
restoreState = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Make it scrool to the top
|
navController.navigate("${item.base}?forceRefresh=${true}") {
|
||||||
navController.navigate(item.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
|
||||||
@@ -136,12 +135,12 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) {
|
private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) {
|
||||||
Box(Modifier.size(if ("Home" == item.route) 25.dp else 23.dp)) {
|
Box(Modifier.size(if ("Home" == item.base) 25.dp else 23.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = item.icon),
|
painter = painterResource(id = item.icon),
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(if ("Home" == item.route) 24.dp else 20.dp),
|
modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp),
|
||||||
tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified
|
tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified
|
||||||
)
|
)
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
@@ -35,17 +35,32 @@ sealed class Route(
|
|||||||
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
|
||||||
) {
|
) {
|
||||||
|
val base: String
|
||||||
|
get() = route.substringBefore("?")
|
||||||
|
|
||||||
object Home : Route(
|
object Home : Route(
|
||||||
"Home",
|
"Home?forceRefresh={forceRefresh}",
|
||||||
R.drawable.ic_home,
|
R.drawable.ic_home,
|
||||||
|
arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }),
|
||||||
hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) },
|
hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) },
|
||||||
buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } }
|
buildScreen = { acc, accSt, nav ->
|
||||||
|
{ backStackEntry ->
|
||||||
|
HomeScreen(acc, nav, backStackEntry.arguments?.getBoolean("forceRefresh", false))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
object Search : Route(
|
object Search : Route(
|
||||||
"Search",
|
"Search?forceRefresh={forceRefresh}",
|
||||||
R.drawable.ic_globe,
|
R.drawable.ic_globe,
|
||||||
buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) } }
|
arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }),
|
||||||
|
buildScreen = { acc, accSt, nav ->
|
||||||
|
{ backStackEntry ->
|
||||||
|
SearchScreen(acc, nav, backStackEntry.arguments?.getBoolean("forceRefresh", false))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
object Notification : Route(
|
object Notification : Route(
|
||||||
"Notification",
|
"Notification",
|
||||||
R.drawable.ic_notifications,
|
R.drawable.ic_notifications,
|
||||||
|
@@ -19,6 +19,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
|||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -40,11 +41,12 @@ fun FeedView(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
routeForLastRead: String?,
|
routeForLastRead: String?,
|
||||||
scrollStateKey: String? = null
|
scrollStateKey: String? = null,
|
||||||
|
forceRefresh: Boolean? = false
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsState()
|
val feedState by viewModel.feedContent.collectAsState()
|
||||||
|
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(forceRefresh!!) }
|
||||||
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)
|
||||||
|
|
||||||
@@ -74,7 +76,8 @@ fun FeedView(
|
|||||||
routeForLastRead,
|
routeForLastRead,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
navController,
|
navController,
|
||||||
scrollStateKey
|
scrollStateKey,
|
||||||
|
forceRefresh!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +98,8 @@ private fun FeedLoaded(
|
|||||||
routeForLastRead: String?,
|
routeForLastRead: String?,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
scrollStateKey: String?
|
scrollStateKey: String?,
|
||||||
|
forceRefresh: Boolean = false
|
||||||
) {
|
) {
|
||||||
val listState = if (scrollStateKey != null) {
|
val listState = if (scrollStateKey != null) {
|
||||||
rememberForeverLazyListState(scrollStateKey)
|
rememberForeverLazyListState(scrollStateKey)
|
||||||
@@ -103,6 +107,12 @@ private fun FeedLoaded(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (forceRefresh) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
top = 10.dp,
|
top = 10.dp,
|
||||||
|
@@ -4,10 +4,17 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
|
||||||
private val savedScrollStates = mutableMapOf<String, ScrollState>()
|
private val savedScrollStates = mutableMapOf<String, ScrollState>()
|
||||||
private data class ScrollState(val index: Int, val scrollOffset: Int)
|
private data class ScrollState(val index: Int, val scrollOffset: Int)
|
||||||
|
|
||||||
|
object ScrollStateKeys {
|
||||||
|
const val GLOBAL_SCREEN = "Global"
|
||||||
|
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
||||||
|
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberForeverLazyListState(
|
fun rememberForeverLazyListState(
|
||||||
key: String,
|
key: String,
|
||||||
|
@@ -34,11 +34,12 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) {
|
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
@@ -106,8 +107,8 @@ 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.route + "Follows", Route.Home.route + "Follows")
|
0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, forceRefresh)
|
||||||
1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies", Route.Home.route + "FollowsReplies")
|
1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -61,7 +61,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) {
|
fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) {
|
||||||
val accountState by accountViewModel.accountContent.collectAsState()
|
val accountState by accountViewModel.accountContent.collectAsState()
|
||||||
|
|
||||||
if (currentRoute(navController) == Route.Home.route) {
|
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
||||||
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is AccountState.LoggedInViewOnly -> {
|
is AccountState.LoggedInViewOnly -> {
|
||||||
@@ -77,7 +77,7 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRoute(navController) == Route.Message.route) {
|
if (currentRoute(navController) == Route.Message.base) {
|
||||||
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is AccountState.LoggedInViewOnly -> {
|
is AccountState.LoggedInViewOnly -> {
|
||||||
|
@@ -66,6 +66,7 @@ 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.NostrGlobalFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -78,7 +79,7 @@ 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) {
|
fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
@@ -114,7 +115,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, "Global")
|
FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user