Brings the creation of Zap Polls back.

This commit is contained in:
Vitor Pamplona
2026-03-19 12:40:14 -04:00
parent 0f4555cfaa
commit 3f4e265dc0
37 changed files with 976 additions and 109 deletions
@@ -58,7 +58,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryE
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -961,7 +961,7 @@ object LocalCache : ILocalCache, ICacheProvider {
@Suppress("DEPRECATION")
fun computeReplyTo(event: Event): List<Note> =
when (event) {
is PollNoteEvent -> {
is ZapPollEvent -> {
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
}
@@ -1059,7 +1059,7 @@ object LocalCache : ILocalCache, ICacheProvider {
}
fun consume(
event: PollNoteEvent,
event: ZapPollEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeRegularEvent(event, relay, wasVerified)
@@ -3141,7 +3141,7 @@ object LocalCache : ILocalCache, ICacheProvider {
is PublicMessageEvent -> consume(event, relay, wasVerified)
is PeopleListEvent -> consume(event, relay, wasVerified)
is CodeSnippetEvent -> consume(event, relay, wasVerified)
is PollNoteEvent -> consume(event, relay, wasVerified)
is ZapPollEvent -> consume(event, relay, wasVerified)
is PollEvent -> consume(event, relay, wasVerified)
is PollResponseEvent -> consume(event, relay, wasVerified)
is ReactionEvent -> consume(event, relay, wasVerified)
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -65,7 +65,7 @@ val NotificationsPerKeyKinds =
ChannelMessageEvent.KIND,
EphemeralChatEvent.KIND,
BadgeAwardEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
PollEvent.KIND,
PollResponseEvent.KIND,
PublicMessageEvent.KIND,
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
@@ -45,7 +45,7 @@ val RepliesAndReactionsToAddressesKinds1 =
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
CommentEvent.KIND,
)
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
@@ -63,7 +63,7 @@ val RepliesAndReactionsKinds2 =
TorrentCommentEvent.KIND,
GitReplyEvent.KIND,
PollResponseEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
)
fun filterRepliesAndReactionsToNotes(
@@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -64,7 +64,7 @@ val SearchPostsByTextKinds1 =
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
ChannelCreateEvent.KIND,
)
@@ -184,7 +184,7 @@ import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -1039,7 +1039,7 @@ private fun RenderNoteRow(
)
}
is PollNoteEvent -> {
is ZapPollEvent -> {
RenderZapPoll(
baseNote,
makeItShort,
@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -32,7 +34,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -58,8 +59,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
@@ -93,13 +97,15 @@ import com.vitorpamplona.amethyst.ui.theme.BigPadding
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size14Modifier
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.collections.immutable.ImmutableList
@@ -115,7 +121,7 @@ import kotlin.uuid.Uuid
@Composable
fun ZapZapPollNotePreview() {
val event =
PollNoteEvent(
ZapPollEvent(
id = "6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72",
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
createdAt = 1729186078,
@@ -188,7 +194,7 @@ fun ZapZapPollNotePreview() {
@Composable
fun ZapZapPollNotePreview2() {
val event =
PollNoteEvent(
ZapPollEvent(
id = "3064bf97800a4b04b612fc0fd498936eae75fffbdca5bbd09d19a6dc598530ab",
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
createdAt = 1729191389,
@@ -311,13 +317,6 @@ private fun OptionNote(
modifier = Modifier.padding(vertical = 3.dp),
) {
if (!pollViewModel.canZap.value) {
val color =
if (poolOption.consensusThreadhold.value) {
Color.Green.copy(alpha = 0.32f)
} else {
MaterialTheme.colorScheme.mediumImportanceLink
}
ZapVote(
baseNote,
poolOption,
@@ -326,7 +325,7 @@ private fun OptionNote(
RenderOptionAfterVote(
baseNote,
poolOption,
color,
poolOption.consensusThreadhold.value,
canPreview,
tags,
backgroundColor,
@@ -366,7 +365,7 @@ private fun OptionNote(
private fun RenderOptionAfterVote(
baseNote: Note,
poolOption: PollOption,
color: Color,
isWinning: Boolean,
canPreview: Boolean,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
@@ -376,14 +375,25 @@ private fun RenderOptionAfterVote(
Box(
Modifier
.fillMaxWidth(0.75f)
.clip(shape = QuoteBorder)
.clip(SmallishBorder)
.border(
2.dp,
color,
QuoteBorder,
width = 1.dp,
color =
if (isWinning) {
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colorScheme.grayText
},
shape = SmallishBorder,
).background(
if (isWinning) {
MaterialTheme.colorScheme.allGoodColor.copy(0.2f)
} else {
MaterialTheme.colorScheme.subtleBorder
},
),
) {
DisplayProgress(poolOption, color, modifier = Modifier.matchParentSize())
DisplayProgress(poolOption, isWinning, modifier = Modifier.matchParentSize())
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -428,22 +438,29 @@ private fun RenderOptionAfterVote(
@Composable
private fun DisplayProgress(
poolOption: PollOption,
color: Color,
isWinning: Boolean,
modifier: Modifier,
) {
val progress by poolOption.tally
// Animate the progress bar when a vote is cast
val animatedProgress by animateFloatAsState(
targetValue = poolOption.tally.value,
animationSpec = tween(durationMillis = 800),
)
// The LinearProgressIndicator has some weird update issues and renders inaccurate percentages.
Box(modifier = modifier) {
Box(
modifier =
Modifier
.fillMaxWidth(progress)
.fillMaxHeight()
.background(color = color),
) {
}
}
val progressBarColor = if (isWinning) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.primary
Box(
modifier =
modifier
.alpha(0.32f)
.drawWithContent {
// Clip the drawing area to show only the progress amount
clipRect(right = size.width * animatedProgress) {
drawRect(progressBarColor)
}
drawContent()
},
)
}
@Composable
@@ -475,11 +492,11 @@ private fun RenderOptionBeforeVote(
Box(
Modifier
.fillMaxWidth(0.75f)
.clip(shape = QuoteBorder)
.clip(SmallishBorder)
.border(
2.dp,
MaterialTheme.colorScheme.primary,
QuoteBorder,
width = 1.dp,
color = MaterialTheme.colorScheme.placeholderText,
shape = SmallishBorder,
),
) {
Column(BigPadding) {
@@ -568,7 +585,19 @@ fun ZapVote(
showErrorMessageDialog = StringToastMsg(title, message)
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
onPayViaIntent = {},
onPayViaIntent = {
if (it.size == 1) {
val payable = it.first()
payViaIntent(payable.invoice, context, { }) { error ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(stringRes(context, R.string.error_dialog_zap_error), error)
}
} else {
val uid = Uuid.random().toString()
accountViewModel.tempManualPaymentCache.put(uid, it)
nav.nav(Route.ManualZapSplitPayment(uid))
}
},
)
} else {
wantsToZap = true
@@ -29,7 +29,7 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -53,7 +53,7 @@ class PollNoteViewModel : ViewModel() {
private lateinit var account: Account
private var pollNote: Note? = null
private var pollEvent: PollNoteEvent? = null
private var pollEvent: ZapPollEvent? = null
private var pollOptions: Map<Int, String>? = null
private var valueMaximum: Long? = null
private var valueMinimum: Long? = null
@@ -76,7 +76,7 @@ class PollNoteViewModel : ViewModel() {
fun load(note: Note?) {
if (pollNote != note) {
pollNote = note
pollEvent = pollNote?.event as PollNoteEvent
pollEvent = pollNote?.event as ZapPollEvent
pollOptions = pollEvent?.pollOptions()
valueMaximum = pollEvent?.maxAmount()
valueMinimum = pollEvent?.minAmount()
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.creators.zappolls
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.CancellationException
@Composable
fun ZapPollConsensusThreshold(pollViewModel: ShortNotePostViewModel) {
var text by rememberSaveable { mutableStateOf("") }
pollViewModel.isValidConsensusThreshold.value = true
if (text.isNotEmpty()) {
try {
val int = text.toInt()
if (int !in 0..100) {
pollViewModel.isValidConsensusThreshold.value = false
} else {
pollViewModel.zapPollConsensusThreshold = int
}
} catch (e: Exception) {
if (e is CancellationException) throw e
pollViewModel.isValidConsensusThreshold.value = false
}
}
val colorInValid =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red,
)
val colorValid =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText,
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
OutlinedTextField(
value = text,
onValueChange = { text = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.width(150.dp),
colors = if (pollViewModel.isValidConsensusThreshold.value) colorValid else colorInValid,
label = {
Text(
text = stringRes(R.string.poll_consensus_threshold),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringRes(R.string.poll_consensus_threshold_percent),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
}
}
@SuppressLint("ViewModelConstructorInComposable")
@Preview
@Composable
fun ZapPollConsensusThresholdPreview() {
ZapPollConsensusThreshold(ShortNotePostViewModel())
}
@@ -0,0 +1,170 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.creators.zappolls
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerDialog
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.utils.TimeUtils
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) {
var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
// Get current time details
val currentTime = Instant.ofEpochMilli(model.zapPollClosedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime()
val datePickerState =
rememberDatePickerState(
initialSelectedDateMillis = model.zapPollClosedAt * 1000,
yearRange = currentTime.year..2050,
selectableDates =
object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
// Only allow today and future dates
return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer
}
},
)
val timePickerState =
rememberTimePickerState(
initialHour = currentTime.hour,
initialMinute = currentTime.minute,
is24Hour = false, // Set to true if you prefer military time
)
val context = LocalContext.current
OutlinedCard(
onClick = { showDatePicker = true },
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.DateRange, contentDescription = stringResource(R.string.accessibility_select_date))
Spacer(Modifier.width(12.dp))
if (model.zapPollClosedAt < TimeUtils.oneMinuteFromNow()) {
Text(stringRes(R.string.poll_closing_date_time) + " " + model.zapPollClosedAt, style = MaterialTheme.typography.bodyLarge)
} else {
Text(
text = stringRes(R.string.poll_closing_in, timeAheadNoDot(model.zapPollClosedAt, context)),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
// --- Date Picker Dialog ---
if (showDatePicker) {
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
showDatePicker = false
showTimePicker = true
}) { Text(stringResource(R.string.next)) }
},
) {
DatePicker(state = datePickerState)
}
}
// --- Time Picker Dialog ---
if (showTimePicker) {
TimePickerDialog(
title = {
Text(stringResource(R.string.closing_time))
},
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(
onClick = {
val datetimeLocalTimeZone =
datePickerState.selectedDateMillis?.let { localDayAtZeroHourMillis ->
(localDayAtZeroHourMillis / 1000) +
(timePickerState.hour * TimeUtils.ONE_HOUR) +
(timePickerState.minute * TimeUtils.ONE_MINUTE)
} ?: TimeUtils.oneDayAhead()
// Get the offset from UTC for the current instant in the local time zone
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
model.zapPollClosedAt = datetimeLocalTimeZone - offset.totalSeconds
showTimePicker = false
},
) { Text(stringResource(R.string.confirm)) }
},
) {
TimePicker(state = timePickerState)
}
}
}
@SuppressLint("ViewModelConstructorInComposable")
@Preview
@Composable
fun ZapPollDeadlinePickerPreview() {
ZapPollDeadlinePicker(
ShortNotePostViewModel(),
)
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.creators.zappolls
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun ZapPollField(postViewModel: ShortNotePostViewModel) {
val optionsList = postViewModel.zapPollOptions
Column(
modifier = Modifier.fillMaxWidth(),
) {
optionsList.forEach { value ->
ZapPollOption(postViewModel, value.key)
}
Spacer(Modifier.height(8.dp))
ZapPollDeadlinePicker(postViewModel)
ZapPollVoteValueRange(postViewModel)
Button(
onClick = {
// postViewModel.pollOptions[postViewModel.pollOptions.size] = ""
optionsList[optionsList.size] = ""
},
border =
BorderStroke(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
),
colors =
ButtonDefaults.outlinedButtonColors(
containerColor = MaterialTheme.colorScheme.surface,
),
) {
Icon(Icons.Default.Add, contentDescription = stringRes(R.string.add_poll_option_button))
}
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.creators.zappolls
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun ZapPollOption(
pollViewModel: ShortNotePostViewModel,
optionIndex: Int,
) {
Row {
val deleteIcon: @Composable (() -> Unit) = {
IconButton(
onClick = {
pollViewModel.removeZapPollOption(optionIndex)
},
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringRes(R.string.clear),
)
}
}
OutlinedTextField(
modifier = Modifier.weight(1F),
value = pollViewModel.zapPollOptions[optionIndex] ?: "",
onValueChange = {
pollViewModel.updateZapPollOption(optionIndex, it)
},
label = {
Text(
text = stringRes(R.string.poll_option_index).format(optionIndex + 1),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringRes(R.string.poll_option_description),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
// colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else
// colorInValid,
trailingIcon = if (optionIndex > 1) deleteIcon else null,
)
}
}
@SuppressLint("ViewModelConstructorInComposable")
@Preview
@Composable
fun ZapPollOptionPreview() {
ZapPollOption(ShortNotePostViewModel(), 0)
}
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.creators.zappolls
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun ZapPollVoteValueRange(pollViewModel: ShortNotePostViewModel) {
val colorInValid =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red,
)
val colorValid =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText,
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
OutlinedTextField(
value = pollViewModel.zapPollValueMinimum?.toString() ?: "",
onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidValueMinimum.value) colorValid else colorInValid,
label = {
Text(
text = stringRes(R.string.poll_zap_value_min),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringRes(R.string.sats),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
Spacer(modifier = DoubleHorzSpacer)
OutlinedTextField(
value = pollViewModel.zapPollValueMaximum?.toString() ?: "",
onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f),
colors = if (pollViewModel.isValidValueMaximum.value) colorValid else colorInValid,
label = {
Text(
text = stringRes(R.string.poll_zap_value_max),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringRes(R.string.sats),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(
text = stringRes(R.string.poll_zap_value_min_max_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp),
)
}
}
@SuppressLint("ViewModelConstructorInComposable")
@Preview
@Composable
fun ZapPollVoteValueRangePreview() {
Column(
modifier = Modifier.fillMaxWidth(),
) {
ZapPollVoteValueRange(ShortNotePostViewModel())
}
}
@@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
@@ -61,7 +61,7 @@ fun RenderZapPoll(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? PollNoteEvent ?: return
val noteEvent = note.event as? ZapPollEvent ?: return
val eventContent = noteEvent.content
val showReply by
@@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -102,7 +102,7 @@ class FollowPackFeedConversationsFeedFilter(
): Boolean =
(
event is TextNoteEvent ||
event is PollNoteEvent ||
event is ZapPollEvent ||
event is ChannelMessageEvent ||
event is CommentEvent ||
event is VoiceReplyEvent ||
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -130,7 +130,7 @@ class FollowPackFeedNewThreadFeedFilter(
noteEvent is GenericRepostEvent ||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
noteEvent is PollNoteEvent ||
noteEvent is ZapPollEvent ||
noteEvent is HighlightEvent ||
noteEvent is InteractiveStoryPrologueEvent ||
noteEvent is CommentEvent ||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
@@ -77,7 +77,7 @@ class GeoHashFeedFilter(
event is WikiNoteEvent ||
event is ChannelMessageEvent ||
event is PrivateDmEvent ||
event is PollNoteEvent ||
event is ZapPollEvent ||
event is AudioHeaderEvent
) &&
event.isTaggedGeoHash(geohash)
@@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -43,7 +43,7 @@ val PostsByGeohashKinds =
ChannelMessageEvent.KIND,
LongTextNoteEvent.KIND,
PollEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
@@ -81,7 +81,7 @@ class HashtagFeedFilter(
event is WikiNoteEvent ||
event is ChannelMessageEvent ||
event is PrivateDmEvent ||
event is PollNoteEvent ||
event is ZapPollEvent ||
event is AudioHeaderEvent
) &&
event.isTaggedHash(hashTag)
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -61,7 +61,7 @@ val PostsByHashtagKinds2 =
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
NipTextEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
)
fun filterPostsByHashtags(
@@ -94,15 +94,18 @@ import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiBut
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -299,6 +302,15 @@ private fun NewPostScreenBody(
}
}
if (postViewModel.wantsZapPoll) {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
) {
ZapPollField(postViewModel)
}
}
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
if (postViewModel.wantsToMarkAsSensitive) {
@@ -531,11 +543,24 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
)
if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringRes(R.string.poll_hashtag)
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
if (postViewModel.wantsPoll) {
if (postViewModel.wantsZapPoll) {
postViewModel.wantsZapPoll = false
}
}
}
}
if (postViewModel.canUseZapPoll) {
AddZapPollButton(postViewModel.wantsZapPoll) {
postViewModel.wantsZapPoll = !postViewModel.wantsZapPoll
if (postViewModel.wantsZapPoll) {
if (postViewModel.wantsPoll) {
postViewModel.wantsPoll = false
}
}
}
}
@@ -584,6 +609,32 @@ private fun BottomRowActionsPreview() {
}
}
@Composable
private fun AddZapPollButton(
isPollActive: Boolean,
onClick: () -> Unit,
) {
IconButton(
onClick = { onClick() },
) {
if (!isPollActive) {
Icon(
painter = painterRes(R.drawable.ic_poll, 1),
contentDescription = stringRes(id = R.string.poll),
modifier = Size19Modifier,
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
painter = painterRes(R.drawable.ic_poll, 1),
contentDescription = stringRes(id = R.string.disable_poll),
modifier = Size19Modifier,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
@Composable
private fun AddPollButton(
isPollActive: Boolean,
@@ -71,6 +71,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaA
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.experimental.zapPolls.closedAt
import com.vitorpamplona.quartz.experimental.zapPolls.consensusThreshold
import com.vitorpamplona.quartz.experimental.zapPolls.maxAmount
import com.vitorpamplona.quartz.experimental.zapPolls.minAmount
import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
@@ -230,6 +236,20 @@ open class ShortNotePostViewModel :
var pollOptions: SnapshotStateMap<Int, OptionTag> = newStateMapPollOptions()
var closedAt by mutableLongStateOf(TimeUtils.oneDayAhead())
// ZapPolls
var canUseZapPoll by mutableStateOf(false)
var wantsZapPoll by mutableStateOf(false)
var zapPollOptions: SnapshotStateMap<Int, String> = newStateMapZapPollOptions()
var zapPollValueMaximum by mutableStateOf<Long?>(null)
var zapPollValueMinimum by mutableStateOf<Long?>(null)
var zapPollConsensusThreshold: Int? = null
var zapPollClosedAt by mutableLongStateOf(TimeUtils.oneDayAhead())
var isValidValueMaximum = mutableStateOf(true)
var isValidValueMinimum = mutableStateOf(true)
var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true)
// Invoices
var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false)
@@ -333,6 +353,7 @@ open class ShortNotePostViewModel :
canAddInvoice = user.lnAddress() != null
canAddZapRaiser = user.lnAddress() != null
canUsePoll = originalNote == null
canUseZapPoll = originalNote == null
multiOrchestrator = null
quote?.let { quotedNote ->
@@ -417,6 +438,10 @@ open class ShortNotePostViewModel :
if (draftEvent is PollEvent) {
loadFromDraft(draftEvent)
}
if (draftEvent is ZapPollEvent) {
loadFromDraft(draftEvent)
}
}
private fun loadFromDraft(draftEvent: TextNoteEvent) {
@@ -489,12 +514,14 @@ open class ShortNotePostViewModel :
}
canUsePoll = originalNote == null
canUseZapPoll = originalNote == null
if (forwardZapTo.value.items.isNotEmpty()) {
wantsForwardZapTo = true
}
wantsPoll = false
wantsZapPoll = false
message = TextFieldValue(draftEvent.content)
@@ -550,11 +577,14 @@ open class ShortNotePostViewModel :
}
canUsePoll = originalNote == null
canUseZapPoll = originalNote == null
if (forwardZapTo.value.items.isNotEmpty()) {
wantsForwardZapTo = true
}
wantsZapPoll = false
val polls = draftEvent.options()
wantsPoll = polls.isNotEmpty()
@@ -571,6 +601,80 @@ open class ShortNotePostViewModel :
urlPreviews.update(message)
}
private fun loadFromDraft(draftEvent: ZapPollEvent) {
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().lnAddress() != null
multiOrchestrator = null
val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
forwardZapTo.value = SplitBuilder()
localForwardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
contentWarningDescription = draftEvent.contentWarningReason() ?: ""
val draftExpiration = draftEvent.tags.expiration()
wantsExpirationDate = draftExpiration != null
expirationDate = draftExpiration ?: TimeUtils.oneDayAhead()
val geohash = draftEvent.getGeoHash()
wantsToAddGeoHash = geohash != null
if (geohash != null) {
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
}
val zapRaiser = draftEvent.zapraiserAmount()
wantsZapRaiser = zapRaiser != null
zapRaiserAmount.value = null
if (zapRaiser != null) {
zapRaiserAmount.value = zapRaiser
}
eTags =
draftEvent.tags.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }.mapNotNull {
val note = LocalCache.checkGetOrCreateNote(it[1])
note
}
pTags =
draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.map {
LocalCache.getOrCreateUser(it[1])
}
canUsePoll = originalNote == null
canUseZapPoll = originalNote == null
if (forwardZapTo.value.items.isNotEmpty()) {
wantsForwardZapTo = true
}
wantsPoll = false
val polls = draftEvent.pollOptionsArray()
wantsZapPoll = polls.isNotEmpty()
polls.forEach { tag ->
zapPollOptions[tag.index] = tag.descriptor
}
zapPollValueMinimum = draftEvent.minAmount()
zapPollValueMaximum = draftEvent.maxAmount()
zapPollConsensusThreshold = draftEvent.consensusThreshold()?.let { (it * 100).toInt() }
zapPollClosedAt = draftEvent.closedAt() ?: TimeUtils.oneDayAhead()
message = TextFieldValue(draftEvent.content)
iMetaAttachments.addAll(draftEvent.imetas())
urlPreviews.update(message)
}
suspend fun sendPostSync() {
// Upload voice message first if it hasn't been uploaded yet
if (voiceRecording != null && voiceMetadata == null) {
@@ -731,6 +835,35 @@ open class ShortNotePostViewModel :
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else if (wantsZapPoll) {
val options = zapPollOptions.map { PollOptionTag(it.key, it.value) }
if (options.isEmpty()) return null
val quotes = findNostrUris(tagger.message)
val relays =
accountViewModel.account.nip65RelayList.outboxFlow.value
.toList()
ZapPollEvent.build(tagger.message, options) {
zapPollValueMinimum?.let { minAmount(it) }
zapPollValueMaximum?.let { maxAmount(it) }
zapPollClosedAt?.let { closedAt(it) }
zapPollConsensusThreshold?.let { consensusThreshold(it / 100.0) }
pTags(tagger.directMentionsUsers.map { it.toPTag() })
quotes(quotes)
hashtags(findHashtags(tagger.message))
geoHash?.let { geohash(it) }
localZapRaiserAmount?.let { zapraiser(it) }
zapReceiver?.let { zapSplits(it) }
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
@@ -915,6 +1048,13 @@ open class ShortNotePostViewModel :
pollOptions = newStateMapPollOptions()
closedAt = TimeUtils.oneDayAhead()
wantsZapPoll = false
zapPollOptions = newStateMapZapPollOptions()
zapPollValueMaximum = null
zapPollValueMinimum = null
zapPollConsensusThreshold = null
zapPollClosedAt = TimeUtils.oneDayAhead()
wantsInvoice = false
wantsZapRaiser = false
zapRaiserAmount.value = null
@@ -1032,6 +1172,12 @@ open class ShortNotePostViewModel :
1 to OptionTag(RandomInstance.randomChars(6), ""),
)
private fun newStateMapZapPollOptions(): SnapshotStateMap<Int, String> =
mutableStateMapOf(
0 to "",
1 to "",
)
fun canPost(): Boolean {
// Voice messages can be posted without text (with either uploaded or pending recording)
if (voiceMetadata != null || voiceRecording != null) {
@@ -1052,6 +1198,14 @@ open class ShortNotePostViewModel :
closedAt > TimeUtils.oneMinuteFromNow()
)
) &&
(
!wantsZapPoll ||
(
zapPollOptions.values.all { it.isNotEmpty() } &&
isValidValueMinimum.value &&
isValidValueMaximum.value
)
) &&
multiOrchestrator == null
}
@@ -1254,6 +1408,61 @@ open class ShortNotePostViewModel :
this.putAll(newEntries)
}
// ---
// Zap Polls
// ---
fun updateMinZapAmountForPoll(textMin: String) {
zapPollValueMinimum = textMin.toLongOrNull()?.takeIf { it > 0 }
checkMinMax()
draftTag.newVersion()
}
fun updateMaxZapAmountForPoll(textMax: String) {
zapPollValueMaximum = textMax.toLongOrNull()?.takeIf { it > 0 }
checkMinMax()
draftTag.newVersion()
}
fun checkMinMax() {
if ((zapPollValueMinimum ?: 0) > (zapPollValueMaximum ?: Long.MAX_VALUE)) {
isValidValueMinimum.value = false
isValidValueMaximum.value = false
} else {
isValidValueMinimum.value = true
isValidValueMaximum.value = true
}
}
fun removeZapPollOption(optionIndex: Int) {
zapPollOptions.removeOrderedZapPoll(optionIndex)
draftTag.newVersion()
}
private fun MutableMap<Int, String>.removeOrderedZapPoll(index: Int) {
val keyList = keys
val elementList = values.toMutableList()
run stop@{
for (i in index until elementList.size) {
val nextIndex = i + 1
if (nextIndex == elementList.size) return@stop
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
}
}
elementList.removeAt(elementList.size - 1)
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
this.clear()
this.putAll(newEntries)
}
fun updateZapPollOption(
optionIndex: Int,
text: String,
) {
zapPollOptions[optionIndex] = text
draftTag.newVersion()
}
fun toggleMarkAsSensitive() {
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
draftTag.newVersion()
@@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -81,7 +81,7 @@ class HomeConversationsFeedFilter(
): Boolean =
(
event is TextNoteEvent ||
event is PollNoteEvent ||
event is ZapPollEvent ||
event is PollResponseEvent ||
event is ChannelMessageEvent ||
event is CommentEvent ||
@@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -116,7 +116,7 @@ class HomeNewThreadFeedFilter(
noteEvent is GenericRepostEvent ||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
noteEvent is PollNoteEvent ||
noteEvent is ZapPollEvent ||
noteEvent is PollEvent ||
noteEvent is HighlightEvent ||
noteEvent is InteractiveStoryPrologueEvent ||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -60,7 +60,7 @@ val HomePostsNewThreadKinds =
HighlightEvent.KIND,
WikiNoteEvent.KIND,
NipTextEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
PollEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
ChessGameEvent.KIND,
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
@@ -106,7 +106,7 @@ class NotificationFeedFilter(
LiveActivitiesChatMessageEvent.KIND,
PictureEvent.KIND,
PollEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
PrivateDmEvent.KIND,
PublicMessageEvent.KIND,
VideoNormalEvent.KIND,
@@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
@@ -64,7 +64,7 @@ class UserProfileConversationsFeedFilter(
it.author == user &&
(
it.event is TextNoteEvent ||
it.event is PollNoteEvent ||
it.event is ZapPollEvent ||
it.event is PollResponseEvent ||
it.event is ChannelMessageEvent ||
it.event is LiveActivitiesChatMessageEvent ||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -64,7 +64,7 @@ val UserProfilePostKinds2 =
InteractiveStoryPrologueEvent.KIND,
CommentEvent.KIND,
VoiceReplyEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
PinListEvent.KIND,
)
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -78,7 +78,7 @@ class UserProfileMutualFeedFilter(
it.event is LongTextNoteEvent ||
it.event is WikiNoteEvent ||
it.event is NipTextEvent ||
it.event is PollNoteEvent ||
it.event is ZapPollEvent ||
it.event is HighlightEvent ||
it.event is InteractiveStoryPrologueEvent ||
it.event is AudioTrackEvent ||
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
@@ -80,7 +80,7 @@ class UserProfileNewThreadFeedFilter(
it.event is LongTextNoteEvent ||
it.event is WikiNoteEvent ||
it.event is NipTextEvent ||
it.event is PollNoteEvent ||
it.event is ZapPollEvent ||
it.event is PollEvent ||
it.event is HighlightEvent ||
it.event is InteractiveStoryPrologueEvent ||
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -58,7 +58,7 @@ val PostsByRelayKinds2 =
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
NipTextEvent.KIND,
PollNoteEvent.KIND,
ZapPollEvent.KIND,
)
fun filterPostsByRelay(
@@ -146,7 +146,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryE
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.ErrorDebugMessage
@@ -619,7 +619,7 @@ private fun kindDisplayName(kind: Int): Int =
PeopleListEvent.KIND -> R.string.kind_people_lists
PictureEvent.KIND -> R.string.kind_pictures
PinListEvent.KIND -> R.string.kind_pins
PollNoteEvent.KIND -> R.string.kind_zap_poll
ZapPollEvent.KIND -> R.string.kind_zap_poll
PollEvent.KIND -> R.string.kind_poll
PollResponseEvent.KIND -> R.string.kind_poll_response
PrivateDmEvent.KIND -> R.string.kind_nip04_dms
@@ -199,7 +199,7 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
@@ -692,7 +692,7 @@ private fun FullBleedNoteCompose(
accountViewModel,
nav,
)
} else if (noteEvent is PollNoteEvent) {
} else if (noteEvent is ZapPollEvent) {
RenderZapPoll(
baseNote,
false,