mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 17:23:21 +00:00
adds basic support for NIP85 Polls
This commit is contained in:
@@ -192,6 +192,8 @@ import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
||||
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
@@ -882,6 +884,10 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
if (event is PollEvent) {
|
||||
relayList.addAll(event.relays())
|
||||
}
|
||||
|
||||
relayList.addAll(computeRelaysForChannels(event))
|
||||
|
||||
return relayList
|
||||
@@ -991,6 +997,23 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun pollRespond(
|
||||
event: PollEvent,
|
||||
responses: Set<String>,
|
||||
) {
|
||||
val poll = cache.getOrCreateNote(event.id).toEventHint<PollEvent>()
|
||||
|
||||
if (poll != null) {
|
||||
val template = PollResponseEvent.build(poll, responses)
|
||||
|
||||
val signedEvent = signer.sign(template)
|
||||
|
||||
cache.justConsumeMyOwnEvent(signedEvent)
|
||||
|
||||
client.send(signedEvent, computeRelayListToBroadcast(signedEvent))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createNip95(
|
||||
byteArray: ByteArray,
|
||||
headerInfo: FileHeader,
|
||||
|
||||
@@ -180,6 +180,8 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||
@@ -1520,6 +1522,12 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
deleteNote.author?.statusStateOrNull()?.removeStatus(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is PollResponseEvent) {
|
||||
deletedEvent.poll()?.eventId?.let {
|
||||
getNoteIfExists(it)?.pollStateOrNull()?.removeResponse(deleteNote)
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedEvent is TorrentCommentEvent) {
|
||||
deletedEvent.torrentIds()?.let {
|
||||
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||
@@ -1998,6 +2006,32 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
wasVerified: Boolean,
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: PollEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeRegularEvent(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: PollResponseEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val pollId = event.poll()?.eventId
|
||||
if (pollId != null) {
|
||||
val pollNote = getOrCreateNote(pollId)
|
||||
val responseNote = getOrCreateNote(event.id)
|
||||
|
||||
val new = consumeRegularEvent(event, relay, wasVerified)
|
||||
if (new) {
|
||||
pollNote.pollState().addResponse(responseNote)
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: FileStorageEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -2611,6 +2645,12 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
note.author?.statusStateOrNull()?.removeStatus(note)
|
||||
}
|
||||
|
||||
if (noteEvent is PollResponseEvent) {
|
||||
noteEvent.poll()?.eventId?.let {
|
||||
getNoteIfExists(it)?.pollStateOrNull()?.removeResponse(note)
|
||||
}
|
||||
}
|
||||
|
||||
note.clearFlow()
|
||||
|
||||
notes.remove(note.idHex)
|
||||
@@ -2998,6 +3038,8 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
is PublicMessageEvent -> consume(event, relay, wasVerified)
|
||||
is PeopleListEvent -> consume(event, relay, wasVerified)
|
||||
is PollNoteEvent -> consume(event, relay, wasVerified)
|
||||
is PollEvent -> consume(event, relay, wasVerified)
|
||||
is PollResponseEvent -> consume(event, relay, wasVerified)
|
||||
is ReactionEvent -> consume(event, relay, wasVerified)
|
||||
is ContactCardEvent -> consume(event, relay, wasVerified)
|
||||
is RelaySetEvent -> consume(event, relay, wasVerified)
|
||||
|
||||
+4
@@ -46,6 +46,8 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
|
||||
val SummaryKinds =
|
||||
listOf(
|
||||
@@ -64,6 +66,8 @@ val NotificationsPerKeyKinds =
|
||||
EphemeralChatEvent.KIND,
|
||||
BadgeAwardEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
PollResponseEvent.KIND,
|
||||
PublicMessageEvent.KIND,
|
||||
)
|
||||
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
@@ -49,7 +50,6 @@ val RepliesAndReactionsKinds =
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
OtsEvent.KIND,
|
||||
TextNoteModificationEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
@@ -62,6 +62,8 @@ val RepliesAndReactionsKinds2 =
|
||||
NIP90StatusEvent.KIND,
|
||||
TorrentCommentEvent.KIND,
|
||||
GitReplyEvent.KIND,
|
||||
PollResponseEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterRepliesAndReactionsToNotes(
|
||||
|
||||
+4
@@ -47,6 +47,8 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
|
||||
val SearchPostsByTextKinds1 =
|
||||
@@ -83,6 +85,8 @@ val SearchPostsByTextKinds3 =
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
FollowListEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
PollResponseEvent.KIND,
|
||||
)
|
||||
|
||||
fun searchPostsByText(
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
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 NewPollClosing(pollViewModel: ShortNotePostViewModel) {
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
pollViewModel.isValidClosedAt.value = true
|
||||
if (text.isNotEmpty()) {
|
||||
try {
|
||||
val int = text.toLong()
|
||||
if (int < 0) {
|
||||
pollViewModel.isValidClosedAt.value = false
|
||||
} else {
|
||||
pollViewModel.closedAt = int
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
pollViewModel.isValidClosedAt.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.isValidClosedAt.value) colorValid else colorInValid,
|
||||
label = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_closing_time),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_closing_time_days),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ViewModelConstructorInComposable")
|
||||
@Preview
|
||||
@Composable
|
||||
fun NewPollClosingPreview() {
|
||||
NewPollClosing(ShortNotePostViewModel())
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
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 NewPollConsensusThreshold(pollViewModel: ShortNotePostViewModel) {
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
pollViewModel.isValidConsensusThreshold.value = true
|
||||
if (text.isNotEmpty()) {
|
||||
try {
|
||||
val int = text.toInt()
|
||||
if (int < 0 || int > 100) {
|
||||
pollViewModel.isValidConsensusThreshold.value = false
|
||||
} else {
|
||||
pollViewModel.consensusThreshold = 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 NewPollConsensusThresholdPreview() {
|
||||
NewPollConsensusThreshold(ShortNotePostViewModel())
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* 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.actions
|
||||
|
||||
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 NewPollOption(
|
||||
pollViewModel: ShortNotePostViewModel,
|
||||
optionIndex: Int,
|
||||
) {
|
||||
Row {
|
||||
val deleteIcon: @Composable (() -> Unit) = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
pollViewModel.removePollOption(optionIndex)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringRes(R.string.clear),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.weight(1F),
|
||||
value = pollViewModel.pollOptions[optionIndex] ?: "",
|
||||
onValueChange = {
|
||||
pollViewModel.updatePollOption(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 NewPollOptionPreview() {
|
||||
NewPollOption(ShortNotePostViewModel(), 0)
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
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.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NewPollPrimaryDescription(pollViewModel: ShortNotePostViewModel) {
|
||||
// initialize focus reference to be able to request focus programmatically
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
var isInputValid = true
|
||||
if (pollViewModel.message.text
|
||||
.isEmpty()
|
||||
) {
|
||||
isInputValid = false
|
||||
}
|
||||
|
||||
val colorInValid =
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.error,
|
||||
unfocusedBorderColor = Color.Red,
|
||||
)
|
||||
val colorValid =
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = pollViewModel.message,
|
||||
onValueChange = { pollViewModel.updateMessage(it) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_primary_description),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().padding(top = 8.dp).focusRequester(focusRequester).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
keyboardController?.show()
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_primary_description),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
colors = if (isInputValid) colorValid else colorInValid,
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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 com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun NewPollRecipientsField(
|
||||
pollViewModel: ShortNotePostViewModel,
|
||||
account: Account,
|
||||
) {
|
||||
// if no recipients, add user's pubkey
|
||||
if (pollViewModel.zapRecipients.isEmpty()) {
|
||||
pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
// TODO allow add multiple recipients and check input validity
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = pollViewModel.zapRecipients[0],
|
||||
onValueChange = { /* TODO */ },
|
||||
enabled = false,
|
||||
label = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_zap_recipients),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_zap_recipients),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* 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.actions
|
||||
|
||||
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 NewPollVoteValueRange(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.valueMinimum?.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.valueMaximum?.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 NewPollVoteValueRangePreview() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
NewPollVoteValueRange(ShortNotePostViewModel())
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -20,8 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -49,7 +51,9 @@ fun RecordVoiceButton(
|
||||
var isRecording by remember { mutableStateOf(false) }
|
||||
var elapsedSeconds by remember { mutableIntStateOf(0) }
|
||||
|
||||
Column {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
// Floating recording indicator at the top
|
||||
FloatingRecordingIndicator(
|
||||
modifier = Modifier.height(50.dp),
|
||||
@@ -58,7 +62,7 @@ fun RecordVoiceButton(
|
||||
)
|
||||
|
||||
RecordAudioBox(
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onRecordTaken = { recording ->
|
||||
isRecording = false
|
||||
elapsedSeconds = 0
|
||||
@@ -77,12 +81,12 @@ fun RecordVoiceButton(
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(48.dp),
|
||||
modifier = Modifier.size(42.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
// Expanding circles background animation
|
||||
ExpandingCirclesAnimation(
|
||||
modifier = Modifier.size(48.dp),
|
||||
modifier = Modifier.size(42.dp),
|
||||
isRecording = recordingState,
|
||||
primaryColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -44,7 +45,7 @@ import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
fun ClickableBox(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier.clickable(
|
||||
@@ -54,9 +55,8 @@ fun ClickableBox(
|
||||
onClick = onClick,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -57,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
|
||||
@@ -111,6 +113,14 @@ fun SensitivityWarning(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ContentWarningNotePreview() {
|
||||
ThemeComparisonColumn {
|
||||
ContentWarningNote({})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
Column {
|
||||
|
||||
@@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.RenderPublicChatChannelHeader
|
||||
@@ -220,6 +221,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
|
||||
@@ -967,7 +969,7 @@ private fun RenderNoteRow(
|
||||
}
|
||||
|
||||
is PollNoteEvent -> {
|
||||
RenderPoll(
|
||||
RenderZapPoll(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
@@ -979,6 +981,18 @@ private fun RenderNoteRow(
|
||||
)
|
||||
}
|
||||
|
||||
is PollEvent -> {
|
||||
RenderPoll(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
is FileHeaderEvent -> {
|
||||
FileHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,46 @@ fun timeAgoNoDot(
|
||||
}
|
||||
}
|
||||
|
||||
fun timeAheadNoDot(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
): String {
|
||||
if (time == null) return " "
|
||||
if (time == 0L) return " ${stringRes(context, R.string.never)}"
|
||||
|
||||
val timeDifference = time - TimeUtils.now()
|
||||
|
||||
return if (timeDifference > TimeUtils.ONE_YEAR) {
|
||||
// Dec 12, 2022
|
||||
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
||||
}
|
||||
|
||||
yearFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.ONE_MONTH) {
|
||||
// Dec 12
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
||||
}
|
||||
|
||||
monthFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.ONE_DAY) {
|
||||
// 2 days
|
||||
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
|
||||
} else if (timeDifference > TimeUtils.ONE_HOUR) {
|
||||
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
|
||||
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
|
||||
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
|
||||
} else {
|
||||
stringRes(context, R.string.now)
|
||||
}
|
||||
}
|
||||
|
||||
fun dateFormatter(
|
||||
time: Long?,
|
||||
never: String,
|
||||
|
||||
+8
-8
@@ -113,7 +113,7 @@ import kotlin.uuid.Uuid
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PollNotePreview() {
|
||||
fun ZapZapPollNotePreview() {
|
||||
val event =
|
||||
PollNoteEvent(
|
||||
id = "6ff9bc13d27490f6e3953325260bd996901a143de89886a0608c39e7d0160a72",
|
||||
@@ -171,7 +171,7 @@ fun PollNotePreview() {
|
||||
Column(
|
||||
Modifier.padding(10.dp),
|
||||
) {
|
||||
PollNote(
|
||||
ZapPollNote(
|
||||
baseNote = baseNote,
|
||||
true,
|
||||
remember { mutableStateOf(color) },
|
||||
@@ -186,7 +186,7 @@ fun PollNotePreview() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PollNotePreview2() {
|
||||
fun ZapZapPollNotePreview2() {
|
||||
val event =
|
||||
PollNoteEvent(
|
||||
id = "3064bf97800a4b04b612fc0fd498936eae75fffbdca5bbd09d19a6dc598530ab",
|
||||
@@ -223,7 +223,7 @@ fun PollNotePreview2() {
|
||||
Column(
|
||||
Modifier.padding(10.dp),
|
||||
) {
|
||||
PollNote(
|
||||
ZapPollNote(
|
||||
baseNote = baseNote,
|
||||
true,
|
||||
remember { mutableStateOf(color) },
|
||||
@@ -237,7 +237,7 @@ fun PollNotePreview2() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollNote(
|
||||
fun ZapPollNote(
|
||||
baseNote: Note,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@@ -249,7 +249,7 @@ fun PollNote(
|
||||
pollViewModel.init(accountViewModel.account)
|
||||
pollViewModel.load(baseNote)
|
||||
|
||||
PollNote(
|
||||
ZapPollNote(
|
||||
baseNote = baseNote,
|
||||
pollViewModel = pollViewModel,
|
||||
canPreview = canPreview,
|
||||
@@ -260,7 +260,7 @@ fun PollNote(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollNote(
|
||||
fun ZapPollNote(
|
||||
baseNote: Note,
|
||||
pollViewModel: PollNoteViewModel,
|
||||
canPreview: Boolean,
|
||||
@@ -426,7 +426,7 @@ private fun RenderOptionAfterVote(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayProgress(
|
||||
private fun DisplayProgress(
|
||||
poolOption: PollOption,
|
||||
color: Color,
|
||||
modifier: Modifier,
|
||||
+3
-5
@@ -20,18 +20,16 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.invoice
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
|
||||
@Composable
|
||||
fun AddLnInvoiceButton(
|
||||
@@ -45,14 +43,14 @@ fun AddLnInvoiceButton(
|
||||
Icon(
|
||||
imageVector = Icons.Default.CurrencyBitcoin,
|
||||
contentDescription = stringRes(id = R.string.add_bitcoin_invoice),
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CurrencyBitcoin,
|
||||
contentDescription = stringRes(id = R.string.cancel_bitcoin_invoice),
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = BitcoinOrange,
|
||||
)
|
||||
}
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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.polls
|
||||
|
||||
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 java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PollDeadlinePicker(model: ShortNotePostViewModel) {
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
// Get current time details
|
||||
val currentTime = LocalDateTime.now()
|
||||
|
||||
val datePickerState =
|
||||
rememberDatePickerState(
|
||||
initialSelectedDateMillis =
|
||||
currentTime
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli(),
|
||||
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 = null)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
if (model.closedAt == null) {
|
||||
Text(stringRes(R.string.poll_closing_date_time), style = MaterialTheme.typography.bodyLarge)
|
||||
} else {
|
||||
Text(
|
||||
text = timeAheadNoDot(model.closedAt, 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 date =
|
||||
datePickerState.selectedDateMillis?.let {
|
||||
Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
} ?: LocalDate.now()
|
||||
|
||||
val finalDateTime =
|
||||
LocalDateTime.of(
|
||||
date,
|
||||
LocalTime.of(timePickerState.hour, timePickerState.minute),
|
||||
)
|
||||
|
||||
// 2. Convert to Unix Timestamp (Seconds)
|
||||
val unixTimestamp =
|
||||
finalDateTime
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.epochSecond
|
||||
|
||||
model.closedAt = unixTimestamp
|
||||
showTimePicker = false
|
||||
}) { Text(stringResource(R.string.confirm)) }
|
||||
},
|
||||
) {
|
||||
TimePicker(state = timePickerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ViewModelConstructorInComposable")
|
||||
@Preview
|
||||
@Composable
|
||||
fun PollDeadlinePickerPreview() {
|
||||
PollDeadlinePicker(
|
||||
ShortNotePostViewModel(),
|
||||
)
|
||||
}
|
||||
+128
@@ -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.polls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
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 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 com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
@Composable
|
||||
fun PollOptionsField(postViewModel: ShortNotePostViewModel) {
|
||||
val optionsList = postViewModel.pollOptions
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
optionsList.forEach { option ->
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.pollOptions[option.key]?.label ?: "",
|
||||
onValueChange = {
|
||||
postViewModel.updatePollOption(option.key, it)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringRes(R.string.poll_option_index, option.key + 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,
|
||||
),
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
postViewModel.removePollOption(option.key)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringRes(R.string.clear),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
PollDeadlinePicker(postViewModel)
|
||||
|
||||
Spacer(Modifier.height(2.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
postViewModel.pollOptions[postViewModel.pollOptions.size] = OptionTag(RandomInstance.randomChars(6), "")
|
||||
},
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ViewModelConstructorInComposable")
|
||||
@Preview
|
||||
@Composable
|
||||
fun NewPollClosingPreview() {
|
||||
PollOptionsField(
|
||||
ShortNotePostViewModel(),
|
||||
)
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
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.ui.actions.NewPollOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPollVoteValueRange
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun PollField(postViewModel: ShortNotePostViewModel) {
|
||||
val optionsList = postViewModel.pollOptions
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
optionsList.forEach { value ->
|
||||
NewPollOption(postViewModel, value.key)
|
||||
}
|
||||
|
||||
NewPollVoteValueRange(postViewModel)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
// postViewModel.pollOptions[postViewModel.pollOptions.size] = ""
|
||||
optionsList[optionsList.size] = ""
|
||||
},
|
||||
border =
|
||||
BorderStroke(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.placeholderText,
|
||||
),
|
||||
colors =
|
||||
ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.placeholderText,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterRes(resourceId = android.R.drawable.ic_input_add, 1),
|
||||
contentDescription = "Add poll option button",
|
||||
modifier = Size18Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,35 +20,83 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
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.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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollResponsesCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip88Polls.TallyResults
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.PollNote
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.collections.map
|
||||
|
||||
@Composable
|
||||
fun RenderPoll(
|
||||
@@ -56,81 +104,692 @@ fun RenderPoll(
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? PollNoteEvent ?: return
|
||||
val eventContent = noteEvent.content
|
||||
|
||||
val showReply by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
!makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo =
|
||||
remember(note) {
|
||||
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||
if (replyingTo != null) {
|
||||
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||
if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) {
|
||||
newNote
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
}
|
||||
if (replyingDirectlyTo != null) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
val noteEvent = note.event as? PollEvent ?: return
|
||||
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
text = noteEvent.content,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
InnerRenderPoll(noteEvent, note, makeItShort, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerRenderPoll(
|
||||
event: PollEvent,
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
Column(
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = event.content,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
RenderPollCard(
|
||||
event = event,
|
||||
pollState = note.pollState(),
|
||||
accountViewModel = accountViewModel,
|
||||
galleryUser = { user ->
|
||||
ClickableUserPicture(
|
||||
user,
|
||||
Size25dp,
|
||||
accountViewModel,
|
||||
onClick = {
|
||||
nav.nav { routeFor(user) }
|
||||
},
|
||||
)
|
||||
},
|
||||
) { code, label ->
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
content = label,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = Modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
id = note.idHex + code,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
PollNote(
|
||||
note,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav)
|
||||
if (event.hasHashtags()) {
|
||||
DisplayUncitedHashtags(event, event.content, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class PollCard(
|
||||
val options: List<PollItemCard>,
|
||||
val type: PollType,
|
||||
val endsAt: Long?,
|
||||
val isMyPoll: Boolean = false,
|
||||
val haveIVotedFlow: Flow<Boolean>,
|
||||
val haveIVoted: () -> Boolean,
|
||||
) {
|
||||
fun hasEnded() = endsAt != null && endsAt < TimeUtils.now()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class PollItemCard(
|
||||
val code: String,
|
||||
val label: String,
|
||||
val results: Flow<TallyResults>,
|
||||
val currentResults: () -> TallyResults,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RenderPollCard(
|
||||
event: PollEvent,
|
||||
pollState: PollResponsesCache,
|
||||
accountViewModel: AccountViewModel,
|
||||
galleryUser: @Composable RowScope.(user: User) -> Unit,
|
||||
labelContent: @Composable RowScope.(code: String, label: String) -> Unit,
|
||||
) {
|
||||
val card =
|
||||
remember(event) {
|
||||
PollCard(
|
||||
options =
|
||||
event.options().map { option ->
|
||||
PollItemCard(
|
||||
code = option.code,
|
||||
label = option.label,
|
||||
results =
|
||||
pollState.tallyFlow(
|
||||
option.code,
|
||||
accountViewModel.account.pubKey,
|
||||
accountViewModel.account.allFollows.flow
|
||||
.map { it.authors },
|
||||
),
|
||||
currentResults = {
|
||||
pollState.currentTally(
|
||||
option.code,
|
||||
accountViewModel.account.pubKey,
|
||||
accountViewModel.account.allFollows.flow.value.authors,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
type = event.pollType(),
|
||||
endsAt = event.endsAt(),
|
||||
isMyPoll = event.pubKey == accountViewModel.account.pubKey,
|
||||
haveIVotedFlow = pollState.hasPubKeyVotedFlow(accountViewModel.account.userProfile()),
|
||||
haveIVoted = {
|
||||
pollState.hasPubKeyVoted(accountViewModel.account.userProfile())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
RenderPollCard(
|
||||
card = card,
|
||||
onRespond = { responses ->
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.pollRespond(event, responses)
|
||||
}
|
||||
},
|
||||
resultContent = galleryUser,
|
||||
labelContent = labelContent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderPollCard(
|
||||
card: PollCard,
|
||||
onRespond: (Set<String>) -> Unit,
|
||||
resultContent: @Composable RowScope.(user: User) -> Unit,
|
||||
labelContent: @Composable RowScope.(code: String, label: String) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
if (card.isMyPoll) {
|
||||
RenderResults(card, resultContent, labelContent)
|
||||
} else {
|
||||
val haveIVoted = card.haveIVoted()
|
||||
if (haveIVoted) {
|
||||
RenderResults(card, resultContent, labelContent)
|
||||
} else {
|
||||
// waits for vote
|
||||
val haveIVoted by card.haveIVotedFlow.collectAsStateWithLifecycle(haveIVoted)
|
||||
if (haveIVoted) {
|
||||
RenderResults(card, resultContent, labelContent)
|
||||
} else if (card.hasEnded()) {
|
||||
RenderResults(card, resultContent, labelContent)
|
||||
} else {
|
||||
when (card.type) {
|
||||
PollType.SINGLE_CHOICE -> RenderSingleChoiceOptions(card, labelContent, onRespond)
|
||||
PollType.MULTI_CHOICE -> RenderMultiChoiceOptions(card, labelContent, onRespond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.RenderSingleChoiceOptions(
|
||||
card: PollCard,
|
||||
labelContent: @Composable (RowScope.(String, String) -> Unit),
|
||||
onRespond: (Set<String>) -> Unit,
|
||||
) {
|
||||
card.options.forEach {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(SmallishBorder)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
shape = SmallishBorder,
|
||||
).clickable {
|
||||
onRespond(setOf(it.code))
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = BigPadding,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
labelContent(it.code, it.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.RenderMultiChoiceOptions(
|
||||
card: PollCard,
|
||||
labelContent: @Composable (RowScope.(String, String) -> Unit),
|
||||
onRespond: (Set<String>) -> Unit,
|
||||
) {
|
||||
var multichoice by
|
||||
remember {
|
||||
mutableStateOf<Set<String>>(emptySet())
|
||||
}
|
||||
|
||||
card.options.forEach { option ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(SmallishBorder)
|
||||
.border(1.dp, MaterialTheme.colorScheme.grayText, SmallishBorder)
|
||||
.clickable {
|
||||
multichoice += option.code
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = option.code in multichoice,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
multichoice += option.code
|
||||
} else {
|
||||
multichoice -= option.code
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labelContent(option.code, option.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onRespond(multichoice)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = multichoice.isNotEmpty(),
|
||||
) {
|
||||
Text("Submit")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderResults(
|
||||
card: PollCard,
|
||||
resultContent: @Composable RowScope.(user: User) -> Unit,
|
||||
labelContent: @Composable (RowScope.(code: String, label: String) -> Unit),
|
||||
) {
|
||||
card.options.forEach { pollItem ->
|
||||
RenderClosedItem(pollItem, resultContent) {
|
||||
labelContent(pollItem.code, pollItem.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderClosedItem(
|
||||
item: PollItemCard,
|
||||
resultContent: @Composable RowScope.(user: User) -> Unit,
|
||||
labelContent: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val tally by item.results.collectAsStateWithLifecycle(item.currentResults())
|
||||
|
||||
RenderClosedItem(tally, resultContent, labelContent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderClosedItem(
|
||||
tally: TallyResults,
|
||||
resultContent: @Composable RowScope.(user: User) -> Unit,
|
||||
labelContent: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(SmallishBorder)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color =
|
||||
if (tally.isWinning) {
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colorScheme.grayText
|
||||
},
|
||||
shape = SmallishBorder,
|
||||
).background(
|
||||
if (tally.isWinning) {
|
||||
MaterialTheme.colorScheme.allGoodColor.copy(0.2f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.subtleBorder
|
||||
},
|
||||
),
|
||||
) {
|
||||
// Animate the progress bar when a vote is cast
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = tally.percent,
|
||||
animationSpec = tween(durationMillis = 800),
|
||||
)
|
||||
|
||||
val progressBarColor = if (tally.isWinning) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.primary
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.alpha(0.32f)
|
||||
.drawWithContent {
|
||||
// Clip the drawing area to show only the progress amount
|
||||
clipRect(right = size.width * animatedProgress) {
|
||||
drawRect(progressBarColor)
|
||||
}
|
||||
drawContent()
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(15.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
labelContent()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = SpacedBy10dp,
|
||||
) {
|
||||
UserGallery(tally, resultContent)
|
||||
|
||||
Text(
|
||||
text = "${(tally.percent * 100).toInt()}%",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserGallery(
|
||||
tally: TallyResults,
|
||||
galleryUser: @Composable RowScope.(user: User) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy((-10).dp),
|
||||
) {
|
||||
tally.users.take(6).forEach {
|
||||
key(it.pubkeyHex) {
|
||||
galleryUser(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (tally.users.size > 6) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size25dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
) {
|
||||
Text(
|
||||
text = "+" + showCount(tally.users.size - 6),
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderPollManualPreview() {
|
||||
val poll =
|
||||
PollCard(
|
||||
options =
|
||||
listOf(
|
||||
PollItemCard(
|
||||
code = "1",
|
||||
label = "Yes",
|
||||
results = flow {},
|
||||
currentResults = {
|
||||
TallyResults(
|
||||
percent = 0.9f,
|
||||
isWinning = true,
|
||||
)
|
||||
},
|
||||
),
|
||||
PollItemCard(
|
||||
code = "2",
|
||||
label = "No",
|
||||
results = flow {},
|
||||
currentResults = {
|
||||
TallyResults(
|
||||
percent = 0.1f,
|
||||
isWinning = false,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
type = PollType.SINGLE_CHOICE,
|
||||
endsAt = null,
|
||||
isMyPoll = true,
|
||||
haveIVotedFlow = flow {},
|
||||
haveIVoted = { true },
|
||||
)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
RenderPollCard(poll, {}, {}) { _, label ->
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderPollGameResultsPreview() {
|
||||
val event =
|
||||
PollEvent(
|
||||
id = "1fae24bb7e1673afc94da3878d051c0f4fd65599960aeb8a9a90642f8cad6299",
|
||||
pubKey = "c21b1a6cdb247ccbd938dcb16b15a4fa382d00ffd7b12d5cbbad172a0cd4d170",
|
||||
createdAt = 1770690664,
|
||||
content = "If there cpuld only be one: ",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("option", "dzv1u5do4", "GTA V"),
|
||||
arrayOf("option", "8mr177irx", "Cyberpunk 2077"),
|
||||
arrayOf("relay", "wss://relay.nostr.band/"),
|
||||
arrayOf("relay", "wss://relay.primal.net/"),
|
||||
arrayOf("relay", "wss://nos.lol/"),
|
||||
arrayOf("relay", "wss://relay.damus.io/"),
|
||||
arrayOf("relay", "wss://wot.nostr.party/"),
|
||||
arrayOf("relay", "wss://relay.mostr.pub/"),
|
||||
arrayOf("relay", "wss://offchain.pub/"),
|
||||
arrayOf("polltype", "singlechoice"),
|
||||
),
|
||||
sig = "7263d7e98348474c9f6670a98225e3594bffa813ff9c9084b7045126562dc352744450b5e667b0069e0276a63d3b9e14df3b1e1a3e6d5e958c3a7c74390345f5",
|
||||
)
|
||||
|
||||
val note = LocalCache.getOrCreateNote("1fae24bb7e1673afc94da3878d051c0f4fd65599960aeb8a9a90642f8cad6299")
|
||||
|
||||
LocalCache.justConsume(event, null, true)
|
||||
|
||||
val response1 =
|
||||
PollResponseEvent(
|
||||
"a503bb8b1bd9062fab4acd003f015791faa04a9f321b91fd6d4649315e2a9307",
|
||||
pubKey = "592295cf2b09a7f9555f43adb734cbee8a84ee892ed3f9336e6a09b6413a0db9",
|
||||
createdAt = 1770729213,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "1fae24bb7e1673afc94da3878d051c0f4fd65599960aeb8a9a90642f8cad6299"),
|
||||
arrayOf("response", "dzv1u5do4"),
|
||||
),
|
||||
content = "",
|
||||
sig = "0affc0a4f39bbae0802628282af871bbdab8d0338956ebd51cbb5453eac8f66f56013af35dafbe847762f6cfc6f9746be59260d2c462602501dad94d2fb67b63",
|
||||
)
|
||||
|
||||
val response2 =
|
||||
PollResponseEvent(
|
||||
"1ad46eabd0044911e2c65ace22d4ad19640a74dc2b3daeea7bc2862fb0dcf5fb",
|
||||
pubKey = "c21b1a6cdb247ccbd938dcb16b15a4fa382d00ffd7b12d5cbbad172a0cd4d170",
|
||||
createdAt = 1770729213,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "1fae24bb7e1673afc94da3878d051c0f4fd65599960aeb8a9a90642f8cad6299"),
|
||||
arrayOf("response", "8mr177irx"),
|
||||
),
|
||||
content = "",
|
||||
sig = "3efa83a899e1038fde76b8130f99881ab88c330adcd7d9f2578fa3c3e346eba05d627bd75ddb5780ffd460fcefb07839d0b379ba20db30772fa4d847b1dcfa6f",
|
||||
)
|
||||
|
||||
LocalCache.justConsume(response1, null, true)
|
||||
LocalCache.justConsume(response2, null, true)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
RenderPoll(
|
||||
note,
|
||||
false,
|
||||
true,
|
||||
2,
|
||||
remember { mutableStateOf(Color.Transparent) },
|
||||
mockAccountViewModel(),
|
||||
EmptyNav(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderPollColorResultsPreview() {
|
||||
val event =
|
||||
PollEvent(
|
||||
id = "64d4acac073860d9080a51988bb1e9ffaef4ae8f3f9cf2d7b510e10d16a33b91",
|
||||
pubKey = "feb88e80a63d6a25b4b56eed4fac67342a97f2206e0a04fad89eb24d55ea303f",
|
||||
createdAt = 1769822711,
|
||||
content = "Color?",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("option", "hrwgtnzue", "Blue"),
|
||||
arrayOf("option", "1cx1gldth", "Red"),
|
||||
arrayOf("option", "5e29bzfjf", "Green"),
|
||||
arrayOf("polltype", "singlechoice"),
|
||||
arrayOf("endsAt", "1769844311"),
|
||||
arrayOf("relay", "wss://relay.ditto.pub"),
|
||||
arrayOf("relay", "wss://relay.primal.net"),
|
||||
arrayOf("relay", "wss://relay.damus.io"),
|
||||
arrayOf("I", "iso3166:BR"),
|
||||
arrayOf("K", "iso3166"),
|
||||
arrayOf("i", "iso3166:BR"),
|
||||
arrayOf("k", "iso3166"),
|
||||
),
|
||||
sig = "d65af73c3b652ea06df3dc00b05a6f155cf8936d645ca002d2c1a5d59b45e9b072e5217d3339711ad62f5b105859d8da95eb636681bd12ad04063d19359f48b5",
|
||||
)
|
||||
|
||||
val note = LocalCache.getOrCreateNote(event)
|
||||
|
||||
LocalCache.justConsume(event, null, true)
|
||||
|
||||
val response1 =
|
||||
PollResponseEvent(
|
||||
id = "b756c6b3112d088355f7e3d288f5ccc0633522d86f770e1bdba67fae1b082719",
|
||||
pubKey = "3ff64bd7dde76af783a9545e2fb3ce84d921be6c8c04f3dc6c4fa734cf7eb812",
|
||||
createdAt = 1769841653,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "64d4acac073860d9080a51988bb1e9ffaef4ae8f3f9cf2d7b510e10d16a33b91"),
|
||||
arrayOf("response", "hrwgtnzue"),
|
||||
arrayOf("client", "www.pollstr.site"),
|
||||
),
|
||||
content = "",
|
||||
sig = "3a8cc549a5736774568e4b9a32e689ad5882fe2d030c4e05ecf78c6b1cfe5b5c9d3383067f525dfbddae600dc18b1266526e6d5d4e1f43e5723ebc7607b0377d",
|
||||
)
|
||||
|
||||
val response2 =
|
||||
PollResponseEvent(
|
||||
id = "6aecc88b2acc8145c68553040da333d62085a6f75b874b1b7fceab2c8158069e",
|
||||
pubKey = "c21b1a6cdb247ccbd938dcb16b15a4fa382d00ffd7b12d5cbbad172a0cd4d170",
|
||||
createdAt = 1769827608,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "64d4acac073860d9080a51988bb1e9ffaef4ae8f3f9cf2d7b510e10d16a33b91"),
|
||||
arrayOf("response", "5e29bzfjf"),
|
||||
),
|
||||
content = "",
|
||||
sig = "1ab865185fa7328d8b64f5c676a370d2dc4896678d6b13786ff142cfe8d7dc635f523740fad25a944de1b01def179548843a893a088f177dd011674a746d02a4",
|
||||
)
|
||||
|
||||
val response3 =
|
||||
PollResponseEvent(
|
||||
id = "53a5ac0972b08276a0ea34a7dcd1d796b14453f4a76ca4626ed42ad19d67ade9",
|
||||
pubKey = "feb88e80a63d6a25b4b56eed4fac67342a97f2206e0a04fad89eb24d55ea303f",
|
||||
createdAt = 1769822723,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "64d4acac073860d9080a51988bb1e9ffaef4ae8f3f9cf2d7b510e10d16a33b91"),
|
||||
arrayOf("response", "5e29bzfjf"),
|
||||
),
|
||||
content = "",
|
||||
sig = "d330c6f10d26674785d7e312929f4b4e01272840c087de45dceb0389ce14368908011b621dad24a9e438c1d757f9eac9940b6b9005721ff995c50258d1a7f6fb",
|
||||
)
|
||||
|
||||
val response4 =
|
||||
PollResponseEvent(
|
||||
id = "5c6670f736f166dad6f1784ebe72bb851a9d82a9b8bb1355d8d74c2c7f1869fd",
|
||||
pubKey = "feb88e80a63d6a25b4b56eed4fac67342a97f2206e0a04fad89eb24d55ea303f",
|
||||
createdAt = 1769822720,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("e", "64d4acac073860d9080a51988bb1e9ffaef4ae8f3f9cf2d7b510e10d16a33b91"),
|
||||
arrayOf("response", "hrwgtnzue"),
|
||||
),
|
||||
content = "",
|
||||
sig = "d2feee481c0e84306fcb0815c7c671ab697948148b18307f864fe6c96a09485c12bf8f843d406b231914d60bd2735d3f3ed0b4e89377794bccd66e2e0acf2eb5",
|
||||
)
|
||||
|
||||
LocalCache.justConsume(response1, null, true)
|
||||
LocalCache.justConsume(response2, null, true)
|
||||
LocalCache.justConsume(response3, null, true)
|
||||
LocalCache.justConsume(response4, null, true)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
RenderPoll(
|
||||
note,
|
||||
false,
|
||||
true,
|
||||
2,
|
||||
remember { mutableStateOf(Color.Transparent) },
|
||||
mockAccountViewModel(),
|
||||
EmptyNav(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderPollMultiChoicePreview() {
|
||||
val event =
|
||||
PollEvent(
|
||||
id = "ab1cad8d8f24aaa53b8cf34dd06ddedda5961c57da7a0df91349901fc15b5042",
|
||||
pubKey = "baeb862f3318390ec5af5c9db64ae5ddb2efc1a97db54c6550656bfa2dcc054b",
|
||||
createdAt = 1769053977,
|
||||
content = "Have you seen the simply Nostr/greeny show?",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("option", "py4d986jp", "Yes"),
|
||||
arrayOf("option", "z0tipo9as", "No "),
|
||||
arrayOf("option", "59ya8j3jo", "GFY"),
|
||||
arrayOf("relay", "wss://nos.lol"),
|
||||
arrayOf("relay", "wss://student.chadpolytechnic.com"),
|
||||
arrayOf("relay", "wss://nostr-relay.wlvs.space"),
|
||||
arrayOf("relay", "wss://nostr.ono.re"),
|
||||
arrayOf("relay", "wss://nostr.bongbong.com"),
|
||||
arrayOf("polltype", "multiplechoice"),
|
||||
arrayOf("client", "www.pollstr.site"),
|
||||
),
|
||||
sig = "aaddf12bc4b7947d8c57dd0fb4378c5f0daab6844cc3205bf1480e2a10e077b1abca67d071530e9fecac517fd6d052e49630a32596d9ecfb862967d0b2b2d899",
|
||||
)
|
||||
|
||||
val note = LocalCache.getOrCreateNote(event)
|
||||
|
||||
LocalCache.justConsume(event, null, true)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
RenderPoll(
|
||||
note,
|
||||
makeItShort = false,
|
||||
canPreview = true,
|
||||
quotesLeft = 2,
|
||||
backgroundColor = remember { mutableStateOf(Color.Transparent) },
|
||||
accountViewModel = mockAccountViewModel(),
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapPollNote
|
||||
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.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
|
||||
@Composable
|
||||
fun RenderZapPoll(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? PollNoteEvent ?: return
|
||||
val eventContent = noteEvent.content
|
||||
|
||||
val showReply by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
!makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo =
|
||||
remember(note) {
|
||||
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||
if (replyingTo != null) {
|
||||
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||
if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) {
|
||||
newNote
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
}
|
||||
if (replyingDirectlyTo != null) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
ZapPollNote(
|
||||
note,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -386,6 +387,7 @@ val DEFAULT_FEED_KINDS =
|
||||
ClassifiedsEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
@@ -406,6 +408,7 @@ val DEFAULT_COMMUNITY_FEEDS =
|
||||
HighlightEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
|
||||
+2
-2
@@ -29,12 +29,12 @@ 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
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
|
||||
val PostsByGeohashKinds =
|
||||
@@ -42,13 +42,13 @@ val PostsByGeohashKinds =
|
||||
TextNoteEvent.KIND,
|
||||
ChannelMessageEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterPostsByGeohash(
|
||||
|
||||
+3
-1
@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
|
||||
val PostsByHashtagsKinds =
|
||||
@@ -46,7 +47,7 @@ val PostsByHashtagsKinds =
|
||||
TextNoteEvent.KIND,
|
||||
ChannelMessageEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
@@ -60,6 +61,7 @@ val PostsByHashtagKinds2 =
|
||||
AudioTrackEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterPostsByHashtags(
|
||||
|
||||
+23
-9
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
@@ -37,6 +38,8 @@ import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Poll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -52,6 +55,7 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -83,25 +87,24 @@ import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.notify.Notifying
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||
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.PollField
|
||||
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.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.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -289,7 +292,7 @@ private fun NewPostScreenBody(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
PollField(postViewModel)
|
||||
PollOptionsField(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,6 +545,17 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ViewModelConstructorInComposable")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BottomRowActionsPreview() {
|
||||
val model = ShortNotePostViewModel()
|
||||
model.canUsePoll = true
|
||||
ThemeComparisonColumn {
|
||||
BottomRowActions(model)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddPollButton(
|
||||
isPollActive: Boolean,
|
||||
@@ -552,17 +566,17 @@ private fun AddPollButton(
|
||||
) {
|
||||
if (!isPollActive) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_poll, 1),
|
||||
imageVector = Icons.Outlined.Poll,
|
||||
contentDescription = stringRes(id = R.string.poll),
|
||||
modifier = Size20Modifier,
|
||||
modifier = Modifier.height(22.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_lists, 1),
|
||||
imageVector = Icons.Outlined.Poll,
|
||||
contentDescription = stringRes(id = R.string.disable_poll),
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.height(22.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+96
-80
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -70,15 +69,8 @@ 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.PollNoteEvent
|
||||
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.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
@@ -112,6 +104,8 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
||||
@@ -128,6 +122,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -224,17 +219,8 @@ open class ShortNotePostViewModel :
|
||||
// Polls
|
||||
var canUsePoll by mutableStateOf(false)
|
||||
var wantsPoll by mutableStateOf(false)
|
||||
var zapRecipients = mutableStateListOf<HexKey>()
|
||||
var pollOptions = newStateMapPollOptions()
|
||||
var valueMaximum by mutableStateOf<Long?>(null)
|
||||
var valueMinimum by mutableStateOf<Long?>(null)
|
||||
var consensusThreshold: Int? = null
|
||||
var closedAt: Long? = null
|
||||
|
||||
var isValidValueMaximum = mutableStateOf(true)
|
||||
var isValidValueMinimum = mutableStateOf(true)
|
||||
var isValidConsensusThreshold = mutableStateOf(true)
|
||||
var isValidClosedAt = mutableStateOf(true)
|
||||
var pollOptions: SnapshotStateMap<Int, OptionTag> = newStateMapPollOptions()
|
||||
var closedAt by mutableStateOf<Long?>(null)
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
@@ -411,9 +397,13 @@ open class ShortNotePostViewModel :
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
if (draftEvent !is TextNoteEvent) return
|
||||
if (draftEvent is TextNoteEvent) {
|
||||
loadFromDraft(draftEvent)
|
||||
}
|
||||
|
||||
loadFromDraft(draftEvent)
|
||||
if (draftEvent is PollEvent) {
|
||||
loadFromDraft(draftEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: TextNoteEvent) {
|
||||
@@ -486,20 +476,71 @@ open class ShortNotePostViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
val polls = draftEvent.tags.filter { it.size > 1 && it[0] == "poll_option" }
|
||||
wantsPoll = polls.isNotEmpty()
|
||||
wantsPoll = false
|
||||
|
||||
polls.forEach {
|
||||
pollOptions[it[1].toInt()] = it[2]
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: PollEvent) {
|
||||
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()
|
||||
|
||||
val geohash = draftEvent.getGeoHash()
|
||||
wantsToAddGeoHash = geohash != null
|
||||
if (geohash != null) {
|
||||
wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND
|
||||
}
|
||||
|
||||
val minMax = draftEvent.tags.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") }
|
||||
minMax.forEach {
|
||||
if (it[0] == "value_maximum") {
|
||||
valueMaximum = it[1].toLong()
|
||||
} else if (it[0] == "value_minimum") {
|
||||
valueMinimum = it[1].toLong()
|
||||
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
|
||||
|
||||
if (forwardZapTo.value.items.isNotEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
val polls = draftEvent.options()
|
||||
wantsPoll = polls.isNotEmpty()
|
||||
|
||||
polls.forEachIndexed { index, tag ->
|
||||
pollOptions[index] = tag
|
||||
}
|
||||
|
||||
draftEvent.endsAt()?.let {
|
||||
closedAt = it
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
@@ -648,18 +689,16 @@ open class ShortNotePostViewModel :
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
||||
|
||||
return if (wantsPoll) {
|
||||
val options = pollOptions.map { PollOptionTag(it.key, it.value) }
|
||||
val options = pollOptions.map { it.value }
|
||||
|
||||
if (options.isEmpty()) return null
|
||||
|
||||
val quotes = findNostrUris(tagger.message)
|
||||
val relays =
|
||||
accountViewModel.account.nip65RelayList.outboxFlow.value
|
||||
.toList()
|
||||
|
||||
PollNoteEvent.build(tagger.message, options) {
|
||||
valueMinimum?.let { minAmount(it) }
|
||||
valueMaximum?.let { maxAmount(it) }
|
||||
closedAt?.let { closedAt(it) }
|
||||
consensusThreshold?.let { consensusThreshold(it / 100.0) }
|
||||
|
||||
PollEvent.build(tagger.message, options, closedAt, relays) {
|
||||
pTags(tagger.directMentionsUsers.map { it.toPTag() })
|
||||
quotes(quotes)
|
||||
hashtags(findHashtags(tagger.message))
|
||||
@@ -849,11 +888,7 @@ open class ShortNotePostViewModel :
|
||||
pTags = null
|
||||
|
||||
wantsPoll = false
|
||||
zapRecipients = mutableStateListOf()
|
||||
pollOptions = newStateMapPollOptions()
|
||||
valueMaximum = null
|
||||
valueMinimum = null
|
||||
consensusThreshold = null
|
||||
closedAt = null
|
||||
|
||||
wantsInvoice = false
|
||||
@@ -962,7 +997,11 @@ open class ShortNotePostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> = mutableStateMapOf(Pair(0, ""), Pair(1, ""))
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, OptionTag> =
|
||||
mutableStateMapOf(
|
||||
0 to OptionTag(RandomInstance.randomChars(6), ""),
|
||||
1 to OptionTag(RandomInstance.randomChars(6), ""),
|
||||
)
|
||||
|
||||
fun canPost(): Boolean {
|
||||
// Voice messages can be posted without text (with either uploaded or pending recording)
|
||||
@@ -979,9 +1018,7 @@ open class ShortNotePostViewModel :
|
||||
(
|
||||
!wantsPoll ||
|
||||
(
|
||||
pollOptions.values.all { it.isNotEmpty() } &&
|
||||
isValidValueMinimum.value &&
|
||||
isValidValueMaximum.value
|
||||
pollOptions.isNotEmpty() && pollOptions.all { it.value.label.isNotEmpty() }
|
||||
)
|
||||
) &&
|
||||
multiOrchestrator == null
|
||||
@@ -1131,28 +1168,6 @@ open class ShortNotePostViewModel :
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
fun updateMinZapAmountForPoll(textMin: String) {
|
||||
valueMinimum = textMin.toLongOrNull()?.takeIf { it > 0 }
|
||||
checkMinMax()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateMaxZapAmountForPoll(textMax: String) {
|
||||
valueMaximum = textMax.toLongOrNull()?.takeIf { it > 0 }
|
||||
checkMinMax()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun checkMinMax() {
|
||||
if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) {
|
||||
isValidValueMinimum.value = false
|
||||
isValidValueMaximum.value = false
|
||||
} else {
|
||||
isValidValueMinimum.value = true
|
||||
isValidValueMaximum.value = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateZapPercentage(
|
||||
index: Int,
|
||||
sliderValue: Float,
|
||||
@@ -1178,19 +1193,28 @@ open class ShortNotePostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun removePollOption(optionIndex: Int) {
|
||||
pollOptions.removeOrdered(optionIndex)
|
||||
fun removePollOption(index: Int) {
|
||||
pollOptions.removeOrdered(index)
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
private fun MutableMap<Int, String>.removeOrdered(index: Int) {
|
||||
fun updatePollOption(
|
||||
index: Int,
|
||||
label: String,
|
||||
) {
|
||||
val current = pollOptions[index]
|
||||
pollOptions[index] = OptionTag(current?.code ?: RandomInstance.randomChars(6), label)
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
private fun MutableMap<Int, OptionTag>.removeOrdered(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[i] = elementList[nextIndex].also { elementList[nextIndex] = OptionTag(RandomInstance.randomChars(6), "") }
|
||||
}
|
||||
}
|
||||
elementList.removeAt(elementList.size - 1)
|
||||
@@ -1199,14 +1223,6 @@ open class ShortNotePostViewModel :
|
||||
this.putAll(newEntries)
|
||||
}
|
||||
|
||||
fun updatePollOption(
|
||||
optionIndex: Int,
|
||||
text: String,
|
||||
) {
|
||||
pollOptions[optionIndex] = text
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun toggleMarkAsSensitive() {
|
||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
||||
draftTag.newVersion()
|
||||
|
||||
+2
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
|
||||
class HomeConversationsFeedFilter(
|
||||
@@ -81,6 +82,7 @@ class HomeConversationsFeedFilter(
|
||||
(
|
||||
event is TextNoteEvent ||
|
||||
event is PollNoteEvent ||
|
||||
event is PollResponseEvent ||
|
||||
event is ChannelMessageEvent ||
|
||||
event is CommentEvent ||
|
||||
event is VoiceReplyEvent ||
|
||||
|
||||
+2
@@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
|
||||
@@ -111,6 +112,7 @@ class HomeNewThreadFeedFilter(
|
||||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
noteEvent is PollNoteEvent ||
|
||||
noteEvent is PollEvent ||
|
||||
noteEvent is HighlightEvent ||
|
||||
noteEvent is InteractiveStoryPrologueEvent ||
|
||||
noteEvent is CommentEvent ||
|
||||
|
||||
+4
@@ -40,6 +40,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
@@ -56,6 +58,7 @@ val HomePostsNewThreadKinds =
|
||||
WikiNoteEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
)
|
||||
|
||||
@@ -67,6 +70,7 @@ val HomePostsConversationKinds =
|
||||
EphemeralChatEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
PollResponseEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterNewHomePostsByAuthors(
|
||||
|
||||
+3
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
|
||||
val HomePostsFromCommunityKinds =
|
||||
@@ -41,6 +42,7 @@ val HomePostsFromCommunityKinds =
|
||||
LongTextNoteEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
@@ -54,6 +56,7 @@ val HomePostsFromCommunityKindsStr =
|
||||
LongTextNoteEvent.KIND.toString(),
|
||||
ClassifiedsEvent.KIND.toString(),
|
||||
HighlightEvent.KIND.toString(),
|
||||
PollEvent.KIND.toString(),
|
||||
WikiNoteEvent.KIND.toString(),
|
||||
NipTextEvent.KIND.toString(),
|
||||
CommunityPostApprovalEvent.KIND.toString(),
|
||||
|
||||
+2
@@ -66,6 +66,7 @@ import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
@@ -105,6 +106,7 @@ class NotificationFeedFilter(
|
||||
LnZapEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
PictureEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PrivateDmEvent.KIND,
|
||||
PublicMessageEvent.KIND,
|
||||
|
||||
+2
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
|
||||
class UserProfileConversationsFeedFilter(
|
||||
@@ -64,6 +65,7 @@ class UserProfileConversationsFeedFilter(
|
||||
(
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is PollResponseEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is LiveActivitiesChatMessageEvent ||
|
||||
it.event is CommentEvent ||
|
||||
|
||||
+3
-1
@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
|
||||
@@ -48,7 +49,7 @@ val UserProfilePostKinds1 =
|
||||
GenericRepostEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
@@ -63,6 +64,7 @@ val UserProfilePostKinds2 =
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
)
|
||||
|
||||
|
||||
+2
@@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
|
||||
@@ -80,6 +81,7 @@ class UserProfileNewThreadFeedFilter(
|
||||
it.event is WikiNoteEvent ||
|
||||
it.event is NipTextEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is PollEvent ||
|
||||
it.event is HighlightEvent ||
|
||||
it.event is InteractiveStoryPrologueEvent ||
|
||||
it.event is AudioTrackEvent ||
|
||||
|
||||
+13
-1
@@ -156,6 +156,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VoiceHeader
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
@@ -233,6 +234,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
@@ -668,7 +670,7 @@ private fun FullBleedNoteCompose(
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is PollNoteEvent) {
|
||||
RenderPoll(
|
||||
RenderZapPoll(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
@@ -678,6 +680,16 @@ private fun FullBleedNoteCompose(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is PollEvent) {
|
||||
RenderPoll(
|
||||
note = baseNote,
|
||||
makeItShort = false,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 3,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (noteEvent is PrivateDmEvent) {
|
||||
RenderPrivateMessage(
|
||||
baseNote,
|
||||
|
||||
@@ -460,6 +460,7 @@
|
||||
<string name="poll_heading_required">Required fields:</string>
|
||||
<string name="poll_zap_recipients">Zap recipients</string>
|
||||
<string name="poll_primary_description">Primary poll description…</string>
|
||||
<string name="poll_option">Option</string>
|
||||
<string name="poll_option_index">Option %s</string>
|
||||
<string name="poll_option_description">Poll option description</string>
|
||||
<string name="poll_heading_optional">Optional fields:</string>
|
||||
@@ -467,6 +468,7 @@
|
||||
<string name="poll_zap_value_max">Zap maximum</string>
|
||||
<string name="poll_consensus_threshold">Consensus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_date_time">Poll Closing Date & Time</string>
|
||||
<string name="poll_closing_time">Close after</string>
|
||||
<string name="poll_closing_time_days">days</string>
|
||||
<string name="poll_unable_to_vote">Unable to vote</string>
|
||||
@@ -1504,4 +1506,8 @@
|
||||
<string name="reaction">Reaction</string>
|
||||
<string name="voice_post">Voice Post</string>
|
||||
<string name="voice_reply">Voice Reply</string>
|
||||
<string name="closing_time">Closing Time</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="add_poll_option_button">Add poll option button</string>
|
||||
</resources>
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip88Polls.PollResponsesCache
|
||||
import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.commons.util.replace
|
||||
@@ -131,6 +132,12 @@ open class Note(
|
||||
removeReport(note)
|
||||
}
|
||||
|
||||
var poll: PollResponsesCache? = null
|
||||
|
||||
fun pollStateOrNull(): PollResponsesCache? = poll
|
||||
|
||||
fun pollState(): PollResponsesCache = poll ?: PollResponsesCache().also { poll = it }
|
||||
|
||||
// These fields are updated every time an event related to this note is received.
|
||||
var replies = listOf<Note>()
|
||||
private set
|
||||
@@ -1007,6 +1014,34 @@ public inline fun <T> Iterable<Note>.anyEvent(predicate: (T) -> Boolean): Boolea
|
||||
return false
|
||||
}
|
||||
|
||||
public inline fun <T> Iterable<Note>.filterEvents(predicate: (T) -> Boolean): List<T> {
|
||||
if (this is Collection && isEmpty()) return emptyList()
|
||||
|
||||
val dest = ArrayList<T>()
|
||||
for (note in this) {
|
||||
val noteEvent = note.event as? T
|
||||
if (noteEvent != null && predicate(noteEvent)) {
|
||||
dest.add(noteEvent)
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
public inline fun <T> Iterable<Note>.filterAuthoredEvents(pubkey: HexKey): List<T> {
|
||||
if (this is Collection && isEmpty()) return emptyList()
|
||||
|
||||
val dest = ArrayList<T>()
|
||||
for (note in this) {
|
||||
if (note.author?.pubkeyHex != pubkey) {
|
||||
val noteEvent = note.event as? T
|
||||
if (noteEvent != null) {
|
||||
dest.add(noteEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
public inline fun Iterable<Note>.anyNotNullEvent(predicate: (Event) -> Boolean): Boolean {
|
||||
if (this is Collection && isEmpty()) return false
|
||||
for (note in this) {
|
||||
@@ -1015,3 +1050,21 @@ public inline fun Iterable<Note>.anyNotNullEvent(predicate: (Event) -> Boolean):
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun <T : Event> List<Note>.latestByAuthor(): Map<User, T> {
|
||||
val oneResponsePerUser = mutableMapOf<User, T>()
|
||||
|
||||
forEach { note ->
|
||||
val author = note.author ?: return@forEach
|
||||
val event = note.event as? T ?: return@forEach
|
||||
|
||||
val currentResponse = oneResponsePerUser[author]
|
||||
if (currentResponse == null) {
|
||||
oneResponsePerUser[author] = event
|
||||
} else if (event.createdAt > currentResponse.createdAt) {
|
||||
oneResponsePerUser[author] = event
|
||||
}
|
||||
}
|
||||
|
||||
return oneResponsePerUser
|
||||
}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.commons.model.nip88Polls
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.UserDependencies
|
||||
import com.vitorpamplona.amethyst.commons.model.latestByAuthor
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
@Stable
|
||||
class PollResponsesCache : UserDependencies {
|
||||
companion object {
|
||||
val DefaultFeedOrder: Comparator<PollResponseEvent> =
|
||||
compareByDescending<PollResponseEvent> { it.createdAt }.thenBy { it.id }
|
||||
}
|
||||
|
||||
class ResponseTally(
|
||||
val allResponses: List<Note> = emptyList(),
|
||||
) {
|
||||
val votes: Map<User, PollResponseEvent> = allResponses.latestByAuthor()
|
||||
val tally: Map<String, Set<User>> = votes.votesByOption()
|
||||
|
||||
fun winning() = tally.maxByOrNull { it.value.size }?.key
|
||||
|
||||
fun totalVotes() = tally.entries.sumOf { it.value.size }
|
||||
}
|
||||
|
||||
val responses = MutableStateFlow(ResponseTally())
|
||||
|
||||
fun addResponse(note: Note) {
|
||||
// if it's already there, quick exit
|
||||
if (responses.value.allResponses.contains(note)) return
|
||||
|
||||
responses.update {
|
||||
ResponseTally(
|
||||
it.allResponses + note,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeResponse(deleteNote: Note) {
|
||||
// if it's not already there, quick exit
|
||||
if (!responses.value.allResponses.contains(deleteNote)) return
|
||||
|
||||
responses.update {
|
||||
ResponseTally(
|
||||
it.allResponses - deleteNote,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ResponseTally.filterTo(
|
||||
code: String,
|
||||
forKey: HexKey,
|
||||
priority: Set<HexKey>,
|
||||
): TallyResults {
|
||||
val comparator = compareByDescending<User> { it.pubkeyHex == forKey }.thenByDescending { it.pubkeyHex in priority }.thenBy { it.pubkeyHex }
|
||||
|
||||
val usersThatVotedForThisOption = tally[code] ?: emptyList()
|
||||
|
||||
val votes = totalVotes()
|
||||
|
||||
val percent =
|
||||
if (votes > 0) {
|
||||
usersThatVotedForThisOption.size.toFloat() / votes.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
val sortedUsers = usersThatVotedForThisOption.sortedWith(comparator)
|
||||
|
||||
return TallyResults(sortedUsers, percent, code == winning())
|
||||
}
|
||||
|
||||
fun currentTally(
|
||||
code: String,
|
||||
forKey: HexKey,
|
||||
priorityAccounts: Set<HexKey>,
|
||||
): TallyResults = responses.value.filterTo(code, forKey, priorityAccounts)
|
||||
|
||||
fun tallyFlow(
|
||||
code: String,
|
||||
forKey: HexKey,
|
||||
priorityAccounts: Flow<Set<HexKey>>,
|
||||
): Flow<TallyResults> =
|
||||
combine(responses, priorityAccounts) { responses, priority ->
|
||||
responses.filterTo(code, forKey, priority)
|
||||
}
|
||||
|
||||
fun hasPubKeyVoted(user: User): Boolean = responses.value.votes.containsKey(user)
|
||||
|
||||
fun hasPubKeyVotedFlow(user: User): Flow<Boolean> = responses.map { hasPubKeyVoted(user) }.distinctUntilChanged()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class TallyResults(
|
||||
val users: List<User> = emptyList(),
|
||||
val percent: Float = 0.0f,
|
||||
val isWinning: Boolean = false,
|
||||
)
|
||||
|
||||
fun Map<User, PollResponseEvent>.votesByOption(): Map<String, Set<User>> {
|
||||
val tally = mutableMapOf<String, MutableSet<User>>()
|
||||
|
||||
this.forEach { (user, responseEvent) ->
|
||||
responseEvent.responses().forEach { code ->
|
||||
val currentTally = tally[code]
|
||||
if (currentTally == null) {
|
||||
tally[code] = mutableSetOf(user)
|
||||
} else {
|
||||
currentTally.add(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tally
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip22Comments.RootScope
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class PollEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
RootScope {
|
||||
fun options() = tags.options()
|
||||
|
||||
fun relays() = tags.relays()
|
||||
|
||||
fun pollType() = tags.pollType()
|
||||
|
||||
fun endsAt() = tags.endsAt()
|
||||
|
||||
fun hasEnded() = tags.hasEnded()
|
||||
|
||||
companion object {
|
||||
const val KIND = 1068
|
||||
const val ALT_DESCRIPTION = "Poll"
|
||||
|
||||
fun build(
|
||||
description: String,
|
||||
options: List<OptionTag>,
|
||||
endsAt: Long?,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
pollType: PollType = PollType.SINGLE_CHOICE,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<PollEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, description, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
poolType(pollType)
|
||||
options(options)
|
||||
relays(relays)
|
||||
endsAt?.let {
|
||||
endsAt(it)
|
||||
}
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.EndsAtTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollTypeTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.RelayTag
|
||||
|
||||
fun TagArrayBuilder<PollEvent>.poolType(type: PollType) = addUnique(PollTypeTag.assemble(type))
|
||||
|
||||
fun TagArrayBuilder<PollEvent>.endsAt(timestamp: Long) = addUnique(EndsAtTag.assemble(timestamp))
|
||||
|
||||
fun TagArrayBuilder<PollEvent>.relays(relays: List<NormalizedRelayUrl>) = addAll(RelayTag.assemble(relays))
|
||||
|
||||
fun TagArrayBuilder<PollEvent>.options(options: List<OptionTag>) = addAll(OptionTag.assemble(options))
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.EndsAtTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.EndsAtTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollTypeTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollTypeTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.RelayTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
fun TagArray.options() = mapNotNull(OptionTag::parse)
|
||||
|
||||
fun TagArray.relays() = mapNotNull(RelayTag::parse)
|
||||
|
||||
fun TagArray.pollType() = firstNotNullOfOrNull(PollTypeTag::parse) ?: PollType.SINGLE_CHOICE
|
||||
|
||||
fun TagArray.endsAt() = firstNotNullOfOrNull(EndsAtTag::parse)
|
||||
|
||||
fun TagArray.hasEnded() = endsAt()?.let { it < TimeUtils.now() } ?: false
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class EndsAtTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "endsAt"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): Long? {
|
||||
ensure(tag.has(1) && tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()
|
||||
}
|
||||
|
||||
fun assemble(time: Long) = arrayOfNotNull(TAG_NAME, time.toString())
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll.tags
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@Stable
|
||||
class OptionTag(
|
||||
val code: String,
|
||||
val label: String,
|
||||
) {
|
||||
companion object {
|
||||
const val TAG_NAME = "option"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(2) && tag[0] == TAG_NAME && tag[1].isNotEmpty() && tag[2].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): OptionTag? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
return OptionTag(tag[1], tag[2])
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
code: String,
|
||||
label: String,
|
||||
) = arrayOf(TAG_NAME, code, label)
|
||||
|
||||
fun assemble(option: OptionTag) = assemble(option.code, option.label)
|
||||
|
||||
fun assemble(labels: List<OptionTag>) = labels.map { opt -> assemble(opt) }
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
enum class PollType(
|
||||
val code: String,
|
||||
) {
|
||||
SINGLE_CHOICE("singlechoice"),
|
||||
MULTI_CHOICE("multiplechoice"),
|
||||
}
|
||||
|
||||
class PollTypeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "polltype"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(2) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): PollType? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return when (tag[1]) {
|
||||
PollType.SINGLE_CHOICE.code -> PollType.SINGLE_CHOICE
|
||||
PollType.MULTI_CHOICE.code -> PollType.MULTI_CHOICE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun assemble(type: PollType) = arrayOf(TAG_NAME, type.code)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.poll.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class RelayTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "relay"
|
||||
|
||||
fun match(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun notMatch(tag: Array<String>) = !(tag.has(0) && tag[0] == TAG_NAME)
|
||||
|
||||
fun parse(tag: Array<String>): NormalizedRelayUrl? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return RelayUrlNormalizer.normalizeOrNull(tag[1])
|
||||
}
|
||||
|
||||
fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url)
|
||||
|
||||
fun assemble(relays: List<NormalizedRelayUrl>) = relays.map { assemble(it) }
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.response
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseId
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey
|
||||
import com.vitorpamplona.quartz.nip22Comments.RootScope
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.responses
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.PollTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class PollResponseEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider,
|
||||
RootScope {
|
||||
override fun eventHints() = tags.mapNotNull(PollTag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(PollTag::parseId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
fun responses() = tags.responses()
|
||||
|
||||
fun poll() = tags.poll()
|
||||
|
||||
companion object {
|
||||
const val KIND = 1018
|
||||
const val ALT_DESCRIPTION = "Poll Response"
|
||||
|
||||
fun build(
|
||||
poll: EventHintBundle<PollEvent>,
|
||||
responses: Set<String>,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<PollResponseEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
poll(poll)
|
||||
notifyAuthor(poll)
|
||||
responses(responses)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.response
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.PollTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.ResponseTag
|
||||
|
||||
fun TagArrayBuilder<PollResponseEvent>.poll(pollHint: EventHintBundle<PollEvent>) = addUnique(PollTag.assemble(pollHint))
|
||||
|
||||
fun TagArrayBuilder<PollResponseEvent>.responses(responses: Set<String>) = addAll(ResponseTag.assemble(responses))
|
||||
|
||||
fun TagArrayBuilder<PollResponseEvent>.notifyAuthor(pollHint: EventHintBundle<PollEvent>) = add(pollHint.toPTag().toTagArray())
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.response
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.EndsAtTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollTypeTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.tags.RelayTag.Companion.parse
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.PollTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.ResponseTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.tags.ResponseTag.Companion.parse
|
||||
|
||||
fun TagArray.responses() = mapNotNull(ResponseTag::parse)
|
||||
|
||||
fun TagArray.poll() = firstNotNullOfOrNull(PollTag::parse)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.response.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@Immutable
|
||||
data class PollTag(
|
||||
override val eventId: HexKey,
|
||||
) : GenericETag {
|
||||
override var relay: NormalizedRelayUrl? = null
|
||||
override var author: HexKey? = null
|
||||
|
||||
constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, authorPubKeyHex: HexKey? = null) : this(eventId) {
|
||||
this.relay = relayHint
|
||||
this.author = authorPubKeyHex
|
||||
}
|
||||
|
||||
fun toNEvent(): String = NEvent.create(eventId, author, null, relay)
|
||||
|
||||
override fun toTagArray() = assemble(eventId, relay, author)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
eventId: HexKey,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
|
||||
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
eventIds: Set<HexKey>,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in eventIds
|
||||
|
||||
fun parse(tag: Array<String>): PollTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
|
||||
return PollTag(tag[1], hint, tag.getOrNull(3))
|
||||
}
|
||||
|
||||
fun parseId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): EventIdHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val hint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
author: HexKey?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author)
|
||||
|
||||
fun assemble(eventHint: EventHintBundle<PollEvent>) = assemble(eventHint.event.id, eventHint.relay, eventHint.event.pubKey)
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.quartz.nip88Polls.response.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ResponseTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "response"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(code: String) = arrayOf(TAG_NAME, code)
|
||||
|
||||
fun assemble(responses: Set<String>) = responses.map { code -> assemble(code) }
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,8 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
|
||||
@@ -254,6 +256,8 @@ class EventFactory {
|
||||
PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PollEvent.KIND -> PollEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PollResponseEvent.KIND -> PollResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ProxyRelayListEvent.KIND -> ProxyRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user