diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 327a7e3f5..c2a2b12d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 382884187..316e97775 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -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(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 38f66df1b..12238097b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -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") + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsSection.kt index e3fa47d43..95f185ab1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsSection.kt @@ -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): List { - 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() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsState.kt new file mode 100644 index 000000000..08c9faf78 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsState.kt @@ -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> = + account.cache + .observeNotes(filter) + .map { notes -> filterOpenPolls(notes) } + .flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + private fun filterOpenPolls(notes: List): List { + 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() } + } +}