diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 59a8526965..c8dcdfbeee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -131,6 +131,7 @@ private object PrefKeys { const val SIGNER_PACKAGE_NAME = "signer_package_name" const val HAS_DONATED_IN_VERSION = "has_donated_in_version" const val DISMISSED_POLL_NOTE_IDS = "dismissed_poll_note_ids" + const val VIEWED_POLL_RESULT_NOTE_IDS = "viewed_poll_result_note_ids" const val PENDING_ATTESTATIONS = "pending_attestations" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" @@ -391,6 +392,10 @@ object LocalPreferences { ) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value) putStringSet(PrefKeys.DISMISSED_POLL_NOTE_IDS, settings.dismissedPollNoteIds.value) + putString( + PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, + JsonMapper.toJson(settings.viewedPollResultNoteIds.value), + ) putString( PrefKeys.PENDING_ATTESTATIONS, @@ -476,6 +481,7 @@ object LocalPreferences { val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false) val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf() val dismissedPollNoteIds = getStringSet(PrefKeys.DISMISSED_POLL_NOTE_IDS, null) ?: setOf() + val viewedPollResultNoteIdsStr = getString(PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, null) val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf() val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) @@ -528,6 +534,7 @@ object LocalPreferences { val zapPaymentRequestServer = async { parseOrNull(zapPaymentRequestServerStr) } val defaultFileServer = async { parseOrNull(defaultFileServerStr) ?: DEFAULT_MEDIA_SERVERS[0] } + val viewedPollResultNoteIds = async { parseOrNull>(viewedPollResultNoteIdsStr) ?: mapOf() } val pendingAttestations = async { parseOrNull>(pendingAttestationsStr) ?: mapOf() } val latestUserMetadata = async { parseEventOrNull(latestUserMetadataStr) } val latestContactList = async { parseEventOrNull(latestContactListStr) } @@ -598,6 +605,7 @@ object LocalPreferences { lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion), dismissedPollNoteIds = MutableStateFlow(dismissedPollNoteIds), + viewedPollResultNoteIds = MutableStateFlow(viewedPollResultNoteIds.await()), pendingAttestations = MutableStateFlow(pendingAttestations.await()), backupNipA3PaymentTargets = latestPaymentTargets.await(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d05c96312d..2349a7307d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2152,6 +2152,13 @@ class Account( fun dismissPollNotification(noteId: String) = settings.dismissPollNotification(noteId) + fun hasViewedPollResults(noteId: String) = settings.hasViewedPollResults(noteId) + + fun markPollResultsViewed( + noteId: String, + pollEndsAt: Long?, + ) = settings.markPollResultsViewed(noteId, pollEndsAt) + init { Log.d("AccountRegisterObservers", "Init") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 7946ee644f..ef54222805 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -194,6 +195,7 @@ class AccountSettings( val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), val hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), val dismissedPollNoteIds: MutableStateFlow> = MutableStateFlow(setOf()), + val viewedPollResultNoteIds: MutableStateFlow> = MutableStateFlow(mapOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow(mapOf()), var backupNipA3PaymentTargets: PaymentTargetsEvent? = null, ) : EphemeralChatRepository, @@ -695,6 +697,38 @@ class AccountSettings( } } + // --- + // viewed poll results + // --- + + fun hasViewedPollResults(noteId: String): Boolean { + val expiresAt = viewedPollResultNoteIds.value[noteId] ?: return false + return expiresAt > TimeUtils.now() + } + + fun markPollResultsViewed( + noteId: String, + pollEndsAt: Long?, + ) { + if (noteId !in viewedPollResultNoteIds.value) { + val expiresAt = + if (pollEndsAt != null && pollEndsAt > TimeUtils.now()) { + pollEndsAt + } else { + TimeUtils.now() + TimeUtils.ONE_DAY + } + viewedPollResultNoteIds.update { + pruneExpiredViews(it) + (noteId to expiresAt) + } + saveAccountSettings() + } + } + + private fun pruneExpiredViews(views: Map): Map { + val now = TimeUtils.now() + return views.filterValues { it > now } + } + // ---- // last read flows // ---- diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 5767a90dc4..8edda46dd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -265,11 +265,14 @@ fun RenderPollCard( RenderPollCard( card = card, + noteId = event.id, onRespond = { responses -> accountViewModel.launchSigner { accountViewModel.account.pollRespond(event, responses) } }, + onViewResults = { accountViewModel.markPollResultsViewed(event.id, event.endsAt()) }, + hasViewedResults = { accountViewModel.hasViewedPollResults(event.id) }, resultContent = galleryUser, labelContent = labelContent, ) @@ -278,7 +281,10 @@ fun RenderPollCard( @Composable fun RenderPollCard( card: PollCard, + noteId: String = "", onRespond: (Set) -> Unit, + onViewResults: () -> Unit = {}, + hasViewedResults: () -> Boolean = { false }, resultContent: @Composable RowScope.(user: User) -> Unit, labelContent: @Composable ColumnScope.(code: String, label: String) -> Unit, ) { @@ -296,12 +302,29 @@ fun RenderPollCard( val haveIVoted by card.haveIVotedFlow.collectAsStateWithLifecycle(haveIVoted) if (haveIVoted) { RenderResults(card, resultContent, labelContent) - } else if (card.hasEnded()) { + } else if (card.hasEnded() || hasViewedResults()) { RenderResults(card, resultContent, labelContent) } else { - when (card.type) { - PollType.SINGLE_CHOICE -> RenderSingleChoiceOptions(card, labelContent, onRespond) - PollType.MULTI_CHOICE -> RenderMultiChoiceOptions(card, labelContent, onRespond) + var viewingResults by remember { mutableStateOf(false) } + if (viewingResults) { + RenderResults(card, resultContent, labelContent) + } else { + when (card.type) { + PollType.SINGLE_CHOICE -> RenderSingleChoiceOptions(card, labelContent, onRespond) + PollType.MULTI_CHOICE -> RenderMultiChoiceOptions(card, labelContent, onRespond) + } + + Text( + text = stringRes(R.string.poll_view_results), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + modifier = + Modifier + .clickable { + onViewResults() + viewingResults = true + }.padding(vertical = 4.dp), + ) } } } @@ -615,7 +638,7 @@ fun RenderPollManualPreview() { ThemeComparisonColumn { Column(Modifier.padding(10.dp)) { - RenderPollCard(poll, {}, {}) { _, label -> + RenderPollCard(poll, onRespond = {}, resultContent = {}) { _, label -> Text( text = label, ) @@ -663,7 +686,7 @@ fun RenderPollManualLongPreview() { ThemeComparisonColumn { Column(Modifier.padding(10.dp)) { - RenderPollCard(poll, {}, {}) { _, label -> + RenderPollCard(poll, onRespond = {}, resultContent = {}) { _, label -> Text( text = label, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index bf3ba6f9dd..801fc8a704 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1128,6 +1128,13 @@ class AccountViewModel( fun dismissPollNotification(noteId: String) = account.dismissPollNotification(noteId) + fun hasViewedPollResults(noteId: String) = account.hasViewedPollResults(noteId) + + fun markPollResultsViewed( + noteId: String, + pollEndsAt: Long?, + ) = account.markPollResultsViewed(noteId, pollEndsAt) + fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom.value fun translateTo() = account.settings.syncedSettings.languages.translateTo.value diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 261c5f8a1c..ea2c30d389 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1789,6 +1789,7 @@ Connected Social proof Submit + View results Restart Accept Decline