Merge pull request #2139 from vitorpamplona/claude/poll-view-only-results-7VBnX

Add poll results preview feature with persistent viewing state
This commit is contained in:
Vitor Pamplona
2026-04-05 12:24:41 -04:00
committed by GitHub
6 changed files with 86 additions and 6 deletions
@@ -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<Nip47WalletConnect.Nip47URI>(zapPaymentRequestServerStr) }
val defaultFileServer = async { parseOrNull<ServerName>(defaultFileServerStr) ?: DEFAULT_MEDIA_SERVERS[0] }
val viewedPollResultNoteIds = async { parseOrNull<Map<String, Long>>(viewedPollResultNoteIdsStr) ?: mapOf() }
val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() }
val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) }
val latestContactList = async { parseEventOrNull<ContactListEvent>(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(),
)
@@ -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")
@@ -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<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val dismissedPollNoteIds: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val viewedPollResultNoteIds: MutableStateFlow<Map<String, Long>> = MutableStateFlow(mapOf()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = 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<String, Long>): Map<String, Long> {
val now = TimeUtils.now()
return views.filterValues { it > now }
}
// ----
// last read flows
// ----
@@ -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<String>) -> 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,
)
@@ -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
+1
View File
@@ -1789,6 +1789,7 @@
<string name="connected">Connected</string>
<string name="social_proof">Social proof</string>
<string name="poll_submit">Submit</string>
<string name="poll_view_results">View results</string>
<string name="restart">Restart</string>
<string name="chess_accept">Accept</string>
<string name="chess_decline">Decline</string>