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
This commit is contained in:
Claude
2026-04-03 22:10:43 +00:00
parent d1bfadcaa9
commit 93e3f22c0d
6 changed files with 48 additions and 5 deletions
@@ -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(),
)
@@ -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")
@@ -193,6 +193,7 @@ class AccountSettings(
var backupTrustProviderList: TrustProviderListEvent? = null,
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val dismissedPollNoteIds: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = 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
// ----
@@ -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
@@ -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,
@@ -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<List<Note>> =
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,