Refactoring Moving feed status from ViewModel to State objects

This commit is contained in:
Vitor Pamplona
2024-08-13 17:32:15 -04:00
parent 3af2cb52cc
commit c32dc61ea1
50 changed files with 986 additions and 626 deletions
@@ -145,10 +145,10 @@ import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
@@ -0,0 +1,52 @@
/**
* 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.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverNIP89FeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
class AccountFeedContentStates(
val accountViewModel: AccountViewModel,
) {
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val homeReplies = FeedContentState(HomeConversationsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val dmNew = FeedContentState(ChatroomListNewFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val videoFeed = FeedContentState(VideoFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
}
@@ -0,0 +1,182 @@
/**
* 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.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.events.DeletionEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@Stable
class FeedContentState(
val localFilter: FeedFilter<Note>,
val viewModelScope: CoroutineScope,
) : InvalidatableContent {
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
val feedContent = _feedContent.asStateFlow()
// Simple counter that changes when it needs to invalidate everything
private val _scrollToTop = MutableStateFlow<Int>(0)
val scrollToTop = _scrollToTop.asStateFlow()
var scrolltoTopPending = false
private var lastFeedKey: String? = null
fun sendToTop() {
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()) }
}
}
@@ -0,0 +1,143 @@
/**
* 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.animation.core.tween
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
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.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun RefresheableFeedContentStateView(
feedContentState: FeedContentState,
routeForLastRead: String?,
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
RefresheableBox(feedContentState, enablePullRefresh) {
SaveableFeedContentState(feedContentState, scrollStateKey) { listState ->
RenderFeedContentState(feedContentState, accountViewModel, listState, nav, routeForLastRead)
}
}
}
@Composable
fun SaveableFeedContentState(
feedContentState: FeedContentState,
scrollStateKey: String? = null,
content: @Composable (LazyListState) -> Unit,
) {
val listState =
if (scrollStateKey != null) {
rememberForeverLazyListState(scrollStateKey)
} else {
rememberLazyListState()
}
WatchScrollToTopFeedContentState(feedContentState, listState)
content(listState)
}
@Composable
fun SaveableGridFeedContentState(
feedContentState: FeedContentState,
scrollStateKey: String? = null,
content: @Composable (LazyGridState) -> Unit,
) {
val gridState =
if (scrollStateKey != null) {
rememberForeverLazyGridState(scrollStateKey)
} else {
rememberLazyGridState()
}
WatchScrollToTopFeedContentState(feedContentState, gridState)
content(gridState)
}
@Composable
fun RenderFeedContentState(
feedContentState: FeedContentState,
accountViewModel: AccountViewModel,
listState: LazyListState,
nav: (String) -> Unit,
routeForLastRead: String?,
onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) },
onEmpty: @Composable () -> Unit = { FeedEmpty(feedContentState::invalidateData) },
onError: @Composable (String) -> Unit = { FeedError(it, feedContentState::invalidateData) },
onLoading: @Composable () -> Unit = { LoadingFeed() },
) {
val feedState by feedContentState.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
animationSpec = tween(durationMillis = 100),
accountViewModel = accountViewModel,
) { state ->
when (state) {
is FeedState.Empty -> onEmpty()
is FeedState.FeedError -> onError(state.errorMessage)
is FeedState.Loaded -> onLoaded(state)
is FeedState.Loading -> onLoading()
}
}
}
@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,47 @@
/**
* 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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable
fun FeedEmpty(onRefresh: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(stringRes(R.string.feed_is_empty))
Spacer(modifier = StdVertSpacer)
OutlinedButton(onClick = onRefresh) { Text(text = stringRes(R.string.refresh)) }
}
}
@@ -0,0 +1,56 @@
/**
* 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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable
fun FeedError(
errorMessage: String,
onRefresh: () -> Unit,
) {
Column(
Modifier.fillMaxHeight().fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("${stringRes(R.string.error_loading_replies)} $errorMessage")
Spacer(modifier = StdVertSpacer)
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = onRefresh,
) {
Text(text = stringRes(R.string.try_again))
}
}
}
@@ -0,0 +1,69 @@
/**
* 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.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FeedLoaded(
state: FeedState.Loaded,
listState: LazyListState,
routeForLastRead: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
LazyColumn(
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItemPlacement()) {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = Modifier.fillMaxWidth(),
isBoostedNote = false,
isHiddenFeed = state.showHidden.value,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@@ -18,7 +18,7 @@
* 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.screen
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.MutableState
import com.vitorpamplona.amethyst.model.Note
@@ -0,0 +1,25 @@
/**
* 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
interface InvalidatableContent {
fun invalidateData(ignoreIfDoing: Boolean = false)
}
@@ -0,0 +1,43 @@
/**
* 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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun LoadingFeed() {
Column(
Modifier.fillMaxHeight().fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(stringRes(R.string.loading_feed))
}
}
@@ -0,0 +1,81 @@
/**
* 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.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
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
@Composable
fun RefresheableBox(
invalidateableContent: InvalidatableContent,
enablePullRefresh: Boolean = true,
content: @Composable () -> Unit,
) {
RefresheableBox(
enablePullRefresh = enablePullRefresh,
onRefresh = { invalidateableContent.invalidateData() },
content = content,
)
}
@Composable
fun RefresheableBox(
enablePullRefresh: Boolean = true,
onRefresh: () -> Unit,
content: @Composable () -> Unit,
) {
var refreshing by remember { mutableStateOf(false) }
val refresh = {
refreshing = true
onRefresh()
refreshing = false
}
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
val modifier =
if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState)
} else {
Modifier.fillMaxSize()
}
Box(modifier) {
content()
if (enablePullRefresh) {
PullRefreshIndicator(
refreshing = refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}
@@ -18,7 +18,7 @@
* 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.screen
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
@@ -42,39 +42,29 @@ import androidx.navigation.compose.composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen
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.ChatroomScreenByAuthor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DraftListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NIP90ContentDiscoveryScreen
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.SettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomScreenByAuthor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.NIP90ContentDiscoveryScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen
import com.vitorpamplona.amethyst.ui.uriToRoute
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -82,16 +72,6 @@ import java.net.URLDecoder
@Composable
fun AppNavigation(
homeFeedViewModel: NostrHomeFeedViewModel,
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
videoFeedViewModel: NostrVideoFeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoverNip89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
notifFeedViewModel: NotificationViewModel,
userReactionsStatsModel: UserReactionsViewModel,
navController: NavHostController,
@@ -125,8 +105,8 @@ fun AppNavigation(
val nip47 = it.arguments?.getString("nip47")
HomeScreen(
homeFeedViewModel = homeFeedViewModel,
repliesFeedViewModel = repliesFeedViewModel,
newThreadsFeedState = accountViewModel.feedStates.homeNewThreads,
repliesFeedState = accountViewModel.feedStates.homeNewThreads,
accountViewModel = accountViewModel,
nav = nav,
nip47 = nip47,
@@ -148,8 +128,8 @@ fun AppNavigation(
Route.Message.route,
content = {
ChatroomListScreen(
knownFeedViewModel,
newFeedViewModel,
accountViewModel.feedStates.dmKnown,
accountViewModel.feedStates.dmNew,
accountViewModel,
nav,
)
@@ -162,7 +142,7 @@ fun AppNavigation(
route.arguments,
content = {
VideoScreen(
videoFeedView = videoFeedViewModel,
videoFeedContentState = accountViewModel.feedStates.videoFeed,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -176,11 +156,11 @@ fun AppNavigation(
route.arguments,
content = {
DiscoverScreen(
discoveryContentNIP89FeedViewModel = discoverNip89FeedViewModel,
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
discoveryContentNIP89FeedContentState = accountViewModel.feedStates.discoverDVMs,
discoveryMarketplaceFeedContentState = accountViewModel.feedStates.discoverMarketplace,
discoveryLiveFeedContentState = accountViewModel.feedStates.discoverLive,
discoveryCommunityFeedContentState = accountViewModel.feedStates.discoverCommunities,
discoveryChatFeedContentState = accountViewModel.feedStates.discoverPublicChats,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -121,14 +121,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagActionOptions
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoom
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoomByAuthor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.observeAppDefinition
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadRoom
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadRoomByAuthor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LongChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LongRoomHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.RoomNameOnlyDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ShortChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.observeAppDefinition
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -73,13 +73,13 @@ import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.observeAppDefinition
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.EndedFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.observeAppDefinition
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
@@ -114,7 +114,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.RenderChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.RenderChannelHeader
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font12SP
@@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.events.ChannelMessageEvent
@@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomHeader
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.events.ChatroomKeyable
@@ -62,9 +62,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.JoinCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
@@ -44,7 +44,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
@@ -58,10 +57,10 @@ import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CrossfadeCheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
@@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
@@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomHeader
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -42,6 +42,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.events.BadgeAwardEvent
@@ -74,7 +75,7 @@ class NotificationViewModel(
open class CardFeedViewModel(
val localFilter: FeedFilter<Note>,
) : ViewModel(),
InvalidatableViewModel {
InvalidatableContent {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
@@ -21,45 +21,23 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyGridState
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable
fun RefresheableFeedView(
@@ -77,53 +55,6 @@ fun RefresheableFeedView(
}
}
@Composable
fun RefresheableBox(
viewModel: InvalidatableViewModel,
enablePullRefresh: Boolean = true,
content: @Composable () -> Unit,
) {
RefresheableBox(
enablePullRefresh = enablePullRefresh,
onRefresh = { viewModel.invalidateData() },
content = content,
)
}
@Composable
fun RefresheableBox(
enablePullRefresh: Boolean = true,
onRefresh: () -> Unit,
content: @Composable () -> Unit,
) {
var refreshing by remember { mutableStateOf(false) }
val refresh = {
refreshing = true
onRefresh()
refreshing = false
}
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
val modifier =
if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState)
} else {
Modifier.fillMaxSize()
}
Box(modifier) {
content()
if (enablePullRefresh) {
PullRefreshIndicator(
refreshing = refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}
@Composable
fun SaveableFeedState(
viewModel: FeedViewModel,
@@ -167,7 +98,10 @@ fun RenderFeedState(
listState: LazyListState,
nav: (String) -> Unit,
routeForLastRead: String?,
onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) },
onLoaded: @Composable (FeedState.Loaded) -> Unit = {
com.vitorpamplona.amethyst.ui.feeds
.FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav)
},
onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } },
onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } },
onLoading: @Composable () -> Unit = { LoadingFeed() },
@@ -217,82 +151,3 @@ private fun WatchScrollToTop(
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
listState: LazyListState,
routeForLastRead: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
LazyColumn(
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItemPlacement()) {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = Modifier.fillMaxWidth(),
isBoostedNote = false,
isHiddenFeed = state.showHidden.value,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@Composable
fun LoadingFeed() {
Column(
Modifier.fillMaxHeight().fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(stringRes(R.string.loading_feed))
}
}
@Composable
fun FeedError(
errorMessage: String,
onRefresh: () -> Unit,
) {
Column(
Modifier.fillMaxHeight().fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("${stringRes(R.string.error_loading_replies)} $errorMessage")
Spacer(modifier = StdVertSpacer)
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = onRefresh,
) {
Text(text = stringRes(R.string.try_again))
}
}
}
@Composable
fun FeedEmpty(onRefresh: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(stringRes(R.string.feed_is_empty))
Spacer(modifier = StdVertSpacer)
OutlinedButton(onClick = onRefresh) { Text(text = stringRes(R.string.refresh)) }
}
}
@@ -38,20 +38,11 @@ import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
import com.vitorpamplona.amethyst.ui.dal.CommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverNIP89FeedFilter
import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryResponseFilter
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
@@ -61,6 +52,8 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.events.ChatroomKey
@@ -109,60 +102,6 @@ class NostrVideoFeedViewModel(
}
}
class NostrDiscoverMarketplaceFeedViewModel(
val account: Account,
) : FeedViewModel(
DiscoverMarketplaceFeedFilter(account),
) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDiscoverMarketplaceFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverMarketplaceFeedViewModel>): NostrDiscoverMarketplaceFeedViewModel = NostrDiscoverMarketplaceFeedViewModel(account) as NostrDiscoverMarketplaceFeedViewModel
}
}
class NostrDiscoverNIP89FeedViewModel(
val account: Account,
) : FeedViewModel(
DiscoverNIP89FeedFilter(account),
) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDiscoverNIP89FeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverNIP89FeedViewModel>): NostrDiscoverNIP89FeedViewModel = NostrDiscoverNIP89FeedViewModel(account) as NostrDiscoverNIP89FeedViewModel
}
}
class NostrDiscoverLiveFeedViewModel(
val account: Account,
) : FeedViewModel(DiscoverLiveFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDiscoverLiveFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverLiveFeedViewModel>): NostrDiscoverLiveFeedViewModel = NostrDiscoverLiveFeedViewModel(account) as NostrDiscoverLiveFeedViewModel
}
}
class NostrDiscoverCommunityFeedViewModel(
val account: Account,
) : FeedViewModel(DiscoverCommunityFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDiscoverCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverCommunityFeedViewModel>): NostrDiscoverCommunityFeedViewModel = NostrDiscoverCommunityFeedViewModel(account) as NostrDiscoverCommunityFeedViewModel
}
}
class NostrDiscoverChatFeedViewModel(
val account: Account,
) : FeedViewModel(DiscoverChatFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDiscoverChatFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverChatFeedViewModel>): NostrDiscoverChatFeedViewModel = NostrDiscoverChatFeedViewModel(account) as NostrDiscoverChatFeedViewModel
}
}
class NostrThreadFeedViewModel(
account: Account,
noteId: String,
@@ -277,48 +216,6 @@ class NostrUserProfileBookmarksFeedViewModel(
}
}
class NostrChatroomListKnownFeedViewModel(
val account: Account,
) : FeedViewModel(ChatroomListKnownFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrChatroomListKnownFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListKnownFeedViewModel>): NostrChatroomListKnownFeedViewModel = NostrChatroomListKnownFeedViewModel(account) as NostrChatroomListKnownFeedViewModel
}
}
class NostrChatroomListNewFeedViewModel(
val account: Account,
) : FeedViewModel(ChatroomListNewFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrChatroomListNewFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListNewFeedViewModel>): NostrChatroomListNewFeedViewModel = NostrChatroomListNewFeedViewModel(account) as NostrChatroomListNewFeedViewModel
}
}
@Stable
class NostrHomeFeedViewModel(
val account: Account,
) : FeedViewModel(HomeNewThreadFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrHomeFeedViewModel : ViewModel> create(modelClass: Class<NostrHomeFeedViewModel>): NostrHomeFeedViewModel = NostrHomeFeedViewModel(account) as NostrHomeFeedViewModel
}
}
@Stable
class NostrHomeRepliesFeedViewModel(
val account: Account,
) : FeedViewModel(HomeConversationsFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrHomeRepliesFeedViewModel : ViewModel> create(modelClass: Class<NostrHomeRepliesFeedViewModel>): NostrHomeRepliesFeedViewModel = NostrHomeRepliesFeedViewModel(account) as NostrHomeRepliesFeedViewModel
}
}
@Stable
class NostrBookmarkPublicFeedViewModel(
val account: Account,
@@ -383,7 +280,7 @@ class NostrUserAppRecommendationsFeedViewModel(
abstract class FeedViewModel(
val localFilter: FeedFilter<Note>,
) : ViewModel(),
InvalidatableViewModel {
InvalidatableContent {
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
val feedContent = _feedContent.asStateFlow()
@@ -32,6 +32,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -38,6 +38,8 @@ import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.note.RelayCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -52,7 +54,7 @@ import kotlinx.coroutines.launch
@Stable
class RelayFeedViewModel :
ViewModel(),
InvalidatableViewModel {
InvalidatableContent {
val order =
compareByDescending<RelayInfo> { it.lastEvent }
.thenByDescending { it.counter }
@@ -37,6 +37,8 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.HiddenWordsFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.ammolite.relays.BundledUpdate
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -57,7 +58,7 @@ class NostrHiddenWordsFeedViewModel(
open class StringFeedViewModel(
val dataSource: FeedFilter<String>,
) : ViewModel(),
InvalidatableViewModel {
InvalidatableContent {
private val _feedContent = MutableStateFlow<StringFeedState>(StringFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
@@ -79,6 +79,8 @@ import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus
@@ -136,8 +138,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThinSendButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ThinSendButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -29,6 +29,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.SpammerAccountsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.ammolite.relays.BundledUpdate
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -97,7 +98,7 @@ class NostrSpammerAccountsFeedViewModel(
open class UserFeedViewModel(
val dataSource: FeedFilter<User>,
) : ViewModel(),
InvalidatableViewModel {
InvalidatableContent {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
@@ -168,7 +169,3 @@ open class UserFeedViewModel(
super.onCleared()
}
}
interface InvalidatableViewModel {
fun invalidateData(ignoreIfDoing: Boolean = false)
}
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.feeds.AccountFeedContentStates
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
@@ -171,6 +172,8 @@ class AccountViewModel(
val toasts = MutableSharedFlow<ToastMsg?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val feedStates = AccountFeedContentStates(this)
val showSensitiveContentChanges =
account.live.map { it.account.showSensitiveContent }.distinctUntilChanged()
@@ -49,13 +49,13 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.NostrDraftEventsFeedViewModel
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.DRAFTS
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.maxWidthWithBackground
@@ -67,11 +67,11 @@ import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.note.elements.AddButton
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.StringFeedView
import com.vitorpamplona.amethyst.ui.stringRes
@@ -98,16 +98,6 @@ import com.vitorpamplona.amethyst.ui.navigation.getRouteWithArguments
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
import com.vitorpamplona.amethyst.ui.screen.AccountState
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -184,55 +174,6 @@ fun MainScreen(
factory = FollowListViewModel.Factory(accountViewModel.account),
)
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
val homeFeedViewModel: NostrHomeFeedViewModel =
viewModel(
key = "NostrHomeFeedViewModel",
factory = NostrHomeFeedViewModel.Factory(accountViewModel.account),
)
val repliesFeedViewModel: NostrHomeRepliesFeedViewModel =
viewModel(
key = "NostrHomeRepliesFeedViewModel",
factory = NostrHomeRepliesFeedViewModel.Factory(accountViewModel.account),
)
val videoFeedViewModel: NostrVideoFeedViewModel =
viewModel(
key = "NostrVideoFeedViewModel",
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account),
)
val discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel =
viewModel(
key = "NostrDiscoveryMarketplaceFeedViewModel",
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account),
)
val discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel =
viewModel(
key = "NostrDiscoveryNIP89FeedViewModel",
factory = NostrDiscoverNIP89FeedViewModel.Factory(accountViewModel.account),
)
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel =
viewModel(
key = "NostrDiscoveryLiveFeedViewModel",
factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account),
)
val discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel =
viewModel(
key = "NostrDiscoveryCommunityFeedViewModel",
factory = NostrDiscoverCommunityFeedViewModel.Factory(accountViewModel.account),
)
val discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel =
viewModel(
key = "NostrDiscoveryChatFeedViewModel",
factory = NostrDiscoverChatFeedViewModel.Factory(accountViewModel.account),
)
val notifFeedViewModel: NotificationViewModel =
viewModel(
key = "NotificationViewModel",
@@ -245,18 +186,6 @@ fun MainScreen(
factory = UserReactionsViewModel.Factory(accountViewModel.account),
)
val knownFeedViewModel: NostrChatroomListKnownFeedViewModel =
viewModel(
key = "NostrChatroomListKnownFeedViewModel",
factory = NostrChatroomListKnownFeedViewModel.Factory(accountViewModel.account),
)
val newFeedViewModel: NostrChatroomListNewFeedViewModel =
viewModel(
key = "NostrChatroomListNewFeedViewModel",
factory = NostrChatroomListNewFeedViewModel.Factory(accountViewModel.account),
)
val navBottomRow =
remember(navController) {
{ route: Route, selected: Boolean ->
@@ -271,17 +200,18 @@ fun MainScreen(
// and having to deal with all recompositions with scroll to top true
when (route.base) {
Route.Home.base -> {
homeFeedViewModel.sendToTop()
repliesFeedViewModel.sendToTop()
accountViewModel.feedStates.homeNewThreads.sendToTop()
accountViewModel.feedStates.homeReplies.sendToTop()
}
Route.Video.base -> {
videoFeedViewModel.sendToTop()
accountViewModel.feedStates.videoFeed.sendToTop()
}
Route.Discover.base -> {
discoverMarketplaceFeedViewModel.sendToTop()
discoveryLiveFeedViewModel.sendToTop()
discoveryCommunityFeedViewModel.sendToTop()
discoveryChatFeedViewModel.sendToTop()
accountViewModel.feedStates.discoverMarketplace.sendToTop()
accountViewModel.feedStates.discoverLive.sendToTop()
accountViewModel.feedStates.discoverCommunities.sendToTop()
accountViewModel.feedStates.discoverPublicChats.sendToTop()
accountViewModel.feedStates.discoverDVMs.sendToTop()
}
Route.Notification.base -> {
notifFeedViewModel.invalidateDataAndSendToTop(true)
@@ -314,16 +244,6 @@ fun MainScreen(
accountStateViewModel = accountStateViewModel,
userReactionsStatsModel = userReactionsStatsModel,
followListsViewModel = followListsViewModel,
homeFeedViewModel = homeFeedViewModel,
repliesFeedViewModel = repliesFeedViewModel,
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
videoFeedViewModel = videoFeedViewModel,
discoverNIP89FeedViewModel = discoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
notifFeedViewModel = notifFeedViewModel,
sharedPreferencesViewModel = sharedPreferencesViewModel,
accountViewModel = accountViewModel,
@@ -363,16 +283,6 @@ private fun MainScaffold(
accountStateViewModel: AccountStateViewModel,
userReactionsStatsModel: UserReactionsViewModel,
followListsViewModel: FollowListViewModel,
homeFeedViewModel: NostrHomeFeedViewModel,
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
videoFeedViewModel: NostrVideoFeedViewModel,
discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
notifFeedViewModel: NotificationViewModel,
sharedPreferencesViewModel: SharedPreferencesViewModel,
accountViewModel: AccountViewModel,
@@ -497,16 +407,6 @@ private fun MainScaffold(
.imePadding(),
) {
AppNavigation(
homeFeedViewModel = homeFeedViewModel,
repliesFeedViewModel = repliesFeedViewModel,
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
videoFeedViewModel = videoFeedViewModel,
discoverNip89FeedViewModel = discoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
notifFeedViewModel = notifFeedViewModel,
userReactionsStatsModel = userReactionsStatsModel,
navController = navController,
@@ -68,6 +68,7 @@ import com.patrykandpatrick.vico.core.chart.values.ChartValues
import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.OneGiga
import com.vitorpamplona.amethyst.ui.note.OneKilo
@@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.showCount
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshableCardView
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -60,6 +60,10 @@ import com.vitorpamplona.amethyst.ui.components.GalleryContentView
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.ClickableNote
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery
@@ -67,11 +71,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
@@ -132,6 +132,9 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
@@ -141,7 +144,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
@@ -152,13 +154,11 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileGalleryFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
import android.widget.Toast
import androidx.compose.animation.animateContentSize
@@ -61,8 +61,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.contentPaddingWithLabel
import androidx.compose.material3.TextFieldDefaults.contentPaddingWithoutLabel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -138,8 +136,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfUrlIsOnline
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
@@ -18,7 +18,7 @@
* 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.screen
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Row
@@ -38,7 +38,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Font14SP
@@ -18,7 +18,7 @@
* 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.screen
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Row
@@ -34,6 +34,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -41,22 +47,22 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable
fun ChatroomListFeedView(
viewModel: FeedViewModel,
feedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
markAsRead: MutableState<Boolean>,
) {
RefresheableBox(viewModel, true) { CrossFadeState(viewModel, accountViewModel, nav, markAsRead) }
RefresheableBox(feedContentState, true) { CrossFadeState(feedContentState, accountViewModel, nav, markAsRead) }
}
@Composable
private fun CrossFadeState(
viewModel: FeedViewModel,
feedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
markAsRead: MutableState<Boolean>,
) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
val feedState by feedContentState.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
@@ -65,10 +71,10 @@ private fun CrossFadeState(
) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty { viewModel.invalidateData() }
FeedEmpty { feedContentState.invalidateData() }
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) { viewModel.invalidateData() }
FeedError(state.errorMessage) { feedContentState.invalidateData() }
}
is FeedState.Loaded -> {
FeedLoaded(state, accountViewModel, nav, markAsRead)
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
@@ -67,10 +67,8 @@ import com.google.accompanist.adaptive.TwoPane
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size20dp
@@ -81,8 +79,8 @@ import kotlinx.coroutines.launch
@Composable
fun ChatroomListScreen(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -102,16 +100,16 @@ fun ChatroomListScreen(
if (twoPane && windowSizeClass != null) {
ChatroomListTwoPane(
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
knownFeedContentState = knownFeedContentState,
newFeedContentState = newFeedContentState,
widthSizeClass = windowSizeClass!!.widthSizeClass,
accountViewModel = accountViewModel,
nav = nav,
)
} else {
ChatroomListScreenOnlyList(
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
knownFeedContentState = knownFeedContentState,
newFeedContentState = newFeedContentState,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -125,8 +123,8 @@ data class RouteId(
@Composable
fun ChatroomListTwoPane(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
widthSizeClass: WindowWidthSizeClass,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
@@ -164,8 +162,8 @@ fun ChatroomListTwoPane(
first = {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) {
ChatroomListScreenOnlyList(
knownFeedViewModel,
newFeedViewModel,
knownFeedContentState,
newFeedContentState,
accountViewModel,
navInterceptor,
)
@@ -209,8 +207,8 @@ fun ChatroomListTwoPane(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChatroomListScreenOnlyList(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -221,7 +219,7 @@ fun ChatroomListScreenOnlyList(
val markKnownAsRead = remember { mutableStateOf(false) }
val markNewAsRead = remember { mutableStateOf(false) }
WatchAccountForListScreen(knownFeedViewModel, newFeedViewModel, accountViewModel)
WatchAccountForListScreen(knownFeedContentState, newFeedContentState, accountViewModel)
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifeCycleOwner) {
@@ -238,11 +236,11 @@ fun ChatroomListScreenOnlyList(
}
val tabs by
remember(knownFeedViewModel, markKnownAsRead) {
remember(knownFeedContentState, markKnownAsRead) {
derivedStateOf {
listOf(
ChatroomListTabItem(R.string.known, knownFeedViewModel, markKnownAsRead),
ChatroomListTabItem(R.string.new_requests, newFeedViewModel, markNewAsRead),
ChatroomListTabItem(R.string.known, knownFeedContentState, markKnownAsRead),
ChatroomListTabItem(R.string.new_requests, newFeedContentState, markNewAsRead),
)
}
}
@@ -290,7 +288,7 @@ fun ChatroomListScreenOnlyList(
modifier = Modifier.fillMaxSize(),
) { page ->
ChatroomListFeedView(
viewModel = tabs[page].viewModel,
feedContentState = tabs[page].feedContentState,
accountViewModel = accountViewModel,
nav = nav,
markAsRead = tabs[page].markAsRead,
@@ -301,16 +299,16 @@ fun ChatroomListScreenOnlyList(
@Composable
fun WatchAccountForListScreen(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
knownFeedContentState: FeedContentState,
newFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
) {
LaunchedEffect(accountViewModel) {
launch(Dispatchers.IO) {
NostrChatroomListDataSource.account = accountViewModel.account
NostrChatroomListDataSource.start()
knownFeedViewModel.invalidateData(true)
newFeedViewModel.invalidateData(true)
knownFeedContentState.invalidateData(true)
newFeedContentState.invalidateData(true)
}
}
}
@@ -318,7 +316,7 @@ fun WatchAccountForListScreen(
@Immutable
class ChatroomListTabItem(
val resource: Int,
val viewModel: FeedViewModel,
val feedContentState: FeedContentState,
val markAsRead: MutableState<Boolean>,
)
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
import android.widget.Toast
import androidx.compose.foundation.clickable
@@ -106,7 +106,8 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.NostrCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun CommunityScreen(
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -58,24 +58,21 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableGridFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -92,11 +89,11 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DiscoverScreen(
discoveryContentNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
discoveryContentNIP89FeedContentState: FeedContentState,
discoveryMarketplaceFeedContentState: FeedContentState,
discoveryLiveFeedContentState: FeedContentState,
discoveryCommunityFeedContentState: FeedContentState,
discoveryChatFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -104,44 +101,46 @@ fun DiscoverScreen(
val tabs by
remember(
discoveryContentNIP89FeedViewModel,
discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel,
discoveryContentNIP89FeedContentState,
discoveryLiveFeedContentState,
discoveryCommunityFeedContentState,
discoveryChatFeedContentState,
discoveryMarketplaceFeedContentState,
) {
mutableStateOf(
listOf(
TabItem(
R.string.discover_content,
discoveryContentNIP89FeedViewModel,
discoveryContentNIP89FeedContentState,
Route.Discover.base + "DiscoverContent",
ScrollStateKeys.DISCOVER_CONTENT,
AppDefinitionEvent.KIND,
),
TabItem(
R.string.discover_live,
discoveryLiveFeedViewModel,
discoveryLiveFeedContentState,
Route.Discover.base + "Live",
ScrollStateKeys.DISCOVER_LIVE,
LiveActivitiesEvent.KIND,
),
TabItem(
R.string.discover_community,
discoveryCommunityFeedViewModel,
discoveryCommunityFeedContentState,
Route.Discover.base + "Community",
ScrollStateKeys.DISCOVER_COMMUNITY,
CommunityDefinitionEvent.KIND,
),
TabItem(
R.string.discover_marketplace,
discoveryMarketplaceFeedViewModel,
discoveryMarketplaceFeedContentState,
Route.Discover.base + "Marketplace",
ScrollStateKeys.DISCOVER_MARKETPLACE,
ClassifiedsEvent.KIND,
useGridLayout = true,
),
TabItem(
R.string.discover_chat,
discoveryChatFeedViewModel,
discoveryChatFeedContentState,
Route.Discover.base + "Chats",
ScrollStateKeys.DISCOVER_CHATS,
ChannelCreateEvent.KIND,
@@ -153,11 +152,11 @@ fun DiscoverScreen(
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen(
discoverNIP89FeedViewModel = discoveryContentNIP89FeedViewModel,
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
discoveryContentNIP89FeedContentState = discoveryContentNIP89FeedContentState,
discoveryMarketplaceFeedContentState = discoveryMarketplaceFeedContentState,
discoveryLiveFeedContentState = discoveryLiveFeedContentState,
discoveryCommunityFeedContentState = discoveryCommunityFeedContentState,
discoveryChatFeedContentState = discoveryChatFeedContentState,
accountViewModel = accountViewModel,
)
@@ -214,11 +213,11 @@ private fun DiscoverPages(
}
HorizontalPager(state = pagerState) { page ->
RefresheableBox(tabs[page].viewModel, true) {
if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) {
SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RefresheableBox(tabs[page].feedState, true) {
if (tabs[page].useGridLayout) {
SaveableGridFeedContentState(tabs[page].feedState, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
viewModel = tabs[page].viewModel,
feedContentState = tabs[page].feedState,
routeForLastRead = tabs[page].routeForLastRead,
forceEventKind = tabs[page].forceEventKind,
listState = listState,
@@ -227,9 +226,9 @@ private fun DiscoverPages(
)
}
} else {
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
SaveableFeedContentState(tabs[page].feedState, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
viewModel = tabs[page].viewModel,
feedContentState = tabs[page].feedState,
routeForLastRead = tabs[page].routeForLastRead,
forceEventKind = tabs[page].forceEventKind,
listState = listState,
@@ -244,14 +243,14 @@ private fun DiscoverPages(
@Composable
private fun RenderDiscoverFeed(
viewModel: FeedViewModel,
feedContentState: FeedContentState,
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyGridState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
val feedState by feedContentState.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
@@ -261,10 +260,10 @@ private fun RenderDiscoverFeed(
) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty { viewModel.invalidateData() }
FeedEmpty(feedContentState::invalidateData)
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) { viewModel.invalidateData() }
FeedError(state.errorMessage, feedContentState::invalidateData)
}
is FeedState.Loaded -> {
DiscoverFeedColumnsLoaded(
@@ -285,14 +284,14 @@ private fun RenderDiscoverFeed(
@Composable
private fun RenderDiscoverFeed(
viewModel: FeedViewModel,
feedContentState: FeedContentState,
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyListState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
val feedState by feedContentState.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
@@ -302,10 +301,10 @@ private fun RenderDiscoverFeed(
) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty { viewModel.invalidateData() }
FeedEmpty(feedContentState::invalidateData)
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) { viewModel.invalidateData() }
FeedError(state.errorMessage, feedContentState::invalidateData)
}
is FeedState.Loaded -> {
DiscoverFeedLoaded(
@@ -326,22 +325,22 @@ private fun RenderDiscoverFeed(
@Composable
fun WatchAccountForDiscoveryScreen(
discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
discoveryContentNIP89FeedContentState: FeedContentState,
discoveryMarketplaceFeedContentState: FeedContentState,
discoveryLiveFeedContentState: FeedContentState,
discoveryCommunityFeedContentState: FeedContentState,
discoveryChatFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState) {
NostrDiscoveryDataSource.resetFilters()
discoverNIP89FeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryChatFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryContentNIP89FeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryMarketplaceFeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryCommunityFeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryChatFeedContentState.checkKeysInvalidateDataAndSendToTop()
}
}
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
@@ -66,6 +66,8 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.DVMCard
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
@@ -78,11 +80,10 @@ import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.ui.note.elements.customZapClick
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel
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.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
@@ -54,17 +54,16 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
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.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
@@ -76,19 +75,19 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomeScreen(
homeFeedViewModel: NostrHomeFeedViewModel,
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
newThreadsFeedState: FeedContentState,
repliesFeedState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
nip47: String? = null,
) {
ResolveNIP47(nip47, accountViewModel)
WatchAccountForHomeScreen(homeFeedViewModel, repliesFeedViewModel, accountViewModel)
WatchAccountForHomeScreen(newThreadsFeedState, repliesFeedState, accountViewModel)
WatchLifeCycleChanges(accountViewModel)
AssembleHomeTabs(homeFeedViewModel, repliesFeedViewModel) { pagerState, tabItems ->
AssembleHomeTabs(newThreadsFeedState, repliesFeedState) { pagerState, tabItems ->
AssembleHomePage(pagerState, tabItems, accountViewModel, nav)
}
}
@@ -96,25 +95,25 @@ fun HomeScreen(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AssembleHomeTabs(
homeFeedViewModel: NostrHomeFeedViewModel,
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
newThreadsFeedState: FeedContentState,
repliesFeedState: FeedContentState,
inner: @Composable (PagerState, ImmutableList<TabItem>) -> Unit,
) {
val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) { 2 }
val tabs by
remember(homeFeedViewModel, repliesFeedViewModel) {
remember(newThreadsFeedState, repliesFeedState) {
mutableStateOf(
listOf(
TabItem(
R.string.new_threads,
homeFeedViewModel,
newThreadsFeedState,
Route.Home.base + "Follows",
ScrollStateKeys.HOME_FOLLOWS,
),
TabItem(
R.string.conversations,
repliesFeedViewModel,
repliesFeedState,
Route.Home.base + "FollowsReplies",
ScrollStateKeys.HOME_REPLIES,
),
@@ -194,7 +193,7 @@ private fun HomePages(
HorizontalPager(state = pagerState, userScrollEnabled = false) { page ->
HomeFeeds(
viewModel = tabs[page].viewModel,
feedState = tabs[page].feedState,
routeForLastRead = tabs[page].routeForLastRead,
scrollStateKey = tabs[page].scrollStateKey,
accountViewModel = accountViewModel,
@@ -205,22 +204,22 @@ private fun HomePages(
@Composable
fun HomeFeeds(
viewModel: FeedViewModel,
feedState: FeedContentState,
routeForLastRead: String?,
enablePullRefresh: Boolean = true,
scrollStateKey: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
RefresheableBox(viewModel, enablePullRefresh) {
SaveableFeedState(viewModel, scrollStateKey) { listState ->
RenderFeedState(
viewModel = viewModel,
RefresheableBox(feedState, enablePullRefresh) {
SaveableFeedContentState(feedState, scrollStateKey) { listState ->
RenderFeedContentState(
feedContentState = feedState,
accountViewModel = accountViewModel,
listState = listState,
nav = nav,
routeForLastRead = routeForLastRead,
onEmpty = { HomeFeedEmpty { viewModel.invalidateData() } },
onEmpty = { HomeFeedEmpty(feedState::invalidateData) },
)
}
}
@@ -303,8 +302,8 @@ fun CrossfadeCheckIfUrlIsOnline(
@Composable
fun WatchAccountForHomeScreen(
homeFeedViewModel: NostrHomeFeedViewModel,
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
newThreadsFeedState: FeedContentState,
repliesFeedState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle()
@@ -312,16 +311,17 @@ fun WatchAccountForHomeScreen(
LaunchedEffect(accountViewModel, homeFollowList) {
NostrHomeDataSource.account = accountViewModel.account
NostrHomeDataSource.invalidateFilters()
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
newThreadsFeedState.checkKeysInvalidateDataAndSendToTop()
repliesFeedState.checkKeysInvalidateDataAndSendToTop()
}
}
@Immutable
class TabItem(
val resource: Int,
val viewModel: FeedViewModel,
val feedState: FeedContentState,
val routeForLastRead: String,
val scrollStateKey: String,
val forceEventKind: Int? = null,
val useGridLayout: Boolean = false,
)
@@ -18,7 +18,7 @@
* 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.screen.loggedIn
package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -64,6 +63,14 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.BoostReaction
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
@@ -77,15 +84,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.AuthorInfoVideoFeed
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
@@ -105,13 +104,13 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
fun VideoScreen(
videoFeedView: NostrVideoFeedViewModel,
videoFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val lifeCycleOwner = LocalLifecycleOwner.current
WatchAccountForVideoScreen(videoFeedView = videoFeedView, accountViewModel = accountViewModel)
WatchAccountForVideoScreen(videoFeedContentState = videoFeedContentState, accountViewModel = accountViewModel)
DisposableEffect(lifeCycleOwner) {
val observer =
@@ -128,7 +127,7 @@ fun VideoScreen(
Column(Modifier.fillMaxHeight()) {
RenderPage(
videoFeedView = videoFeedView,
videoFeedContentState = videoFeedContentState,
pagerStateKey = ScrollStateKeys.VIDEO_SCREEN,
accountViewModel = accountViewModel,
nav = nav,
@@ -138,7 +137,7 @@ fun VideoScreen(
@Composable
fun WatchAccountForVideoScreen(
videoFeedView: NostrVideoFeedViewModel,
videoFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle()
@@ -146,34 +145,34 @@ fun WatchAccountForVideoScreen(
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
NostrVideoDataSource.resetFilters()
videoFeedView.checkKeysInvalidateDataAndSendToTop()
videoFeedContentState.checkKeysInvalidateDataAndSendToTop()
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
public fun WatchScrollToTop(
viewModel: FeedViewModel,
videoFeedContentState: FeedContentState,
pagerState: PagerState,
) {
val scrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
if (scrollToTop > 0 && videoFeedContentState.scrolltoTopPending) {
pagerState.scrollToPage(page = 0)
viewModel.sentToTop()
videoFeedContentState.sentToTop()
}
}
}
@Composable
fun RenderPage(
videoFeedView: NostrVideoFeedViewModel,
videoFeedContentState: FeedContentState,
pagerStateKey: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val feedState by videoFeedView.feedContent.collectAsStateWithLifecycle()
val feedState by videoFeedContentState.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
@@ -183,13 +182,13 @@ fun RenderPage(
) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty {}
FeedEmpty(videoFeedContentState::invalidateData)
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) {}
FeedError(state.errorMessage, videoFeedContentState::invalidateData)
}
is FeedState.Loaded -> {
LoadedState(state, pagerStateKey, videoFeedView, accountViewModel, nav)
LoadedState(state, pagerStateKey, videoFeedContentState, accountViewModel, nav)
}
is FeedState.Loading -> {
LoadingFeed()
@@ -203,7 +202,7 @@ fun RenderPage(
private fun LoadedState(
state: FeedState.Loaded,
pagerStateKey: String?,
videoFeedView: NostrVideoFeedViewModel,
videoFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -214,9 +213,9 @@ private fun LoadedState(
rememberPagerState { state.feed.value.size }
}
WatchScrollToTop(videoFeedView, pagerState)
WatchScrollToTop(videoFeedContentState, pagerState)
RefresheableBox(viewModel = videoFeedView) {
RefresheableBox(invalidateableContent = videoFeedContentState) {
SlidingCarousel(
state.feed,
pagerState,