Adds Home-only empty feed screen.

This commit is contained in:
Vitor Pamplona
2024-03-02 15:57:49 -05:00
parent 16c171ec40
commit a301421915
9 changed files with 85 additions and 47 deletions
@@ -54,7 +54,7 @@ fun RefreshingChatroomFeedView(
scrollStateKey: String? = null, scrollStateKey: String? = null,
enablePullRefresh: Boolean = true, enablePullRefresh: Boolean = true,
) { ) {
RefresheableView(viewModel, enablePullRefresh) { RefresheableBox(viewModel, enablePullRefresh) {
SaveableFeedState(viewModel, scrollStateKey) { listState -> SaveableFeedState(viewModel, scrollStateKey) { listState ->
RenderChatroomFeedView( RenderChatroomFeedView(
viewModel, viewModel,
@@ -45,7 +45,7 @@ fun ChatroomListFeedView(
nav: (String) -> Unit, nav: (String) -> Unit,
markAsRead: MutableState<Boolean>, markAsRead: MutableState<Boolean>,
) { ) {
RefresheableView(viewModel, true) { CrossFadeState(viewModel, accountViewModel, nav, markAsRead) } RefresheableBox(viewModel, true) { CrossFadeState(viewModel, accountViewModel, nav, markAsRead) }
} }
@Composable @Composable
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -56,6 +57,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@Composable @Composable
@@ -67,13 +69,15 @@ fun RefresheableFeedView(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
RefresheableView(viewModel, enablePullRefresh) { RefresheableBox(viewModel, enablePullRefresh) {
SaveableFeedState(viewModel, routeForLastRead, scrollStateKey, accountViewModel, nav) SaveableFeedState(viewModel, scrollStateKey) { listState ->
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
}
} }
} }
@Composable @Composable
fun RefresheableView( fun RefresheableBox(
viewModel: InvalidatableViewModel, viewModel: InvalidatableViewModel,
enablePullRefresh: Boolean = true, enablePullRefresh: Boolean = true,
content: @Composable () -> Unit, content: @Composable () -> Unit,
@@ -108,19 +112,6 @@ fun RefresheableView(
} }
} }
@Composable
private fun SaveableFeedState(
viewModel: FeedViewModel,
routeForLastRead: String?,
scrollStateKey: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
SaveableFeedState(viewModel, scrollStateKey) { listState ->
RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
}
}
@Composable @Composable
fun SaveableFeedState( fun SaveableFeedState(
viewModel: FeedViewModel, viewModel: FeedViewModel,
@@ -158,12 +149,16 @@ fun SaveableGridFeedState(
} }
@Composable @Composable
private fun RenderFeed( fun RenderFeedState(
viewModel: FeedViewModel, viewModel: FeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String?, routeForLastRead: String?,
onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) },
onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } },
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
onLoading: @Composable () -> Unit = { LoadingFeed() },
) { ) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle() val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
@@ -172,24 +167,10 @@ private fun RenderFeed(
animationSpec = tween(durationMillis = 100), animationSpec = tween(durationMillis = 100),
) { state -> ) { state ->
when (state) { when (state) {
is FeedState.Empty -> { is FeedState.Empty -> onEmpty()
FeedEmpty { viewModel.invalidateData() } is FeedState.FeedError -> onError(state.errorMessage)
} is FeedState.Loaded -> onLoaded(state)
is FeedState.FeedError -> { is FeedState.Loading -> onLoading()
FeedError(state.errorMessage) { viewModel.invalidateData() }
}
is FeedState.Loaded -> {
FeedLoaded(
state = state,
listState = listState,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
nav = nav,
)
}
is FeedState.Loading -> {
LoadingFeed()
}
} }
} }
} }
@@ -277,6 +258,7 @@ fun FeedError(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text("${stringResource(R.string.error_loading_replies)} $errorMessage") Text("${stringResource(R.string.error_loading_replies)} $errorMessage")
Spacer(modifier = StdVertSpacer)
Button( Button(
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = onRefresh, onClick = onRefresh,
@@ -294,6 +276,7 @@ fun FeedEmpty(onRefresh: () -> Unit) {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text(stringResource(R.string.feed_is_empty)) Text(stringResource(R.string.feed_is_empty))
Spacer(modifier = StdVertSpacer)
OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) } OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) }
} }
} }
@@ -45,7 +45,7 @@ fun RefreshingFeedStringFeedView(
enablePullRefresh: Boolean = true, enablePullRefresh: Boolean = true,
inner: @Composable (String) -> Unit, inner: @Composable (String) -> Unit,
) { ) {
RefresheableView(viewModel, enablePullRefresh) { StringFeedView(viewModel, inner = inner) } RefresheableBox(viewModel, enablePullRefresh) { StringFeedView(viewModel, inner = inner) }
} }
@Composable @Composable
@@ -39,7 +39,7 @@ fun RefreshingFeedUserFeedView(
nav: (String) -> Unit, nav: (String) -> Unit,
enablePullRefresh: Boolean = true, enablePullRefresh: Boolean = true,
) { ) {
RefresheableView(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) } RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) }
} }
@Composable @Composable
@@ -69,7 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
import com.vitorpamplona.amethyst.ui.screen.RefresheableView import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
@@ -196,7 +196,7 @@ private fun DiscoverPages(
} }
HorizontalPager(state = pagerState) { page -> HorizontalPager(state = pagerState) { page ->
RefresheableView(tabs[page].viewModel, true) { RefresheableBox(tabs[page].viewModel, true) {
if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) { if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) {
SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) {
listState -> listState ->
@@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.ui.elements.AddButton
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.StringFeedView import com.vitorpamplona.amethyst.ui.screen.StringFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
@@ -216,7 +216,7 @@ private fun HiddenWordsFeed(
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel, hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
RefresheableView(hiddenWordsViewModel, false) { RefresheableBox(hiddenWordsViewModel, false) {
StringFeedView( StringFeedView(
hiddenWordsViewModel, hiddenWordsViewModel,
post = { AddMuteWordTextField(accountViewModel) }, post = { AddMuteWordTextField(accountViewModel) },
@@ -22,11 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -39,9 +43,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -54,10 +60,14 @@ import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
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.PagerStateKeys import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -181,7 +191,7 @@ private fun HomePages(
} }
HorizontalPager(state = pagerState, userScrollEnabled = false) { page -> HorizontalPager(state = pagerState, userScrollEnabled = false) { page ->
RefresheableFeedView( HomeFeeds(
viewModel = tabs[page].viewModel, viewModel = tabs[page].viewModel,
routeForLastRead = tabs[page].routeForLastRead, routeForLastRead = tabs[page].routeForLastRead,
scrollStateKey = tabs[page].scrollStateKey, scrollStateKey = tabs[page].scrollStateKey,
@@ -191,6 +201,51 @@ private fun HomePages(
} }
} }
@Composable
fun HomeFeeds(
viewModel: FeedViewModel,
routeForLastRead: String?,
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
RefresheableBox(viewModel, enablePullRefresh) {
SaveableFeedState(viewModel, scrollStateKey) { listState ->
RenderFeedState(
viewModel = viewModel,
accountViewModel = accountViewModel,
listState = listState,
nav = nav,
routeForLastRead = routeForLastRead,
onEmpty = { HomeFeedEmpty { viewModel.invalidateData() } },
)
}
}
}
@Preview
@Composable
fun HomeFeedEmptyPreview() {
ThemeComparisonRow(
onDark = { HomeFeedEmpty {} },
onLight = { HomeFeedEmpty {} },
)
}
@Composable
fun HomeFeedEmpty(onRefresh: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(stringResource(R.string.feed_is_empty))
Spacer(modifier = StdVertSpacer)
OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) }
}
}
@Composable @Composable
fun CheckIfUrlIsOnline( fun CheckIfUrlIsOnline(
url: String, url: String,
@@ -86,7 +86,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
@@ -214,7 +214,7 @@ private fun LoadedState(
WatchScrollToTop(videoFeedView, pagerState) WatchScrollToTop(videoFeedView, pagerState)
RefresheableView(viewModel = videoFeedView) { RefresheableBox(viewModel = videoFeedView) {
SlidingCarousel( SlidingCarousel(
state.feed, state.feed,
pagerState, pagerState,