Moves to hold the feed itself in a stateflow

This commit is contained in:
Vitor Pamplona
2024-08-26 17:48:27 -04:00
parent c88dded01b
commit 356740479c
14 changed files with 129 additions and 123 deletions
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.feeds
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
@@ -38,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable @Stable
@@ -79,7 +77,7 @@ class FeedContentState(
val oldNotesState = _feedContent.value val oldNotesState = _feedContent.value
if (oldNotesState is FeedState.Loaded) { if (oldNotesState is FeedState.Loaded) {
if (!equalImmutableLists(notes, oldNotesState.feed.value)) { if (!equalImmutableLists(notes, oldNotesState.feed.value.list)) {
updateFeed(notes) updateFeed(notes)
} }
} else { } else {
@@ -88,21 +86,15 @@ class FeedContentState(
} }
private fun updateFeed(notes: ImmutableList<Note>) { private fun updateFeed(notes: ImmutableList<Note>) {
viewModelScope.launch(Dispatchers.Main) { val currentState = _feedContent.value
val currentState = _feedContent.value if (notes.isEmpty()) {
if (notes.isEmpty()) { _feedContent.tryEmit(FeedState.Empty)
_feedContent.update { FeedState.Empty } } else if (currentState is FeedState.Loaded) {
} else if (currentState is FeedState.Loaded) { currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey()))
// updates the current list } else {
if (currentState.showHidden.value != localFilter.showHiddenKey()) { _feedContent.tryEmit(
currentState.showHidden.value = localFilter.showHiddenKey() FeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))),
} )
currentState.feed.value = notes
} else {
_feedContent.update {
FeedState.Loaded(mutableStateOf(notes), mutableStateOf(localFilter.showHiddenKey()))
}
}
} }
} }
@@ -118,11 +110,11 @@ class FeedContentState(
val oldList = val oldList =
if (deletionEvents.isEmpty()) { if (deletionEvents.isEmpty()) {
oldNotesState.feed.value oldNotesState.feed.value.list
} else { } else {
val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEvents() } val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEvents() }
val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() } val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() }
oldNotesState.feed.value oldNotesState.feed.value.list
.filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) } .filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) }
.toImmutableList() .toImmutableList()
} }
@@ -132,7 +124,7 @@ class FeedContentState(
.updateListWith(oldList, newItems) .updateListWith(oldList, newItems)
.distinctBy { it.idHex } .distinctBy { it.idHex }
.toImmutableList() .toImmutableList()
if (!equalImmutableLists(newList, oldNotesState.feed.value)) { if (!equalImmutableLists(newList, oldNotesState.feed.value.list)) {
updateFeed(newList) updateFeed(newList)
} }
} else if (oldNotesState is FeedState.Empty) { } else if (oldNotesState is FeedState.Empty) {
@@ -28,7 +28,9 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -37,24 +39,26 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun FeedLoaded( fun FeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
listState: LazyListState, listState: LazyListState,
routeForLastRead: String?, routeForLastRead: String?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn( LazyColumn(
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItemPlacement()) { Row(Modifier.fillMaxWidth().animateItemPlacement()) {
NoteCompose( NoteCompose(
item, item,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
isBoostedNote = false, isBoostedNote = false,
isHiddenFeed = state.showHidden.value, isHiddenFeed = items.showHidden,
quotesLeft = 3, quotesLeft = 3,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -20,16 +20,17 @@
*/ */
package com.vitorpamplona.amethyst.ui.feeds package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
sealed class FeedState { sealed class FeedState {
object Loading : FeedState() object Loading : FeedState()
class Loaded( class Loaded(
val feed: MutableState<ImmutableList<Note>>, val feed: MutableStateFlow<LoadedFeedState<Note>>,
val showHidden: MutableState<Boolean>,
) : FeedState() ) : FeedState()
object Empty : FeedState() object Empty : FeedState()
@@ -38,3 +39,9 @@ sealed class FeedState {
val errorMessage: String, val errorMessage: String,
) : FeedState() ) : FeedState()
} }
@Stable
class LoadedFeedState<T>(
val list: ImmutableList<T>,
val showHidden: Boolean,
)
@@ -69,6 +69,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -224,22 +225,24 @@ fun ThreadFeedView(
@Composable @Composable
fun RenderThreadFeed( fun RenderThreadFeed(
noteId: String, noteId: String,
state: FeedState.Loaded, loaded: FeedState.Loaded,
listState: LazyListState, listState: LazyListState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LaunchedEffect(noteId) { LaunchedEffect(noteId) {
// waits to load the thread to scroll to item. // waits to load the thread to scroll to item.
delay(100) delay(100)
val noteForPosition = val noteForPosition =
state.feed.value items.list
.filter { it.idHex == noteId } .filter { it.idHex == noteId }
.firstOrNull() .firstOrNull()
var position = state.feed.value.indexOf(noteForPosition) var position = items.list.indexOf(noteForPosition)
if (position >= 0) { if (position >= 0) {
if (position >= 1 && position < state.feed.value.size - 1) { if (position >= 1 && position < items.list.size - 1) {
position-- // show the replying note position-- // show the replying note
} }
@@ -251,7 +254,7 @@ fun RenderThreadFeed(
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item ->
if (index == 0) { if (index == 0) {
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) { ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
NoteMaster( NoteMaster(
@@ -46,6 +46,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer
@@ -119,12 +120,14 @@ private fun RenderDraftListScreen(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun DraftFeedLoaded( private fun DraftFeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
listState: LazyListState, listState: LazyListState,
routeForLastRead: String?, routeForLastRead: String?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) { if (showDeleteDialog) {
@@ -141,7 +144,7 @@ private fun DraftFeedLoaded(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
accountViewModel.delete(state.feed.value) accountViewModel.delete(items.list)
showDeleteDialog = false showDeleteDialog = false
}, },
) { ) {
@@ -177,7 +180,7 @@ private fun DraftFeedLoaded(
} }
} }
} }
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -196,7 +199,7 @@ private fun DraftFeedLoaded(
modifier = MaterialTheme.colorScheme.maxWidthWithBackground, modifier = MaterialTheme.colorScheme.maxWidthWithBackground,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
isBoostedNote = false, isBoostedNote = false,
isHiddenFeed = state.showHidden.value, isHiddenFeed = items.showHidden,
quotesLeft = 3, quotesLeft = 3,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -40,7 +40,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -118,26 +117,21 @@ fun RenderGalleryFeed(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun GalleryFeedLoaded( private fun GalleryFeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
routeForLastRead: String?, routeForLastRead: String?,
listState: LazyGridState, listState: LazyGridState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = Row(Modifier.fillMaxWidth().animateItemPlacement()) {
remember {
Modifier
.fillMaxWidth()
.animateItemPlacement()
}
Row(defaultModifier) {
GalleryCardCompose( GalleryCardCompose(
baseNote = item, baseNote = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
@@ -1238,12 +1238,7 @@ private fun DisplayAppRecommendations(
Column { Column {
Text(stringRes(id = R.string.recommended_apps)) Text(stringRes(id = R.string.recommended_apps))
FlowRow( Recommends(state, nav)
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(vertical = 5.dp),
) {
state.feed.value.forEach { app -> WatchApp(app, nav) }
}
} }
} }
else -> {} else -> {}
@@ -1251,6 +1246,21 @@ private fun DisplayAppRecommendations(
} }
} }
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun Recommends(
loaded: FeedState.Loaded,
nav: (String) -> Unit,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
FlowRow(
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(vertical = 5.dp),
) {
items.list.forEach { app -> WatchApp(app, nav) }
}
}
@Composable @Composable
private fun WatchApp( private fun WatchApp(
baseApp: Note, baseApp: Note,
@@ -122,7 +122,7 @@ fun RenderChatroomFeedView(
@Composable @Composable
fun ChatroomFeedLoaded( fun ChatroomFeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -131,7 +131,9 @@ fun ChatroomFeedLoaded(
onWantsToEditDraft: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit,
avoidDraft: String? = null, avoidDraft: String? = null,
) { ) {
LaunchedEffect(state.feed.value.firstOrNull()) { val items by loaded.feed.collectAsStateWithLifecycle()
LaunchedEffect(items.list.firstOrNull()) {
if (listState.firstVisibleItemIndex <= 1) { if (listState.firstVisibleItemIndex <= 1) {
listState.animateScrollToItem(0) listState.animateScrollToItem(0)
} }
@@ -143,7 +145,7 @@ fun ChatroomFeedLoaded(
reverseLayout = true, reverseLayout = true,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
val noteEvent = item.event val noteEvent = item.event
if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() != avoidDraft) { if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() != avoidDraft) {
ChatroomMessageCompose( ChatroomMessageCompose(
@@ -88,16 +88,18 @@ private fun CrossFadeState(
@Composable @Composable
private fun FeedLoaded( private fun FeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
markAsRead: MutableState<Boolean>, markAsRead: MutableState<Boolean>,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
val listState = rememberLazyListState() val listState = rememberLazyListState()
LaunchedEffect(key1 = markAsRead.value) { LaunchedEffect(key1 = markAsRead.value) {
if (markAsRead.value) { if (markAsRead.value) {
accountViewModel.markAllAsRead(state.feed.value) { markAsRead.value = false } accountViewModel.markAllAsRead(items.list) { markAsRead.value = false }
} }
} }
@@ -106,7 +108,7 @@ private fun FeedLoaded(
state = listState, state = listState,
) { ) {
itemsIndexed( itemsIndexed(
state.feed.value, items.list,
key = { index, item -> if (index == 0) index else item.idHex }, key = { index, item -> if (index == 0) index else item.idHex },
) { _, item -> ) { _, item ->
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
@@ -347,18 +347,20 @@ fun WatchAccountForDiscoveryScreen(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun DiscoverFeedLoaded( private fun DiscoverFeedLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
routeForLastRead: String?, routeForLastRead: String?,
listState: LazyListState, listState: LazyListState,
forceEventKind: Int?, forceEventKind: Int?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn( LazyColumn(
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
Row(defaultModifier) { Row(defaultModifier) {
@@ -382,19 +384,21 @@ private fun DiscoverFeedLoaded(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun DiscoverFeedColumnsLoaded( private fun DiscoverFeedColumnsLoaded(
state: FeedState.Loaded, loaded: FeedState.Loaded,
routeForLastRead: String?, routeForLastRead: String?,
listState: LazyGridState, listState: LazyGridState,
forceEventKind: Int?, forceEventKind: Int?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
contentPadding = FeedPadding, contentPadding = FeedPadding,
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
Row(defaultModifier) { Row(defaultModifier) {
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -34,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrderCard
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState
import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent
@@ -51,7 +51,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@@ -109,13 +108,13 @@ class CardFeedContentState(
lastAccount = (localFilter as? NotificationFeedFilter)?.account lastAccount = (localFilter as? NotificationFeedFilter)?.account
val updatedCards = val updatedCards =
(oldNotesState.feed.value + newCards) (oldNotesState.feed.value.list + newCards)
.distinctBy { it.id() } .distinctBy { it.id() }
.sortedWith(DefaultFeedOrderCard) .sortedWith(DefaultFeedOrderCard)
.take(localFilter.limit()) .take(localFilter.limit())
.toImmutableList() .toImmutableList()
if (!equalImmutableLists(oldNotesState.feed.value, updatedCards)) { if (!equalImmutableLists(oldNotesState.feed.value.list, updatedCards)) {
updateFeed(updatedCards) updateFeed(updatedCards)
} }
} }
@@ -291,21 +290,15 @@ class CardFeedContentState(
} }
private fun updateFeed(notes: ImmutableList<Card>) { private fun updateFeed(notes: ImmutableList<Card>) {
viewModelScope.launch(Dispatchers.Main) { val currentState = _feedContent.value
val currentState = _feedContent.value if (notes.isEmpty()) {
_feedContent.tryEmit(CardFeedState.Empty)
if (notes.isEmpty()) { } else if (currentState is CardFeedState.Loaded) {
_feedContent.update { CardFeedState.Empty } currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey()))
} else if (currentState is CardFeedState.Loaded) { } else {
if (currentState.showHidden.value != localFilter.showHiddenKey()) { _feedContent.tryEmit(
currentState.showHidden.value = localFilter.showHiddenKey() CardFeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))),
} )
currentState.feed.value = notes
} else {
_feedContent.update {
CardFeedState.Loaded(mutableStateOf(notes), mutableStateOf(localFilter.showHiddenKey()))
}
}
} }
} }
@@ -336,14 +329,14 @@ class CardFeedContentState(
lastAccount = (localFilter as? NotificationFeedFilter)?.account lastAccount = (localFilter as? NotificationFeedFilter)?.account
val updatedCards = val updatedCards =
(oldNotesState.feed.value + newCards) (oldNotesState.feed.value.list + newCards)
.distinctBy { it.id() } .distinctBy { it.id() }
.sortedWith(compareBy({ it.createdAt() }, { it.id() })) .sortedWith(compareBy({ it.createdAt() }, { it.id() }))
.reversed() .reversed()
.take(localFilter.limit()) .take(localFilter.limit())
.toImmutableList() .toImmutableList()
if (!equalImmutableLists(oldNotesState.feed.value, updatedCards)) { if (!equalImmutableLists(oldNotesState.feed.value.list, updatedCards)) {
updateFeed(updatedCards) updateFeed(updatedCards)
} }
} }
@@ -21,15 +21,16 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.MutableStateFlow
@Immutable @Immutable
abstract class Card { abstract class Card {
@@ -119,8 +120,7 @@ sealed class CardFeedState {
@Stable @Stable
class Loaded( class Loaded(
val feed: MutableState<ImmutableList<Card>>, val feed: MutableStateFlow<LoadedFeedState<Card>>,
val showHidden: MutableState<Boolean>,
) : CardFeedState() ) : CardFeedState()
@Immutable object Empty : CardFeedState() @Immutable object Empty : CardFeedState()
@@ -33,7 +33,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -115,7 +114,7 @@ fun RenderCardFeed(
} }
is CardFeedState.Loaded -> { is CardFeedState.Loaded -> {
FeedLoaded( FeedLoaded(
state = state, loaded = state,
listState = listState, listState = listState,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -132,12 +131,14 @@ fun RenderCardFeed(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun FeedLoaded( private fun FeedLoaded(
state: CardFeedState.Loaded, loaded: CardFeedState.Loaded,
listState: LazyListState, listState: LazyListState,
routeForLastRead: String, routeForLastRead: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = FeedPadding, contentPadding = FeedPadding,
@@ -148,22 +149,15 @@ private fun FeedLoaded(
} }
itemsIndexed( itemsIndexed(
items = state.feed.value, items = items.list,
key = { _, item -> item.id() }, key = { _, item -> item.id() },
contentType = { _, item -> item.javaClass.simpleName }, contentType = { _, item -> item.javaClass.simpleName },
) { _, item -> ) { _, item ->
val defaultModifier = Row(Modifier.fillMaxWidth().animateItemPlacement()) {
remember {
Modifier
.fillMaxWidth()
.animateItemPlacement()
}
Row(defaultModifier) {
RenderCardItem( RenderCardItem(
item, item,
routeForLastRead, routeForLastRead,
showHidden = state.showHidden.value, showHidden = items.showHidden,
accountViewModel, accountViewModel,
nav, nav,
) )
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -42,7 +41,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -101,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent
import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun VideoScreen( fun VideoScreen(
@@ -183,28 +180,18 @@ fun RenderPage(
} }
@Composable @Composable
@OptIn(ExperimentalFoundationApi::class)
private fun LoadedState( private fun LoadedState(
state: FeedState.Loaded, loaded: FeedState.Loaded,
pagerStateKey: String?, pagerStateKey: String?,
videoFeedContentState: FeedContentState, videoFeedContentState: FeedContentState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val pagerState =
if (pagerStateKey != null) {
rememberForeverPagerState(pagerStateKey) { state.feed.value.size }
} else {
rememberPagerState { state.feed.value.size }
}
WatchScrollToTop(videoFeedContentState, pagerState)
RefresheableBox(invalidateableContent = videoFeedContentState) { RefresheableBox(invalidateableContent = videoFeedContentState) {
SlidingCarousel( SlidingCarousel(
state.feed, loaded,
pagerState, pagerStateKey,
state.showHidden.value, videoFeedContentState,
accountViewModel, accountViewModel,
nav, nav,
) )
@@ -214,25 +201,36 @@ private fun LoadedState(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun SlidingCarousel( fun SlidingCarousel(
feed: MutableState<ImmutableList<Note>>, loaded: FeedState.Loaded,
pagerState: PagerState, pagerStateKey: String?,
showHidden: Boolean, videoFeedContentState: FeedContentState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val items by loaded.feed.collectAsStateWithLifecycle()
val pagerState =
if (pagerStateKey != null) {
rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
} else {
rememberPagerState(items.list.size) { items.list.size }
}
WatchScrollToTop(videoFeedContentState, pagerState)
VerticalPager( VerticalPager(
state = pagerState, state = pagerState,
beyondBoundsPageCount = 1, beyondBoundsPageCount = 1,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
key = { index -> feed.value.getOrNull(index)?.idHex ?: "$index" }, key = { index -> items.list.getOrNull(index)?.idHex ?: "$index" },
) { index -> ) { index ->
feed.value.getOrNull(index)?.let { note -> items.list.getOrNull(index)?.let { note ->
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CheckHiddenFeedWatchBlockAndReport( CheckHiddenFeedWatchBlockAndReport(
note = note, note = note,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
showHiddenWarning = true, showHiddenWarning = true,
ignoreAllBlocksAndReports = showHidden, ignoreAllBlocksAndReports = items.showHidden,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) { ) {