refactors the Watch feed to scroll to the top
This commit is contained in:
@@ -26,7 +26,6 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
|
|||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
@@ -61,7 +60,7 @@ fun SaveableFeedContentState(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchScrollToTopFeedContentState(feedContentState, listState)
|
WatchScrollToTop(feedContentState, listState)
|
||||||
|
|
||||||
content(listState)
|
content(listState)
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,7 @@ fun SaveableGridFeedContentState(
|
|||||||
rememberLazyGridState()
|
rememberLazyGridState()
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchScrollToTopFeedContentState(feedContentState, gridState)
|
WatchScrollToTop(feedContentState, gridState)
|
||||||
|
|
||||||
content(gridState)
|
content(gridState)
|
||||||
}
|
}
|
||||||
@@ -111,33 +110,3 @@ fun RenderFeedContentState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchScrollToTopFeedContentState(
|
|
||||||
feedContentState: FeedContentState,
|
|
||||||
listState: LazyListState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && feedContentState.scrolltoTopPending) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
feedContentState.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchScrollToTopFeedContentState(
|
|
||||||
feedContentState: FeedContentState,
|
|
||||||
listState: LazyGridState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && feedContentState.scrolltoTopPending) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
feedContentState.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.feeds
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchScrollToTop(
|
||||||
|
feedContentState: FeedContentState,
|
||||||
|
listState: LazyListState,
|
||||||
|
) {
|
||||||
|
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && feedContentState.scrolltoTopPending) {
|
||||||
|
listState.scrollToItem(index = 0)
|
||||||
|
feedContentState.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchScrollToTop(
|
||||||
|
feedContentState: FeedContentState,
|
||||||
|
listState: LazyGridState,
|
||||||
|
) {
|
||||||
|
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && feedContentState.scrolltoTopPending) {
|
||||||
|
listState.scrollToItem(index = 0)
|
||||||
|
feedContentState.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchScrollToTop(
|
||||||
|
feedContent: CardFeedContentState,
|
||||||
|
listState: LazyListState,
|
||||||
|
) {
|
||||||
|
val scrollToTop by feedContent.scrollToTop.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && feedContent.scrolltoTopPending) {
|
||||||
|
listState.scrollToItem(index = 0)
|
||||||
|
feedContent.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun WatchScrollToTop(
|
||||||
|
videoFeedContentState: FeedContentState,
|
||||||
|
pagerState: PagerState,
|
||||||
|
) {
|
||||||
|
val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && videoFeedContentState.scrolltoTopPending) {
|
||||||
|
pagerState.scrollToPage(page = 0)
|
||||||
|
videoFeedContentState.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
|
|||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
@@ -35,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
|||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyGridState
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyGridState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -68,7 +68,7 @@ fun SaveableFeedState(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchScrollToTop(viewModel, listState)
|
WatchScrollToTop(viewModel.feedState, listState)
|
||||||
|
|
||||||
content(listState)
|
content(listState)
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ fun SaveableGridFeedState(
|
|||||||
rememberLazyGridState()
|
rememberLazyGridState()
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchScrollToTop(viewModel, gridState)
|
WatchScrollToTop(viewModel.feedState, gridState)
|
||||||
|
|
||||||
content(gridState)
|
content(gridState)
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ fun RenderFeedState(
|
|||||||
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
|
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
|
||||||
onLoading: @Composable () -> Unit = { LoadingFeed() },
|
onLoading: @Composable () -> Unit = { LoadingFeed() },
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
CrossfadeIfEnabled(
|
CrossfadeIfEnabled(
|
||||||
targetState = feedState,
|
targetState = feedState,
|
||||||
@@ -121,33 +121,3 @@ fun RenderFeedState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchScrollToTop(
|
|
||||||
viewModel: FeedViewModel,
|
|
||||||
listState: LazyListState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
viewModel.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchScrollToTop(
|
|
||||||
viewModel: FeedViewModel,
|
|
||||||
listState: LazyGridState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
viewModel.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
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.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -32,8 +31,6 @@ import com.vitorpamplona.amethyst.model.Channel
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
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.checkNotInMainThread
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
|
||||||
@@ -51,21 +48,11 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
|
||||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
|
||||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
|
||||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NostrChannelFeedViewModel(
|
class NostrChannelFeedViewModel(
|
||||||
@@ -92,17 +79,6 @@ class NostrChatroomFeedViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
|
||||||
class NostrVideoFeedViewModel(
|
|
||||||
val account: Account,
|
|
||||||
) : FeedViewModel(VideoFeedFilter(account)) {
|
|
||||||
class Factory(
|
|
||||||
val account: Account,
|
|
||||||
) : ViewModelProvider.Factory {
|
|
||||||
override fun <NostrVideoFeedViewModel : ViewModel> create(modelClass: Class<NostrVideoFeedViewModel>): NostrVideoFeedViewModel = NostrVideoFeedViewModel(account) as NostrVideoFeedViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrThreadFeedViewModel(
|
class NostrThreadFeedViewModel(
|
||||||
account: Account,
|
account: Account,
|
||||||
noteId: String,
|
noteId: String,
|
||||||
@@ -279,144 +255,16 @@ class NostrUserAppRecommendationsFeedViewModel(
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
abstract class FeedViewModel(
|
abstract class FeedViewModel(
|
||||||
val localFilter: FeedFilter<Note>,
|
localFilter: FeedFilter<Note>,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
InvalidatableContent {
|
InvalidatableContent {
|
||||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
val feedState = FeedContentState(localFilter, viewModelScope)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
|
||||||
|
|
||||||
// Simple counter that changes when it needs to invalidate everything
|
fun sendToTop() = feedState.sendToTop()
|
||||||
private val _scrollToTop = MutableStateFlow<Int>(0)
|
|
||||||
val scrollToTop = _scrollToTop.asStateFlow()
|
|
||||||
var scrolltoTopPending = false
|
|
||||||
|
|
||||||
private var lastFeedKey: String? = null
|
suspend fun sentToTop() = feedState.sentToTop()
|
||||||
|
|
||||||
fun sendToTop() {
|
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
|
||||||
if (scrolltoTopPending) return
|
|
||||||
|
|
||||||
scrolltoTopPending = true
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { _scrollToTop.emit(_scrollToTop.value + 1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sentToTop() {
|
|
||||||
scrolltoTopPending = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) { refreshSuspended() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshSuspended() {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
lastFeedKey = localFilter.feedKey()
|
|
||||||
val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList()
|
|
||||||
|
|
||||||
val oldNotesState = _feedContent.value
|
|
||||||
if (oldNotesState is FeedState.Loaded) {
|
|
||||||
if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
|
|
||||||
updateFeed(notes)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateFeed(notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFeed(notes: ImmutableList<Note>) {
|
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
val currentState = _feedContent.value
|
|
||||||
if (notes.isEmpty()) {
|
|
||||||
_feedContent.update { FeedState.Empty }
|
|
||||||
} else if (currentState is FeedState.Loaded) {
|
|
||||||
// updates the current list
|
|
||||||
if (currentState.showHidden.value != localFilter.showHiddenKey()) {
|
|
||||||
currentState.showHidden.value = localFilter.showHiddenKey()
|
|
||||||
}
|
|
||||||
currentState.feed.value = notes
|
|
||||||
} else {
|
|
||||||
_feedContent.update {
|
|
||||||
FeedState.Loaded(mutableStateOf(notes), mutableStateOf(localFilter.showHiddenKey()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshFromOldState(newItems: Set<Note>) {
|
|
||||||
val oldNotesState = _feedContent.value
|
|
||||||
if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) {
|
|
||||||
if (oldNotesState is FeedState.Loaded) {
|
|
||||||
val deletionEvents: List<DeletionEvent> =
|
|
||||||
newItems.mapNotNull {
|
|
||||||
val noteEvent = it.event
|
|
||||||
if (noteEvent is DeletionEvent) noteEvent else null
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldList =
|
|
||||||
if (deletionEvents.isEmpty()) {
|
|
||||||
oldNotesState.feed.value
|
|
||||||
} else {
|
|
||||||
val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEvents() }
|
|
||||||
val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() }
|
|
||||||
oldNotesState.feed.value
|
|
||||||
.filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) }
|
|
||||||
.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val newList =
|
|
||||||
localFilter
|
|
||||||
.updateListWith(oldList, newItems)
|
|
||||||
.distinctBy { it.idHex }
|
|
||||||
.toImmutableList()
|
|
||||||
if (!equalImmutableLists(newList, oldNotesState.feed.value)) {
|
|
||||||
updateFeed(newList)
|
|
||||||
}
|
|
||||||
} else if (oldNotesState is FeedState.Empty) {
|
|
||||||
val newList =
|
|
||||||
localFilter
|
|
||||||
.updateListWith(emptyList(), newItems)
|
|
||||||
.distinctBy { it.idHex }
|
|
||||||
.toImmutableList()
|
|
||||||
if (newList.isNotEmpty()) {
|
|
||||||
updateFeed(newList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Refresh Everything
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Refresh Everything
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
|
||||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
|
||||||
|
|
||||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
bundler.invalidate(ignoreIfDoing) {
|
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkKeysInvalidateDataAndSendToTop() {
|
|
||||||
if (lastFeedKey != localFilter.feedKey()) {
|
|
||||||
bundler.invalidate(false) {
|
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
sendToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateInsertData(newItems: Set<Note>) {
|
|
||||||
bundlerInsert.invalidateList(newItems) { refreshFromOldState(it.flatten().toSet()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var collectorJob: Job? = null
|
private var collectorJob: Job? = null
|
||||||
|
|
||||||
@@ -425,25 +273,13 @@ abstract class FeedViewModel(
|
|||||||
collectorJob =
|
collectorJob =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
checkNotInMainThread()
|
feedState.updateFeedWith(newNotes)
|
||||||
|
|
||||||
if (
|
|
||||||
localFilter is AdditiveFeedFilter &&
|
|
||||||
(_feedContent.value is FeedState.Loaded || _feedContent.value is FeedState.Empty)
|
|
||||||
) {
|
|
||||||
invalidateInsertData(newNotes)
|
|
||||||
} else {
|
|
||||||
// Refresh Everything
|
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
bundlerInsert.cancel()
|
|
||||||
bundler.cancel()
|
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ fun RenderGalleryFeed(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||||
CrossfadeIfEnabled(
|
CrossfadeIfEnabled(
|
||||||
targetState = feedState,
|
targetState = feedState,
|
||||||
animationSpec = tween(durationMillis = 100),
|
animationSpec = tween(durationMillis = 100),
|
||||||
|
|||||||
+1
-1
@@ -1224,7 +1224,7 @@ private fun DisplayAppRecommendations(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val feedState by appRecommendations.feedContent.collectAsStateWithLifecycle()
|
val feedState by appRecommendations.feedState.feedContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(key1 = Unit) { appRecommendations.invalidateData() }
|
LaunchedEffect(key1 = Unit) { appRecommendations.invalidateData() }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ fun RenderChatroomFeedView(
|
|||||||
onWantsToEditDraft: (Note) -> Unit,
|
onWantsToEditDraft: (Note) -> Unit,
|
||||||
avoidDraft: String? = null,
|
avoidDraft: String? = null,
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state ->
|
CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
|
|||||||
+1
-16
@@ -31,7 +31,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
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
|
||||||
@@ -46,6 +45,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
|||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
||||||
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
|
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
|
||||||
@@ -91,21 +91,6 @@ private fun SaveableCardFeedState(
|
|||||||
RenderCardFeed(feedContent, accountViewModel, listState, nav, routeForLastRead)
|
RenderCardFeed(feedContent, accountViewModel, listState, nav, routeForLastRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchScrollToTop(
|
|
||||||
feedContent: CardFeedContentState,
|
|
||||||
listState: LazyListState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by feedContent.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && feedContent.scrolltoTopPending) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
feedContent.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderCardFeed(
|
fun RenderCardFeed(
|
||||||
feedContent: CardFeedContentState,
|
feedContent: CardFeedContentState,
|
||||||
|
|||||||
+1
-16
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
|||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.note.BoostReaction
|
import com.vitorpamplona.amethyst.ui.note.BoostReaction
|
||||||
@@ -149,22 +150,6 @@ fun WatchAccountForVideoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
public fun WatchScrollToTop(
|
|
||||||
videoFeedContentState: FeedContentState,
|
|
||||||
pagerState: PagerState,
|
|
||||||
) {
|
|
||||||
val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(scrollToTop) {
|
|
||||||
if (scrollToTop > 0 && videoFeedContentState.scrolltoTopPending) {
|
|
||||||
pagerState.scrollToPage(page = 0)
|
|
||||||
videoFeedContentState.sentToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderPage(
|
fun RenderPage(
|
||||||
videoFeedContentState: FeedContentState,
|
videoFeedContentState: FeedContentState,
|
||||||
|
|||||||
Reference in New Issue
Block a user