From 3a78dd6afe0ab8d1f2a2db4347ead3cddfb398e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Apr 2026 02:22:18 +0000 Subject: [PATCH] feat: add "View results" option to polls that prevents voting after viewing Adds a "View results" link below poll voting options. Once a user clicks it, the poll results are shown and the user can no longer vote on that poll. The viewed state is persisted in account settings via SharedPreferences. https://claude.ai/code/session_01EkUYT4giQPUvbAJZ54o1se --- .../amethyst/LocalPreferences.kt | 4 +++ .../vitorpamplona/amethyst/model/Account.kt | 4 +++ .../amethyst/model/AccountSettings.kt | 16 +++++++++ .../amethyst/ui/note/types/Poll.kt | 35 +++++++++++++++---- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +++ amethyst/src/main/res/values/strings.xml | 1 + 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 59a8526965..51a2719e46 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,7 @@ object LocalPreferences { ) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value) putStringSet(PrefKeys.DISMISSED_POLL_NOTE_IDS, settings.dismissedPollNoteIds.value) + putStringSet(PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, settings.viewedPollResultNoteIds.value) putString( PrefKeys.PENDING_ATTESTATIONS, @@ -476,6 +478,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 viewedPollResultNoteIds = getStringSet(PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, null) ?: setOf() val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf() val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) @@ -598,6 +601,7 @@ object LocalPreferences { lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion), dismissedPollNoteIds = MutableStateFlow(dismissedPollNoteIds), + viewedPollResultNoteIds = MutableStateFlow(viewedPollResultNoteIds), 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..c913f3de11 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,10 @@ class Account( fun dismissPollNotification(noteId: String) = settings.dismissPollNotification(noteId) + fun hasViewedPollResults(noteId: String) = settings.hasViewedPollResults(noteId) + + fun markPollResultsViewed(noteId: String) = settings.markPollResultsViewed(noteId) + 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..fdc6b5520d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -194,6 +194,7 @@ class AccountSettings( val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), val hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), val dismissedPollNoteIds: MutableStateFlow> = MutableStateFlow(setOf()), + val viewedPollResultNoteIds: MutableStateFlow> = MutableStateFlow(setOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow(mapOf()), var backupNipA3PaymentTargets: PaymentTargetsEvent? = null, ) : EphemeralChatRepository, @@ -695,6 +696,21 @@ class AccountSettings( } } + // --- + // viewed poll results + // --- + + fun hasViewedPollResults(noteId: String) = viewedPollResultNoteIds.value.contains(noteId) + + fun markPollResultsViewed(noteId: String) { + if (!viewedPollResultNoteIds.value.contains(noteId)) { + viewedPollResultNoteIds.update { + it + noteId + } + saveAccountSettings() + } + } + // ---- // 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..049ef36e07 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) }, + 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..00043b7f1e 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,10 @@ class AccountViewModel( fun dismissPollNotification(noteId: String) = account.dismissPollNotification(noteId) + fun hasViewedPollResults(noteId: String) = account.hasViewedPollResults(noteId) + + fun markPollResultsViewed(noteId: String) = account.markPollResultsViewed(noteId) + 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