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 dfe09eb44..54f7189a5 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 @@ -165,6 +165,7 @@ private fun FeedLoaded( nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val openPolls by openPollsState(accountViewModel) LazyColumn( modifier = Modifier.fillMaxSize(), @@ -175,6 +176,34 @@ private fun FeedLoaded( ShowDonationCard(accountViewModel, nav) } + if (openPolls.isNotEmpty()) { + item { + OpenPollsSectionHeader() + } + + itemsIndexed( + items = openPolls, + key = { _, item -> "open-poll-${item.idHex}" }, + contentType = { _, _ -> "OpenPoll" }, + ) { _, note -> + Row(Modifier.fillMaxWidth().animateItem()) { + NoteCompose( + baseNote = note, + modifier = Modifier.fillMaxWidth(), + routeForLastRead = routeForLastRead, + isBoostedNote = false, + isQuotedNote = false, + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + HorizontalDivider( + thickness = DividerThickness, + ) + } + } + itemsIndexed( items = items.list, key = { _, item -> item.id() }, 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 new file mode 100644 index 000000000..ec1952309 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/OpenPollsSection.kt @@ -0,0 +1,95 @@ +/* + * 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.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() { + Text( + text = stringRes(R.string.open_polls), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + ) +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 27d5402e2..82350e045 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -258,6 +258,7 @@ "Error loading replies: " Try again No notifications yet. + Open Polls Feed is empty. Refresh created