Moves open polls view to a state flow in thew viewmodel
This commit is contained in:
+2
@@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.OpenPollsState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -68,6 +69,7 @@ class AccountFeedContentStates(
|
|||||||
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
|
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
|
||||||
|
|
||||||
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
||||||
|
val notificationsOpenPolls = OpenPollsState(account, scope)
|
||||||
val notificationSummary = NotificationSummaryState(account)
|
val notificationSummary = NotificationSummaryState(account)
|
||||||
|
|
||||||
val feedListOptions = TopNavFilterState(account, scope)
|
val feedListOptions = TopNavFilterState(account, scope)
|
||||||
|
|||||||
+4
-39
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -52,9 +51,6 @@ import com.vitorpamplona.amethyst.logTime
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
|
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
|
||||||
@@ -71,43 +67,10 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RefreshableCardView(
|
|
||||||
feedContent: CardFeedContentState,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
routeForLastRead: String,
|
|
||||||
scrollStateKey: String? = null,
|
|
||||||
enablePullRefresh: Boolean = true,
|
|
||||||
) {
|
|
||||||
RefresheableBox(feedContent, enablePullRefresh) {
|
|
||||||
SaveableCardFeedState(feedContent, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SaveableCardFeedState(
|
|
||||||
feedContent: CardFeedContentState,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
routeForLastRead: String,
|
|
||||||
scrollStateKey: String? = null,
|
|
||||||
) {
|
|
||||||
val listState =
|
|
||||||
if (scrollStateKey != null) {
|
|
||||||
rememberForeverLazyListState(scrollStateKey)
|
|
||||||
} else {
|
|
||||||
rememberLazyListState()
|
|
||||||
}
|
|
||||||
|
|
||||||
WatchScrollToTop(feedContent, listState)
|
|
||||||
|
|
||||||
RenderCardFeed(feedContent, accountViewModel, listState, nav, routeForLastRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderCardFeed(
|
fun RenderCardFeed(
|
||||||
feedContent: CardFeedContentState,
|
feedContent: CardFeedContentState,
|
||||||
|
pollContent: OpenPollsState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -133,6 +96,7 @@ fun RenderCardFeed(
|
|||||||
is CardFeedState.Loaded -> {
|
is CardFeedState.Loaded -> {
|
||||||
FeedLoaded(
|
FeedLoaded(
|
||||||
loaded = state,
|
loaded = state,
|
||||||
|
polls = pollContent,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
routeForLastRead = routeForLastRead,
|
routeForLastRead = routeForLastRead,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
@@ -164,13 +128,14 @@ fun NotificationFeedEmpty(onRefresh: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun FeedLoaded(
|
private fun FeedLoaded(
|
||||||
loaded: CardFeedState.Loaded,
|
loaded: CardFeedState.Loaded,
|
||||||
|
polls: OpenPollsState,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
routeForLastRead: String,
|
routeForLastRead: String,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||||
val openPolls by openPollsState(accountViewModel)
|
val openPolls by polls.flow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|||||||
+12
-7
@@ -30,7 +30,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
@@ -45,6 +48,7 @@ fun NotificationScreen(
|
|||||||
NotificationScreen(
|
NotificationScreen(
|
||||||
notifFeedContentState = accountViewModel.feedStates.notifications,
|
notifFeedContentState = accountViewModel.feedStates.notifications,
|
||||||
notifSummaryState = accountViewModel.feedStates.notificationSummary,
|
notifSummaryState = accountViewModel.feedStates.notificationSummary,
|
||||||
|
notifPolls = accountViewModel.feedStates.notificationsOpenPolls,
|
||||||
sharedPrefs = accountViewModel.settings.uiSettingsFlow,
|
sharedPrefs = accountViewModel.settings.uiSettingsFlow,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
@@ -55,6 +59,7 @@ fun NotificationScreen(
|
|||||||
fun NotificationScreen(
|
fun NotificationScreen(
|
||||||
notifFeedContentState: CardFeedContentState,
|
notifFeedContentState: CardFeedContentState,
|
||||||
notifSummaryState: NotificationSummaryState,
|
notifSummaryState: NotificationSummaryState,
|
||||||
|
notifPolls: OpenPollsState,
|
||||||
sharedPrefs: UiSettingsFlow,
|
sharedPrefs: UiSettingsFlow,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -88,13 +93,13 @@ fun NotificationScreen(
|
|||||||
modifier = Modifier.padding(it).consumeWindowInsets(it),
|
modifier = Modifier.padding(it).consumeWindowInsets(it),
|
||||||
) {
|
) {
|
||||||
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
|
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
|
||||||
RefreshableCardView(
|
RefresheableBox(notifFeedContentState, true) {
|
||||||
feedContent = notifFeedContentState,
|
val listState = rememberForeverLazyListState(ScrollStateKeys.NOTIFICATION_SCREEN)
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
WatchScrollToTop(notifFeedContentState, listState)
|
||||||
routeForLastRead = "Notification",
|
|
||||||
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
|
RenderCardFeed(notifFeedContentState, notifPolls, accountViewModel, listState, nav, "Notification")
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-54
@@ -24,65 +24,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun openPollsState(accountViewModel: AccountViewModel) =
|
|
||||||
remember(accountViewModel) {
|
|
||||||
val userPubkeyHex = accountViewModel.account.userProfile().pubkeyHex
|
|
||||||
|
|
||||||
val filter =
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(PollEvent.KIND, ZapPollEvent.KIND),
|
|
||||||
authors = listOf(userPubkeyHex),
|
|
||||||
)
|
|
||||||
|
|
||||||
accountViewModel.account.cache
|
|
||||||
.observeNotes(filter)
|
|
||||||
.map { notes -> filterOpenPolls(notes) }
|
|
||||||
.flowOn(Dispatchers.IO)
|
|
||||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
|
|
||||||
private fun filterOpenPolls(notes: List<Note>): List<Note> {
|
|
||||||
val now = TimeUtils.now()
|
|
||||||
val oneDayInSeconds = TimeUtils.ONE_DAY
|
|
||||||
|
|
||||||
return notes
|
|
||||||
.filter { note ->
|
|
||||||
val event = note.event ?: return@filter false
|
|
||||||
|
|
||||||
when (event) {
|
|
||||||
is PollEvent -> {
|
|
||||||
val endsAt = event.endsAt()
|
|
||||||
if (endsAt != null) endsAt > now else event.createdAt + oneDayInSeconds > now
|
|
||||||
}
|
|
||||||
|
|
||||||
is ZapPollEvent -> {
|
|
||||||
val closedAt = event.closedAt()
|
|
||||||
if (closedAt != null) closedAt > now else event.createdAt + oneDayInSeconds > now
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.sortedByDescending { it.createdAt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenPollsSectionHeader() {
|
fun OpenPollsSectionHeader() {
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.screen.loggedIn.notifications
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache.notes
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class OpenPollsState(
|
||||||
|
account: Account,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val filter =
|
||||||
|
Filter(
|
||||||
|
kinds = listOf(PollEvent.KIND, ZapPollEvent.KIND),
|
||||||
|
authors = listOf(account.pubKey),
|
||||||
|
)
|
||||||
|
|
||||||
|
val flow: StateFlow<List<Note>> =
|
||||||
|
account.cache
|
||||||
|
.observeNotes(filter)
|
||||||
|
.map { notes -> filterOpenPolls(notes) }
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun filterOpenPolls(notes: List<Note>): List<Note> {
|
||||||
|
val now = TimeUtils.now()
|
||||||
|
val oneDayInSeconds = TimeUtils.ONE_DAY
|
||||||
|
|
||||||
|
return notes
|
||||||
|
.filter { note ->
|
||||||
|
val event = note.event ?: return@filter false
|
||||||
|
|
||||||
|
when (event) {
|
||||||
|
is PollEvent -> {
|
||||||
|
val endsAt = event.endsAt()
|
||||||
|
if (endsAt != null) endsAt > now else event.createdAt + oneDayInSeconds > now
|
||||||
|
}
|
||||||
|
|
||||||
|
is ZapPollEvent -> {
|
||||||
|
val closedAt = event.closedAt()
|
||||||
|
if (closedAt != null) closedAt > now else event.createdAt + oneDayInSeconds > now
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.sortedByDescending { it.createdAt() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user