Moves open polls view to a state flow in thew viewmodel

This commit is contained in:
Vitor Pamplona
2026-03-21 16:20:15 -04:00
parent 90cced23d2
commit 459f69ff2c
5 changed files with 104 additions and 100 deletions
@@ -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.notifications.CardFeedContentState
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.video.dal.VideoFeedFilter
import kotlinx.coroutines.CoroutineScope
@@ -68,6 +69,7 @@ class AccountFeedContentStates(
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
val notificationsOpenPolls = OpenPollsState(account, scope)
val notificationSummary = NotificationSummaryState(account)
val feedListOptions = TopNavFilterState(account, scope)
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
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.feeds.FeedError
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.note.BadgeCompose
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.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
fun RenderCardFeed(
feedContent: CardFeedContentState,
pollContent: OpenPollsState,
accountViewModel: AccountViewModel,
listState: LazyListState,
nav: INav,
@@ -133,6 +96,7 @@ fun RenderCardFeed(
is CardFeedState.Loaded -> {
FeedLoaded(
loaded = state,
polls = pollContent,
listState = listState,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
@@ -164,13 +128,14 @@ fun NotificationFeedEmpty(onRefresh: () -> Unit) {
@Composable
private fun FeedLoaded(
loaded: CardFeedState.Loaded,
polls: OpenPollsState,
listState: LazyListState,
routeForLastRead: String,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
val openPolls by openPollsState(accountViewModel)
val openPolls by polls.flow.collectAsStateWithLifecycle()
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -30,7 +30,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.UiSettingsFlow
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.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -45,6 +48,7 @@ fun NotificationScreen(
NotificationScreen(
notifFeedContentState = accountViewModel.feedStates.notifications,
notifSummaryState = accountViewModel.feedStates.notificationSummary,
notifPolls = accountViewModel.feedStates.notificationsOpenPolls,
sharedPrefs = accountViewModel.settings.uiSettingsFlow,
accountViewModel = accountViewModel,
nav = nav,
@@ -55,6 +59,7 @@ fun NotificationScreen(
fun NotificationScreen(
notifFeedContentState: CardFeedContentState,
notifSummaryState: NotificationSummaryState,
notifPolls: OpenPollsState,
sharedPrefs: UiSettingsFlow,
accountViewModel: AccountViewModel,
nav: INav,
@@ -88,13 +93,13 @@ fun NotificationScreen(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
RefreshableCardView(
feedContent = notifFeedContentState,
accountViewModel = accountViewModel,
nav = nav,
routeForLastRead = "Notification",
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
)
RefresheableBox(notifFeedContentState, true) {
val listState = rememberForeverLazyListState(ScrollStateKeys.NOTIFICATION_SCREEN)
WatchScrollToTop(notifFeedContentState, listState)
RenderCardFeed(notifFeedContentState, notifPolls, accountViewModel, listState, nav, "Notification")
}
}
}
}
@@ -24,65 +24,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.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
fun OpenPollsSectionHeader() {
@@ -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() }
}
}