From 93e3f22c0d827262430b76a85bfbbe6f70e91384 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 22:10:43 +0000 Subject: [PATCH] feat: add dismiss button to active poll notification cards Allow users to individually dismiss poll notification cards at the top of the notification screen, matching the existing Zap the Devs dismiss pattern. Dismissed poll IDs are persisted in SharedPreferences. https://claude.ai/code/session_013ECrmmshiF9SNTxr9SXkvC --- .../vitorpamplona/amethyst/LocalPreferences.kt | 4 ++++ .../com/vitorpamplona/amethyst/model/Account.kt | 2 ++ .../amethyst/model/AccountSettings.kt | 16 ++++++++++++++++ .../ui/screen/loggedIn/AccountViewModel.kt | 2 ++ .../loggedIn/notifications/CardFeedView.kt | 16 +++++++++++++++- .../loggedIn/notifications/OpenPollsState.kt | 13 +++++++++---- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index a0a006ceb..59a852696 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -130,6 +130,7 @@ private object PrefKeys { const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" 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 PENDING_ATTESTATIONS = "pending_attestations" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" @@ -389,6 +390,7 @@ object LocalPreferences { JsonMapper.toJson(regularMap), ) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value) + putStringSet(PrefKeys.DISMISSED_POLL_NOTE_IDS, settings.dismissedPollNoteIds.value) putString( PrefKeys.PENDING_ATTESTATIONS, @@ -473,6 +475,7 @@ object LocalPreferences { val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) 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 localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf() val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) @@ -594,6 +597,7 @@ object LocalPreferences { backupTrustProviderList = latestTrustProviderList.await(), lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion), + dismissedPollNoteIds = MutableStateFlow(dismissedPollNoteIds), 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 457f669da..160bde398 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2149,6 +2149,8 @@ class Account( fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) + fun dismissPollNotification(noteId: String) = settings.dismissPollNotification(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 a600384ac..7946ee644 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -193,6 +193,7 @@ class AccountSettings( var backupTrustProviderList: TrustProviderListEvent? = null, val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), val hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), + val dismissedPollNoteIds: MutableStateFlow> = MutableStateFlow(setOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow(mapOf()), var backupNipA3PaymentTargets: PaymentTargetsEvent? = null, ) : EphemeralChatRepository, @@ -679,6 +680,21 @@ class AccountSettings( return false } + // --- + // dismissed polls + // --- + + fun isDismissedPoll(noteId: String) = dismissedPollNoteIds.value.contains(noteId) + + fun dismissPollNotification(noteId: String) { + if (!dismissedPollNoteIds.value.contains(noteId)) { + dismissedPollNoteIds.update { + it + noteId + } + saveAccountSettings() + } + } + // ---- // last read flows // ---- 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 84889bb95..bf3ba6f9d 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 @@ -1126,6 +1126,8 @@ class AccountViewModel( fun markDonatedInThisVersion() = account.markDonatedInThisVersion() + fun dismissPollNotification(noteId: String) = account.dismissPollNotification(noteId) + fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom.value fun translateTo() = account.settings.syncedSettings.languages.translateTo.value 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 ff434728f..f4dba1193 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 @@ -36,6 +36,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Card import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -59,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.BadgeCompose +import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose @@ -184,7 +186,19 @@ private fun FeedLoaded( Card( modifier = MaterialTheme.colorScheme.imageModifier, ) { - OpenPollsSectionHeader() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OpenPollsSectionHeader() + IconButton( + modifier = Modifier.padding(end = Size10dp), + onClick = { accountViewModel.dismissPollNotification(note.idHex) }, + ) { + CloseIcon() + } + } Row(Modifier.fillMaxWidth().animateItem()) { NoteCompose( baseNote = note, 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 index ede972e7c..ee33578d4 100644 --- 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 @@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -47,10 +48,14 @@ class OpenPollsState( ) val flow: StateFlow> = - account.cache - .observeNotes(filter) - .map { notes -> filterOpenPolls(notes) } - .flowOn(Dispatchers.IO) + combine( + account.cache + .observeNotes(filter) + .map { notes -> filterOpenPolls(notes) }, + account.settings.dismissedPollNoteIds, + ) { polls, dismissed -> + polls.filter { it.idHex !in dismissed } + }.flowOn(Dispatchers.IO) .stateIn( scope, SharingStarted.Eagerly,