From be19d0a9cf3cc74a6ba057ba199fa0c6a95d4223 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 10:55:59 +0800 Subject: [PATCH 1/9] Add persistent lazy list state composable --- .../amethyst/ui/screen/LazyListState.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt new file mode 100644 index 000000000..e6eb0c39e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt @@ -0,0 +1,34 @@ +package com.vitorpamplona.amethyst.ui.screen + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.saveable.rememberSaveable + +private val savedScrollStates = mutableMapOf() +private data class ScrollState(val index: Int, val scrollOffset: Int) + +@Composable +fun rememberForeverLazyListState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Int = 0 +): LazyListState { + val scrollState = rememberSaveable(saver = LazyListState.Saver) { + val savedValue = savedScrollStates[key] + val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex + val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset + LazyListState( + savedIndex, + savedOffset + ) + } + DisposableEffect(Unit) { + onDispose { + val lastIndex = scrollState.firstVisibleItemIndex + val lastOffset = scrollState.firstVisibleItemScrollOffset + savedScrollStates[key] = ScrollState(lastIndex, lastOffset) + } + } + return scrollState +} From 5da70a0f9554453aed0e3abd9d84a196700c9edf Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:00:18 +0800 Subject: [PATCH 2/9] Persist lazy list state on Home, Global and Chatrooms --- .../amethyst/ui/screen/ChatroomFeedView.kt | 5 ++--- .../vitorpamplona/amethyst/ui/screen/FeedView.kt | 15 +++++++++++---- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 4 ++-- .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 6763b4d7c..805f9abc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -21,12 +20,12 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?, onWantsToReply: (Note) -> Unit) { +fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String, onWantsToReply: (Note) -> Unit) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } - val listState = rememberLazyListState() + val listState = rememberForeverLazyListState(routeForLastRead) LaunchedEffect(isRefreshing) { if (isRefreshing) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index be08602fb..ea412e861 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -36,7 +36,8 @@ fun FeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, - routeForLastRead: String? + routeForLastRead: String?, + scrollStateKey: String? = null ) { val feedState by viewModel.feedContent.collectAsState() @@ -74,7 +75,8 @@ fun FeedView( state, routeForLastRead, accountViewModel, - navController + navController, + scrollStateKey ) } is FeedState.Loading -> { @@ -91,9 +93,14 @@ private fun FeedLoaded( state: FeedState.Loaded, routeForLastRead: String?, accountViewModel: AccountViewModel, - navController: NavController + navController: NavController, + scrollStateKey: String? ) { - val listState = rememberLazyListState() + val listState = if (scrollStateKey != null) { + rememberForeverLazyListState(scrollStateKey) + } else { + rememberLazyListState() + } LazyColumn( contentPadding = PaddingValues( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index fb0809f79..b5a8e35aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -106,8 +106,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows") - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies") + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows", Route.Home.route + "Follows") + 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies", Route.Home.route + "FollowsReplies") } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 444e46271..b9000e89b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -114,7 +114,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController, null) + FeedView(feedViewModel, accountViewModel, navController, null, "Global") } } } From 7630e07dc9b065ae9d384564a431528b0ddef54d Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:09:03 +0800 Subject: [PATCH 3/9] Replace deprecated SwipeRefresh with PullRefresh --- .../amethyst/ui/screen/CardFeedView.kt | 37 +++++++------- .../amethyst/ui/screen/ChatroomFeedView.kt | 3 +- .../ui/screen/ChatroomListFeedView.kt | 49 ++++++++----------- .../amethyst/ui/screen/FeedView.kt | 47 +++++++++--------- .../amethyst/ui/screen/LnZapFeedView.kt | 37 +++++++------- .../amethyst/ui/screen/RelayFeedView.kt | 32 ++++++------ .../amethyst/ui/screen/ThreadFeedView.kt | 34 ++++++------- .../amethyst/ui/screen/UserFeedView.kt | 37 +++++++------- 8 files changed, 126 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index df4fa0ebf..9b77d4156 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -2,22 +2,26 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.BoostSetCompose import com.vitorpamplona.amethyst.ui.note.LikeSetCompose @@ -27,40 +31,31 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is CardFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is CardFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is CardFeedState.Loaded -> { + refreshing = false FeedLoaded( state, accountViewModel, @@ -74,6 +69,8 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 805f9abc5..8ba6eb059 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -62,8 +62,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode reverseLayout = true, state = listState ) { - var previousDate: String = "" - itemsIndexed(state.feed.value, key = { index, item -> item.idHex }) { index, item -> + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController, onWantsToReply = onWantsToReply) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 1bc51f58d..743bd4f5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -2,11 +2,16 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -14,18 +19,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.ui.note.ChatroomCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun ChatroomListFeedView( viewModel: FeedViewModel, @@ -35,24 +40,11 @@ fun ChatroomListFeedView( ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade( targetState = feedState, @@ -61,17 +53,18 @@ fun ChatroomListFeedView( when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is FeedState.Loaded -> { + refreshing = false FeedLoaded(state, accountViewModel, navController, markAsRead) } @@ -81,6 +74,8 @@ fun ChatroomListFeedView( } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -103,11 +98,9 @@ private fun FeedLoaded( if (markAsRead.value) { for (note in state.feed.value) { note.event?.let { - var route = "" val channel = note.channel() - - if (channel != null) { - route = "Channel/${channel.idHex}" + val route = if (channel != null) { + "Channel/${channel.idHex}" } else { val replyAuthorBase = note.mentions?.first() var userToComposeOn = note.author!! @@ -116,7 +109,7 @@ private fun FeedLoaded( userToComposeOn = replyAuthorBase } } - route = "Room/${userToComposeOn.pubkeyHex}" + "Room/${userToComposeOn.pubkeyHex}" } notificationCache.cache.markAsRead(route, it.createdAt(), context) @@ -136,7 +129,7 @@ private fun FeedLoaded( itemsIndexed( state.feed.value, key = { index, item -> if (index == 0) index else item.idHex } - ) { index, item -> + ) { _, item -> ChatroomCompose( item, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index ea412e861..aed87aaee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight @@ -11,10 +12,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.OutlinedButton import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,12 +29,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun FeedView( viewModel: FeedViewModel, @@ -41,36 +44,31 @@ fun FeedView( ) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { - Column() { - Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> + Box(Modifier.pullRefresh(pullRefreshState)) { + Column { + Crossfade( + targetState = feedState, + animationSpec = tween(durationMillis = 100) + ) { state -> when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } + is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } + is FeedState.Loaded -> { + refreshing = false FeedLoaded( state, routeForLastRead, @@ -79,12 +77,15 @@ fun FeedView( scrollStateKey ) } + is FeedState.Loading -> { LoadingFeed() } } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -109,7 +110,7 @@ private fun FeedLoaded( ), state = listState ) { - itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item -> + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> NoteCompose( item, isBoostedNote = false, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt index 4570bef66..d4d8767bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt @@ -2,59 +2,54 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun LnZapFeedView(viewModel: LnZapFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is LnZapFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is LnZapFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is LnZapFeedState.Loaded -> { + refreshing = false LnZapFeedLoaded(state, accountViewModel, navController) } is LnZapFeedState.Loading -> { @@ -63,6 +58,8 @@ fun LnZapFeedView(viewModel: LnZapFeedViewModel, accountViewModel: AccountViewMo } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 2461b982f..2b27ef43c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -1,24 +1,28 @@ package com.vitorpamplona.amethyst.ui.screen +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState @@ -101,6 +105,7 @@ class RelayFeedViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val accountState by accountViewModel.accountLiveData.observeAsState() @@ -108,9 +113,6 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) - var wantsToAddRelay by remember { mutableStateOf("") } @@ -119,19 +121,11 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo NewRelayListView({ wantsToAddRelay = "" }, account, wantsToAddRelay) } - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { val listState = rememberLazyListState() @@ -153,5 +147,7 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index aeffef514..d9a5a667b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -16,12 +17,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -44,8 +49,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent @@ -65,42 +68,33 @@ import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.delay +@OptIn(ExperimentalMaterialApi::class) @Composable fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) - val listState = rememberLazyListState() - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is FeedState.Loaded -> { + refreshing = false LaunchedEffect(noteId) { // waits to load the thread to scroll to item. delay(100) @@ -163,6 +157,8 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index ed9b04b7c..3452677c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -2,59 +2,54 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is UserFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is UserFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is UserFeedState.Loaded -> { + refreshing = false FeedLoaded(state, accountViewModel, navController) } is UserFeedState.Loading -> { @@ -63,6 +58,8 @@ fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewMode } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } From 4fa584ca1a3551b9d3b552b20189ca085b2eef96 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:14:32 +0800 Subject: [PATCH 4/9] Remove swiperefresh dependency --- app/build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0477fed22..b395bd09d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,9 +80,6 @@ dependencies { // Biometrics implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" - // Swipe Refresh - implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha' - // Bitcoin secp256k1 bindings to Android implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.7.1' From 90147ce557a8948330ea9e33ace33f9bf10aaaf2 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 00:59:18 +0800 Subject: [PATCH 5/9] Allow tapping nav icon to refresh & scroll to top --- .../amethyst/ui/navigation/AppBottomBar.kt | 15 ++++++------ .../amethyst/ui/navigation/Routes.kt | 23 +++++++++++++++---- .../amethyst/ui/screen/FeedView.kt | 18 +++++++++++---- .../amethyst/ui/screen/LazyListState.kt | 7 ++++++ .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 7 +++--- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 4 ++-- .../ui/screen/loggedIn/SearchScreen.kt | 5 ++-- 7 files changed, 56 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index f59437670..52c54034c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -83,7 +83,7 @@ fun keyboardAsState(): State { @Composable fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) { - val currentRoute = currentRoute(navController) + val currentRoute = currentRoute(navController)?.substringBefore("?") val coroutineScope = rememberCoroutineScope() val isKeyboardOpen by keyboardAsState() @@ -101,10 +101,10 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView bottomNavigationItems.forEach { item -> BottomNavigationItem( icon = { NotifiableIcon(item, currentRoute, accountViewModel) }, - selected = currentRoute == item.route, + selected = currentRoute == item.base, onClick = { coroutineScope.launch { - if (currentRoute != item.route) { + if (currentRoute != item.base) { navController.navigate(item.route) { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) @@ -114,8 +114,7 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView restoreState = true } } else { - // TODO: Make it scrool to the top - navController.navigate(item.route) { + navController.navigate("${item.base}?forceRefresh=${true}") { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) { inclusive = item.route == Route.Home.route } restoreState = true @@ -136,12 +135,12 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView @Composable 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( painter = painterResource(id = item.icon), null, - modifier = Modifier.size(if ("Home" == item.route) 24.dp else 20.dp), - tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified + modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp), + tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified ) val accountState by accountViewModel.accountLiveData.observeAsState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 66bed57fa..69b7ecba8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -35,17 +35,32 @@ sealed class Route( val arguments: List = emptyList(), val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { + val base: String + get() = route.substringBefore("?") + object Home : Route( - "Home", + "Home?forceRefresh={forceRefresh}", R.drawable.ic_home, + arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), 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( - "Search", + "Search?forceRefresh={forceRefresh}", 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( "Notification", R.drawable.ic_notifications, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index aed87aaee..bca089f1e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -19,6 +19,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,11 +41,12 @@ fun FeedView( accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?, - scrollStateKey: String? = null + scrollStateKey: String? = null, + forceRefresh: Boolean? = false ) { 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 pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) @@ -74,7 +76,8 @@ fun FeedView( routeForLastRead, accountViewModel, navController, - scrollStateKey + scrollStateKey, + forceRefresh!! ) } @@ -95,7 +98,8 @@ private fun FeedLoaded( routeForLastRead: String?, accountViewModel: AccountViewModel, navController: NavController, - scrollStateKey: String? + scrollStateKey: String?, + forceRefresh: Boolean = false ) { val listState = if (scrollStateKey != null) { rememberForeverLazyListState(scrollStateKey) @@ -103,6 +107,12 @@ private fun FeedLoaded( rememberLazyListState() } + if (forceRefresh) { + LaunchedEffect(Unit) { + listState.animateScrollToItem(0) + } + } + LazyColumn( contentPadding = PaddingValues( top = 10.dp, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt index e6eb0c39e..0a4a2d5cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt @@ -4,10 +4,17 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.saveable.rememberSaveable +import com.vitorpamplona.amethyst.ui.navigation.Route private val savedScrollStates = mutableMapOf() 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 fun rememberForeverLazyListState( key: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index b5a8e35aa..0e80382ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -34,11 +34,12 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -106,8 +107,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows", Route.Home.route + "Follows") - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies", Route.Home.route + "FollowsReplies") + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, forceRefresh) + 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index fa698b275..62345d678 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -61,7 +61,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) { 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 -> when (state) { 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 -> when (state) { is AccountState.LoggedInViewOnly -> { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index b9000e89b..f74e4f553 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -66,6 +66,7 @@ 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.ScrollStateKeys import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest @@ -78,7 +79,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.Channel as CoroutineChannel @Composable -fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -114,7 +115,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController, null, "Global") + FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh) } } } From e4cec20d1a2b8a388d36f5979a88e4dc3756d715 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 13:07:01 +0800 Subject: [PATCH 6/9] Make forceRefresh non-null --- .../java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt | 4 ++-- .../java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt | 6 +++--- .../vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index e94e29581..d84b0b1cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -45,7 +45,7 @@ sealed class Route( hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) }, buildScreen = { accountViewModel, _, navController -> { backStackEntry -> - HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) } } ) @@ -56,7 +56,7 @@ sealed class Route( arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), buildScreen = { acc, _, navController -> { backStackEntry -> - SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) } } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index bca089f1e..af98fe35c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -42,11 +42,11 @@ fun FeedView( navController: NavController, routeForLastRead: String?, scrollStateKey: String? = null, - forceRefresh: Boolean? = false + forceRefresh: Boolean = false ) { val feedState by viewModel.feedContent.collectAsState() - var refreshing by remember { mutableStateOf(forceRefresh!!) } + var refreshing by remember { mutableStateOf(forceRefresh) } val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) @@ -77,7 +77,7 @@ fun FeedView( accountViewModel, navController, scrollStateKey, - forceRefresh!! + forceRefresh ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 0e80382ec..203684542 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -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, forceRefresh: Boolean = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index f74e4f553..c4dbfaab3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -79,7 +79,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.Channel as CoroutineChannel @Composable -fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { +fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return From 85d722f96d8ed1183ad45a9ff22677118da84753 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 19:03:15 +0800 Subject: [PATCH 7/9] Hoist globalfeed state out of Screen component --- .../amethyst/ui/navigation/AppBottomBar.kt | 31 ++++++++++--------- .../amethyst/ui/navigation/AppNavigation.kt | 27 +++++++++++++++- .../amethyst/ui/navigation/Routes.kt | 24 +++++--------- .../amethyst/ui/screen/FeedView.kt | 12 +++---- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 4 +-- .../ui/screen/loggedIn/SearchScreen.kt | 18 +++++------ 6 files changed, 67 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 52c54034c..5d68ddba0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -83,9 +83,9 @@ fun keyboardAsState(): State { @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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 76e7164ed..27d2b4b16 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -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)) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index d84b0b1cc..ec7b30a00 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -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 = 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 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index af98fe35c..10453c6df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 203684542..cfed81664 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index c4dbfaab3..f55a5f3a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -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) } } } From 70434eb24440ffe98158b6d06d5b6961c4ffb729 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 20:17:35 +0800 Subject: [PATCH 8/9] Remove Route buildScreen member --- .../amethyst/ui/navigation/AppNavigation.kt | 65 +++++++++++++++- .../amethyst/ui/navigation/Routes.kt | 77 +++---------------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 27d2b4b16..86935b29b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -11,7 +11,15 @@ 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.ChannelScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen +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 @Composable fun AppNavigation( @@ -38,10 +46,59 @@ fun AppNavigation( }) } - Routes.forEach { - it.buildScreen?.let { fn -> - composable(it.route, it.arguments, content = fn(accountViewModel, accountStateViewModel, navController)) - } + Route.Home.let { route -> + composable(route.route, route.arguments, content = { + HomeScreen( + accountViewModel = accountViewModel, + navController = navController, + scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false + ) + }) + } + + composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) }) + composable(Route.Notification.route, content = { NotificationScreen(accountViewModel, navController) }) + composable(Route.Filters.route, content = { FiltersScreen(accountViewModel, navController) }) + + Route.Profile.let { route -> + composable(route.route, route.arguments, content = { + ProfileScreen( + userId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Note.let { route -> + composable(route.route, route.arguments, content = { + ThreadScreen( + noteId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Room.let { route -> + composable(route.route, route.arguments, content = { + ChatroomScreen( + userId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Channel.let { route -> + composable(route.route, route.arguments, content = { + ChannelScreen( + channelId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + accountStateViewModel = accountStateViewModel, + navController = navController + ) + }) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index ec7b30a00..3ee743e87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -4,8 +4,6 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState @@ -16,35 +14,21 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen -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.ThreadScreen sealed class Route( val route: String, val icon: Int, val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false }, - val arguments: List = emptyList(), - val buildScreen: ((AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit)? = null + val arguments: List = emptyList() ) { val base: String get() = route.substringBefore("?") object Home : Route( - "Home?scrollToTop={scrollToTop}", - R.drawable.ic_home, + route = "Home?scrollToTop={scrollToTop}", + icon = R.drawable.ic_home, arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }), - hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) }, - buildScreen = { accountViewModel, _, navController -> - { HomeScreen(accountViewModel, navController, it.arguments?.getBoolean("scrollToTop", false) ?: false) } - } + hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) } ) object Search : Route( @@ -58,9 +42,6 @@ sealed class Route( icon = R.drawable.ic_notifications, hasNewItems = { accountViewModel, cache, context -> notificationHasNewItems(accountViewModel, cache, context) - }, - buildScreen = { accountViewModel, _, navController -> - { NotificationScreen(accountViewModel, navController) } } ) @@ -69,79 +50,39 @@ sealed class Route( icon = R.drawable.ic_dm, hasNewItems = { accountViewModel, cache, context -> messagesHasNewItems(accountViewModel, cache, context) - }, - buildScreen = { accountViewModel, _, navController -> - { ChatroomListScreen(accountViewModel, navController) } } ) object Filters : Route( route = "Filters", - icon = R.drawable.ic_security, - buildScreen = { accountViewModel, _, navController -> - { FiltersScreen(accountViewModel, navController) } - } + icon = R.drawable.ic_security ) object Profile : Route( route = "User/{id}", icon = R.drawable.ic_profile, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ProfileScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Note : Route( route = "Note/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ThreadScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Room : Route( route = "Room/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ChatroomScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Channel : Route( route = "Channel/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, accountStateViewModel, navController -> - { - ChannelScreen( - it.arguments?.getString("id"), - accountViewModel, - accountStateViewModel, - navController - ) - } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) } -val Routes = listOf( - // bottom - Route.Home, - Route.Message, - Route.Search, - Route.Notification, - - // drawer - Route.Profile, - Route.Note, - Route.Room, - Route.Channel, - Route.Filters -) - // ** // * Functions below only exist because we have not broken the datasource classes into backend and frontend. // ** From d8c2e623d1872e7bf54951fb84cc9ddea8ed3a1b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 20:28:30 +0800 Subject: [PATCH 9/9] Hoist HomeScreen state --- .../amethyst/ui/navigation/AppNavigation.kt | 16 ++++++++ .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 40 ++++++++----------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 86935b29b..95f475004 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -7,9 +7,15 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen @@ -21,6 +27,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen +@OptIn(ExperimentalPagerApi::class) @Composable fun AppNavigation( navController: NavHostController, @@ -32,7 +39,13 @@ fun AppNavigation( val account = accountState?.account ?: return GlobalFeedFilter.account = account + HomeNewThreadFeedFilter.account = account + HomeConversationsFeedFilter.account = account + val globalFeedViewModel: NostrGlobalFeedViewModel = viewModel() + val homeFeedViewModel: NostrHomeFeedViewModel = viewModel() + val homeRepliesFeedViewModel: NostrHomeRepliesFeedViewModel = viewModel() + val homePagerState = rememberPagerState() NavHost(navController, startDestination = Route.Home.route) { Route.Search.let { route -> @@ -51,6 +64,9 @@ fun AppNavigation( HomeScreen( accountViewModel = accountViewModel, navController = navController, + homeFeedViewModel = homeFeedViewModel, + repliesFeedViewModel = homeRepliesFeedViewModel, + pagerState = homePagerState, scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false ) }) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index cfed81664..ddf6af5ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -11,8 +11,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner @@ -20,16 +18,13 @@ import androidx.compose.ui.res.stringResource 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.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.pagerTabIndicatorOffset -import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter -import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel @@ -39,33 +34,30 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, scrollToTop: Boolean = false) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - - HomeNewThreadFeedFilter.account = account - HomeConversationsFeedFilter.account = account - - val feedViewModel: NostrHomeFeedViewModel = viewModel() - val feedViewModelReplies: NostrHomeRepliesFeedViewModel = viewModel() - - val pagerState = rememberPagerState() +fun HomeScreen( + accountViewModel: AccountViewModel, + navController: NavController, + homeFeedViewModel: NostrHomeFeedViewModel, + repliesFeedViewModel: NostrHomeRepliesFeedViewModel, + pagerState: PagerState, + scrollToTop: Boolean = false +) { val coroutineScope = rememberCoroutineScope() LaunchedEffect(accountViewModel) { NostrHomeDataSource.resetFilters() - feedViewModel.refresh() - feedViewModelReplies.refresh() + homeFeedViewModel.refresh() + repliesFeedViewModel.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current DisposableEffect(accountViewModel) { - val observer = LifecycleEventObserver { source, event -> + val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.resetFilters() - feedViewModel.refresh() - feedViewModelReplies.refresh() + homeFeedViewModel.refresh() + repliesFeedViewModel.refresh() } } @@ -107,8 +99,8 @@ 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, scrollToTop) - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES) + 0 -> FeedView(homeFeedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop) + 1 -> FeedView(repliesFeedViewModel, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES, scrollToTop) } } }