merge branch polls into main
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
/.idea/androidTestResultsUserPreferences.xml
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
|
/.idea/deploymentTargetDropDown.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
|||||||
- [ ] Delegated Event Signing (NIP-26)
|
- [ ] Delegated Event Signing (NIP-26)
|
||||||
- [ ] Account Creation / Backup Guidance (NIP-06)
|
- [ ] Account Creation / Backup Guidance (NIP-06)
|
||||||
- [ ] Message Sent feedback (NIP-20)
|
- [ ] Message Sent feedback (NIP-20)
|
||||||
|
- [ ] Polls (NIP-69)
|
||||||
|
|
||||||
# Development Overview
|
# Development Overview
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ android {
|
|||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
versionNameSuffix '-DEBUG'
|
versionNameSuffix '-DEBUG'
|
||||||
resValue "string", "app_name", "@string/app_name_debug"
|
resValue "string", "app_name", "@string/app_name_debug"
|
||||||
|
|
||||||
|
lintOptions{
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
|
import java.util.*
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
val DefaultChannels = setOf(
|
val DefaultChannels = setOf(
|
||||||
@@ -152,13 +156,18 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createZapRequestFor(note: Note): LnZapRequestEvent? {
|
fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? {
|
||||||
if (!isWriteable()) return null
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
note.event?.let {
|
note.event?.let { event ->
|
||||||
return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
|
return LnZapRequestEvent.create(
|
||||||
|
event,
|
||||||
|
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
|
||||||
|
?: localRelays.map { it.url }.toSet(),
|
||||||
|
loggedIn.privKey!!,
|
||||||
|
pollOption
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,6 +382,39 @@ class Account(
|
|||||||
LocalCache.consume(signedEvent)
|
LocalCache.consume(signedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendPoll(
|
||||||
|
message: String,
|
||||||
|
replyTo: List<Note>?,
|
||||||
|
mentions: List<User>?,
|
||||||
|
pollOptions: Map<Int, String>,
|
||||||
|
valueMaximum: Int?,
|
||||||
|
valueMinimum: Int?,
|
||||||
|
consensusThreshold: Int?,
|
||||||
|
closedAt: Int?
|
||||||
|
) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val repliesToHex = replyTo?.map { it.idHex }
|
||||||
|
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||||
|
val addresses = replyTo?.mapNotNull { it.address() }
|
||||||
|
|
||||||
|
val signedEvent = PollNoteEvent.create(
|
||||||
|
msg = message,
|
||||||
|
replyTos = repliesToHex,
|
||||||
|
mentions = mentionsHex,
|
||||||
|
addresses = addresses,
|
||||||
|
privateKey = loggedIn.privKey!!,
|
||||||
|
pollOptions = pollOptions,
|
||||||
|
valueMaximum = valueMaximum,
|
||||||
|
valueMinimum = valueMinimum,
|
||||||
|
consensusThreshold = consensusThreshold,
|
||||||
|
closedAt = closedAt
|
||||||
|
)
|
||||||
|
println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
|
||||||
|
Client.send(signedEvent)
|
||||||
|
LocalCache.consume(signedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List<User>?) {
|
fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List<User>?) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -716,7 +758,7 @@ class Account(
|
|||||||
isAcceptableDirect(note) &&
|
isAcceptableDirect(note) &&
|
||||||
(
|
(
|
||||||
note.event !is RepostEvent ||
|
note.event !is RepostEvent ||
|
||||||
(note.event is RepostEvent && note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null)
|
(note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null)
|
||||||
) // is not a reaction about a blocked post
|
) // is not a reaction about a blocked post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
@@ -241,6 +243,42 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: PollNoteEvent, relay: Relay? = null) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event)) {
|
||||||
|
relay?.let {
|
||||||
|
it.spamCounter++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Prepares user's profile view.
|
||||||
|
author.addNote(note)
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach {
|
||||||
|
it.addReply(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers()
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(event: BadgeDefinitionEvent) {
|
fun consume(event: BadgeDefinitionEvent) {
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
@@ -642,6 +680,7 @@ object LocalCache {
|
|||||||
fun findNotesStartingWith(text: String): List<Note> {
|
fun findNotesStartingWith(text: String): List<Note> {
|
||||||
return notes.values.filter {
|
return notes.values.filter {
|
||||||
(it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) ||
|
(it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) ||
|
||||||
|
(it.event is PollNoteEvent && it.event?.content()?.contains(text, true) ?: false) ||
|
||||||
(it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) ||
|
(it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) ||
|
||||||
it.idHex.startsWith(text, true) ||
|
it.idHex.startsWith(text, true) ||
|
||||||
it.idNote().startsWith(text, true)
|
it.idNote().startsWith(text, true)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||||
@@ -78,6 +79,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind,
|
TextNoteEvent.kind,
|
||||||
|
PollNoteEvent.kind,
|
||||||
ReactionEvent.kind,
|
ReactionEvent.kind,
|
||||||
RepostEvent.kind,
|
RepostEvent.kind,
|
||||||
ReportEvent.kind,
|
ReportEvent.kind,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
@@ -90,6 +91,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
}
|
}
|
||||||
is TextNoteEvent -> LocalCache.consume(event, relay)
|
is TextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
|
is PollNoteEvent -> LocalCache.consume(event, relay)
|
||||||
else -> {
|
else -> {
|
||||||
Log.w("Event Not Supported", event.toJson())
|
Log.w("Event Not Supported", event.toJson())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
@@ -11,7 +12,7 @@ object NostrGlobalDataSource : NostrDataSource("GlobalFeed") {
|
|||||||
fun createGlobalFilter() = TypedFilter(
|
fun createGlobalFilter() = TypedFilter(
|
||||||
types = setOf(FeedType.GLOBAL),
|
types = setOf(FeedType.GLOBAL),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind),
|
||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
@@ -51,7 +52,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
|
||||||
authors = followSet,
|
authors = followSet,
|
||||||
limit = 400
|
limit = 400
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-7
@@ -1,12 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
@@ -65,7 +60,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind),
|
||||||
search = mySearchString,
|
search = mySearchString,
|
||||||
limit = 20
|
limit = 20
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,19 +2,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
@@ -42,7 +30,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
TextNoteEvent.kind, LongTextNoteEvent.kind,
|
TextNoteEvent.kind, LongTextNoteEvent.kind,
|
||||||
ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind,
|
ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind,
|
||||||
LnZapEvent.kind, LnZapRequestEvent.kind,
|
LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||||
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind
|
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind,
|
||||||
|
PollNoteEvent.kind
|
||||||
),
|
),
|
||||||
tags = mapOf("a" to listOf(aTag.toTag())),
|
tags = mapOf("a" to listOf(aTag.toTag())),
|
||||||
since = it.lastReactionsDownloadTime
|
since = it.lastReactionsDownloadTime
|
||||||
@@ -95,7 +84,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
RepostEvent.kind,
|
RepostEvent.kind,
|
||||||
ReportEvent.kind,
|
ReportEvent.kind,
|
||||||
LnZapEvent.kind,
|
LnZapEvent.kind,
|
||||||
LnZapRequestEvent.kind
|
LnZapRequestEvent.kind,
|
||||||
|
PollNoteEvent.kind
|
||||||
),
|
),
|
||||||
tags = mapOf("e" to listOf(it.idHex)),
|
tags = mapOf("e" to listOf(it.idHex)),
|
||||||
since = it.lastReactionsDownloadTime
|
since = it.lastReactionsDownloadTime
|
||||||
@@ -128,7 +118,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind,
|
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||||
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind
|
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind,
|
||||||
|
PollNoteEvent.kind
|
||||||
),
|
),
|
||||||
ids = interestedEvents.toList()
|
ids = interestedEvents.toList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
@@ -43,7 +44,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
|||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
|
||||||
authors = listOf(it.pubkeyHex),
|
authors = listOf(it.pubkeyHex),
|
||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ open class Event(
|
|||||||
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient)
|
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient)
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ class LnZapEvent(
|
|||||||
.filter { it.firstOrNull() == "e" }
|
.filter { it.firstOrNull() == "e" }
|
||||||
.mapNotNull { it.getOrNull(1) }
|
.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
|
/* // TODO add poll_option tag to LnZapEvent
|
||||||
|
override fun zappedPollOption(): Int? = tags
|
||||||
|
.filter { it.firstOrNull() == "poll_option" }
|
||||||
|
.getOrNull(1)?.getOrNull(1)?.toInt()
|
||||||
|
*/
|
||||||
|
// TODO replace this hacky way to get poll option with above function
|
||||||
|
override fun zappedPollOption(): Int? = description()
|
||||||
|
?.substringAfter("poll_option\",\"")
|
||||||
|
?.substringBefore("\"")
|
||||||
|
?.toInt()
|
||||||
|
|
||||||
override fun zappedAuthor() = tags
|
override fun zappedAuthor() = tags
|
||||||
.filter { it.firstOrNull() == "p" }
|
.filter { it.firstOrNull() == "p" }
|
||||||
.mapNotNull { it.getOrNull(1) }
|
.mapNotNull { it.getOrNull(1) }
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ interface LnZapEventInterface : EventInterface {
|
|||||||
|
|
||||||
fun zappedPost(): List<String>
|
fun zappedPost(): List<String>
|
||||||
|
|
||||||
|
fun zappedPollOption(): Int?
|
||||||
|
|
||||||
fun zappedAuthor(): List<String>
|
fun zappedAuthor(): List<String>
|
||||||
|
|
||||||
fun taggedAddresses(): List<ATag>
|
fun taggedAddresses(): List<ATag>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class LnZapRequestEvent(
|
|||||||
originalNote: EventInterface,
|
originalNote: EventInterface,
|
||||||
relays: Set<String>,
|
relays: Set<String>,
|
||||||
privateKey: ByteArray,
|
privateKey: ByteArray,
|
||||||
|
pollOption: Int?,
|
||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): LnZapRequestEvent {
|
): LnZapRequestEvent {
|
||||||
val content = ""
|
val content = ""
|
||||||
@@ -35,6 +36,9 @@ class LnZapRequestEvent(
|
|||||||
if (originalNote is LongTextNoteEvent) {
|
if (originalNote is LongTextNoteEvent) {
|
||||||
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
|
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
|
||||||
}
|
}
|
||||||
|
if (pollOption != null && pollOption >= 0) {
|
||||||
|
tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
@@ -89,7 +93,11 @@ class LnZapRequestEvent(
|
|||||||
"wss://nostr.bitcoiner.social",
|
"wss://nostr.bitcoiner.social",
|
||||||
"ws://monad.jb55.com:8080",
|
"ws://monad.jb55.com:8080",
|
||||||
"wss://relay.snort.social"
|
"wss://relay.snort.social"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"poll_option", "n"
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
"ots": <base64-encoded OTS file data> // TODO
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
const val POLL_OPTION = "poll_option"
|
||||||
|
const val VALUE_MAXIMUM = "value_maximum"
|
||||||
|
const val VALUE_MINIMUM = "value_minimum"
|
||||||
|
const val CONSENSUS_THRESHOLD = "consensus_threshold"
|
||||||
|
const val CLOSED_AT = "closed_at"
|
||||||
|
|
||||||
|
class PollNoteEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
// ots: String?, TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun pollOptions(): Map<Int, String> {
|
||||||
|
val map = mutableMapOf<Int, String>()
|
||||||
|
tags.filter { it.first() == POLL_OPTION }
|
||||||
|
.forEach { map[it[1].toInt()] = it[2] }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTagInt(property: String): Int? {
|
||||||
|
val tagList = tags.filter {
|
||||||
|
it.firstOrNull() == property
|
||||||
|
}
|
||||||
|
val tag = tagList.getOrNull(0)
|
||||||
|
val s = tag?.getOrNull(1)
|
||||||
|
|
||||||
|
return if (s.isNullOrBlank() || s == "null") {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
s.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 6969
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
msg: String,
|
||||||
|
replyTos: List<String>?,
|
||||||
|
mentions: List<String>?,
|
||||||
|
addresses: List<ATag>?,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = Date().time / 1000,
|
||||||
|
pollOptions: Map<Int, String>,
|
||||||
|
valueMaximum: Int?,
|
||||||
|
valueMinimum: Int?,
|
||||||
|
consensusThreshold: Int?,
|
||||||
|
closedAt: Int?
|
||||||
|
): PollNoteEvent {
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
replyTos?.forEach {
|
||||||
|
tags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
mentions?.forEach {
|
||||||
|
tags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
tags.add(listOf("a", it.toTag()))
|
||||||
|
}
|
||||||
|
pollOptions.forEach { poll_op ->
|
||||||
|
tags.add(listOf(POLL_OPTION, poll_op.key.toString(), poll_op.value))
|
||||||
|
}
|
||||||
|
tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString()))
|
||||||
|
tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString()))
|
||||||
|
tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString()))
|
||||||
|
tags.add(listOf(CLOSED_AT, closedAt.toString()))
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>
|
||||||
|
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
|
||||||
|
"created_at": <unix timestamp in seconds>,
|
||||||
|
"kind": 6969,
|
||||||
|
"tags": [
|
||||||
|
["e", <32-bytes hex of the id of the poll event>, <primary poll host relay URL>],
|
||||||
|
["p", <32-bytes hex of the key>, <primary poll host relay URL>],
|
||||||
|
["poll_option", "0", "poll option 0 description string"],
|
||||||
|
["poll_option", "1", "poll option 1 description string"],
|
||||||
|
["poll_option", "n", "poll option <n> description string"],
|
||||||
|
["value_maximum", "maximum satoshi value for inclusion in tally"],
|
||||||
|
["value_minimum", "minimum satoshi value for inclusion in tally"],
|
||||||
|
["consensus_threshold", "required percentage to attain consensus <0..100>"],
|
||||||
|
["closed_at", "unix timestamp in seconds"],
|
||||||
|
],
|
||||||
|
"ots": <base64-encoded OTS file data>
|
||||||
|
"content": <primary poll description string>,
|
||||||
|
"sig": <64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field>
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
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.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
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.res.stringResource
|
||||||
|
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.actions.NewPollViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollClosing(pollViewModel: NewPollViewModel) {
|
||||||
|
var text by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
pollViewModel.isValidClosedAt.value = true
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val int = text.toInt()
|
||||||
|
if (int < 0) {
|
||||||
|
pollViewModel.isValidClosedAt.value = false
|
||||||
|
} else { pollViewModel.closedAt = int }
|
||||||
|
} catch (e: Exception) { pollViewModel.isValidClosedAt.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = stringResource(R.string.poll_closing_time),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_closing_time_days),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewPollClosingPreview() {
|
||||||
|
NewPollClosing(NewPollViewModel())
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
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.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
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.res.stringResource
|
||||||
|
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.actions.NewPollViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) {
|
||||||
|
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) { pollViewModel.isValidConsensusThreshold.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = stringResource(R.string.poll_consensus_threshold),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_consensus_threshold_percent),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewPollConsensusThresholdPreview() {
|
||||||
|
NewPollConsensusThreshold(NewPollViewModel())
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) {
|
||||||
|
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1F),
|
||||||
|
value = pollViewModel.pollOptions[optionIndex] ?: "",
|
||||||
|
onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_option_index).format(optionIndex),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_option_description),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid
|
||||||
|
)
|
||||||
|
if (optionIndex > 1) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 6.dp, top = 2.dp)
|
||||||
|
.imePadding(),
|
||||||
|
onClick = { pollViewModel.pollOptions.remove(optionIndex) },
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painterResource(id = android.R.drawable.ic_delete),
|
||||||
|
contentDescription = "Remove poll option button",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewPollOptionPreview() {
|
||||||
|
NewPollOption(NewPollViewModel(), 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.res.stringResource
|
||||||
|
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.NewPollViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NewPollPrimaryDescription(pollViewModel: NewPollViewModel) {
|
||||||
|
// 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 = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = pollViewModel.message,
|
||||||
|
onValueChange = {
|
||||||
|
pollViewModel.updateMessage(it)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_primary_description),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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 = stringResource(R.string.poll_primary_description),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = if (isInputValid) colorValid else colorInValid,
|
||||||
|
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollRecipientsField(pollViewModel: NewPollViewModel, 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, // TODO enable add recipients
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_zap_recipients),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_zap_recipients),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.*
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) {
|
||||||
|
val pollViewModel: NewPollViewModel = viewModel()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
pollViewModel.load(account, baseReplyTo, quote)
|
||||||
|
delay(100)
|
||||||
|
|
||||||
|
pollViewModel.imageUploadingError.collect { error ->
|
||||||
|
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { onClose() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
decorFitsSystemWindows = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||||
|
.imePadding()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CloseButton(onCancel = {
|
||||||
|
pollViewModel.cancel()
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
PollButton(
|
||||||
|
onPost = {
|
||||||
|
pollViewModel.sendPoll()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
isActive = pollViewModel.message.text.isNotBlank() &&
|
||||||
|
pollViewModel.pollOptions.values.all { it.isNotEmpty() } &&
|
||||||
|
pollViewModel.isValidRecipients.value &&
|
||||||
|
pollViewModel.isValidvalueMaximum.value &&
|
||||||
|
pollViewModel.isValidvalueMinimum.value &&
|
||||||
|
pollViewModel.isValidConsensusThreshold.value &&
|
||||||
|
pollViewModel.isValidClosedAt.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
if (pollViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
|
||||||
|
ReplyInformation(pollViewModel.replyTos, pollViewModel.mentions, account, "✖ ") {
|
||||||
|
pollViewModel.removeFromReplyList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(stringResource(R.string.poll_heading_required))
|
||||||
|
// NewPollRecipientsField(pollViewModel, account)
|
||||||
|
NewPollPrimaryDescription(pollViewModel)
|
||||||
|
pollViewModel.pollOptions.values.forEachIndexed { index, element ->
|
||||||
|
NewPollOption(pollViewModel, index)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { pollViewModel.pollOptions[pollViewModel.pollOptions.size] = "" },
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painterResource(id = android.R.drawable.ic_input_add),
|
||||||
|
contentDescription = "Add poll option button",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.poll_heading_optional))
|
||||||
|
NewPollVoteValueRange(pollViewModel)
|
||||||
|
NewPollConsensusThreshold(pollViewModel)
|
||||||
|
NewPollClosing(pollViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val userSuggestions = pollViewModel.userSuggestions
|
||||||
|
if (userSuggestions.isNotEmpty()) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 10.dp
|
||||||
|
),
|
||||||
|
modifier = Modifier.heightIn(0.dp, 300.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
userSuggestions,
|
||||||
|
key = { _, item -> item.pubkeyHex }
|
||||||
|
) { index, item ->
|
||||||
|
UserLine(item, account) {
|
||||||
|
pollViewModel.autocompleteWithUser(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
/*UploadFromGallery(
|
||||||
|
isUploading = pollViewModel.isUploadingImage
|
||||||
|
) {
|
||||||
|
pollViewModel.upload(it, context)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) {
|
||||||
|
Button(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = {
|
||||||
|
if (isActive) {
|
||||||
|
onPost()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.post_poll), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewPollViewPreview() {
|
||||||
|
NewPollView(onClose = {}, account = Account(loggedIn = Persona()))
|
||||||
|
}*/
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import com.vitorpamplona.amethyst.model.*
|
||||||
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
|
|
||||||
|
class NewPollViewModel : NewPostViewModel() {
|
||||||
|
|
||||||
|
var zapRecipients = mutableStateListOf<HexKey>()
|
||||||
|
var pollOptions = newStateMapPollOptions()
|
||||||
|
var valueMaximum: Int? = null
|
||||||
|
var valueMinimum: Int? = null
|
||||||
|
var consensusThreshold: Int? = null
|
||||||
|
var closedAt: Int? = null
|
||||||
|
|
||||||
|
var isValidRecipients = mutableStateOf(true)
|
||||||
|
var isValidvalueMaximum = mutableStateOf(true)
|
||||||
|
var isValidvalueMinimum = mutableStateOf(true)
|
||||||
|
var isValidConsensusThreshold = mutableStateOf(true)
|
||||||
|
var isValidClosedAt = mutableStateOf(true)
|
||||||
|
|
||||||
|
override fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||||
|
super.load(account, replyingTo, quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUserToMentions(user: User) {
|
||||||
|
super.addUserToMentions(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addNoteToReplyTos(note: Note) {
|
||||||
|
super.addNoteToReplyTos(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun tagIndex(user: User): Int {
|
||||||
|
return super.tagIndex(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun tagIndex(note: Note): Int {
|
||||||
|
return super.tagIndex(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendPoll() {
|
||||||
|
// adds all references to mentions and reply tos
|
||||||
|
message.text.split('\n').forEach { paragraph: String ->
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
val results = parseDirtyWordForKey(word)
|
||||||
|
|
||||||
|
if (results?.key?.type == Nip19.Type.USER) {
|
||||||
|
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
||||||
|
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||||
|
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||||
|
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||||
|
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||||
|
if (note != null) {
|
||||||
|
addNoteToReplyTos(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags the text in the correct order.
|
||||||
|
val newMessage = message.text.split('\n').map { paragraph: String ->
|
||||||
|
paragraph.split(' ').map { word: String ->
|
||||||
|
val results = parseDirtyWordForKey(word)
|
||||||
|
if (results?.key?.type == Nip19.Type.USER) {
|
||||||
|
val user = LocalCache.getOrCreateUser(results.key.hex)
|
||||||
|
|
||||||
|
"#[${tagIndex(user)}]${results.restOfWord}"
|
||||||
|
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||||
|
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||||
|
|
||||||
|
"#[${tagIndex(note)}]${results.restOfWord}"
|
||||||
|
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||||
|
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||||
|
if (note != null) {
|
||||||
|
"#[${tagIndex(note)}]${results.restOfWord}"
|
||||||
|
} else {
|
||||||
|
word
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
word
|
||||||
|
}
|
||||||
|
}.joinToString(" ")
|
||||||
|
}.joinToString("\n")
|
||||||
|
|
||||||
|
/* if (originalNote?.channel() != null) {
|
||||||
|
account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
|
||||||
|
} else {
|
||||||
|
account?.sendPoll(newMessage, replyTos, mentions)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt)
|
||||||
|
|
||||||
|
clearPollStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
super.cancel()
|
||||||
|
|
||||||
|
clearPollStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findUrlInMessage(): String? {
|
||||||
|
return super.findUrlInMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeFromReplyList(it: User) {
|
||||||
|
super.removeFromReplyList(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateMessage(it: TextFieldValue) {
|
||||||
|
super.updateMessage(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun autocompleteWithUser(item: User) {
|
||||||
|
super.autocompleteWithUser(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear all states
|
||||||
|
private fun clearPollStates() {
|
||||||
|
message = TextFieldValue("")
|
||||||
|
urlPreview = null
|
||||||
|
isUploadingImage = false
|
||||||
|
mentions = null
|
||||||
|
|
||||||
|
zapRecipients = mutableStateListOf<HexKey>()
|
||||||
|
pollOptions = newStateMapPollOptions()
|
||||||
|
valueMaximum = null
|
||||||
|
valueMinimum = null
|
||||||
|
consensusThreshold = null
|
||||||
|
closedAt = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
|
||||||
|
return mutableStateMapOf(Pair(0, ""), Pair(1, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
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.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
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.res.stringResource
|
||||||
|
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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
|
||||||
|
var textMax by rememberSaveable { mutableStateOf("") }
|
||||||
|
var textMin by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
// check for zapMax amounts < 1
|
||||||
|
pollViewModel.isValidvalueMaximum.value = true
|
||||||
|
if (textMax.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val int = textMax.toInt()
|
||||||
|
if (int < 1) {
|
||||||
|
pollViewModel.isValidvalueMaximum.value = false
|
||||||
|
} else { pollViewModel.valueMaximum = int }
|
||||||
|
} catch (e: Exception) { pollViewModel.isValidvalueMaximum.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for minZap amounts < 1
|
||||||
|
pollViewModel.isValidvalueMinimum.value = true
|
||||||
|
if (textMin.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val int = textMin.toInt()
|
||||||
|
if (int < 1) {
|
||||||
|
pollViewModel.isValidvalueMinimum.value = false
|
||||||
|
} else { pollViewModel.valueMinimum = int }
|
||||||
|
} catch (e: Exception) { pollViewModel.isValidvalueMinimum.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for zapMin > zapMax
|
||||||
|
if (textMin.isNotEmpty() && textMax.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val intMin = textMin.toInt()
|
||||||
|
val intMax = textMax.toInt()
|
||||||
|
|
||||||
|
if (intMin > intMax) {
|
||||||
|
pollViewModel.isValidvalueMinimum.value = false
|
||||||
|
pollViewModel.isValidvalueMaximum.value = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pollViewModel.isValidvalueMinimum.value = false
|
||||||
|
pollViewModel.isValidvalueMaximum.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = textMin,
|
||||||
|
onValueChange = { textMin = it },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.width(150.dp),
|
||||||
|
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_zap_value_min),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sats),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = textMax,
|
||||||
|
onValueChange = { textMax = it },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.width(150.dp),
|
||||||
|
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_zap_value_max),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sats),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NewPollVoteValueRangePreview() {
|
||||||
|
NewPollVoteValueRange(NewPollViewModel())
|
||||||
|
}
|
||||||
@@ -19,9 +19,9 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NewPostViewModel : ViewModel() {
|
open class NewPostViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
var account: Account? = null
|
||||||
private var originalNote: Note? = null
|
var originalNote: Note? = null
|
||||||
|
|
||||||
var mentions by mutableStateOf<List<User>?>(null)
|
var mentions by mutableStateOf<List<User>?>(null)
|
||||||
var replyTos by mutableStateOf<List<Note>?>(null)
|
var replyTos by mutableStateOf<List<Note>?>(null)
|
||||||
@@ -34,7 +34,7 @@ class NewPostViewModel : ViewModel() {
|
|||||||
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
||||||
var userSuggestionAnchor: TextRange? = null
|
var userSuggestionAnchor: TextRange? = null
|
||||||
|
|
||||||
fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||||
originalNote = replyingTo
|
originalNote = replyingTo
|
||||||
replyingTo?.let { replyNote ->
|
replyingTo?.let { replyNote ->
|
||||||
this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
||||||
@@ -58,21 +58,21 @@ class NewPostViewModel : ViewModel() {
|
|||||||
this.account = account
|
this.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addUserToMentions(user: User) {
|
open fun addUserToMentions(user: User) {
|
||||||
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
|
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNoteToReplyTos(note: Note) {
|
open fun addNoteToReplyTos(note: Note) {
|
||||||
note.author?.let { addUserToMentions(it) }
|
note.author?.let { addUserToMentions(it) }
|
||||||
replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
|
replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tagIndex(user: User): Int {
|
open fun tagIndex(user: User): Int {
|
||||||
// Postr Events assembles replies before mentions in the tag order
|
// Postr Events assembles replies before mentions in the tag order
|
||||||
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
|
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tagIndex(note: Note): Int {
|
open fun tagIndex(note: Note): Int {
|
||||||
// Postr Events assembles replies before mentions in the tag order
|
// Postr Events assembles replies before mentions in the tag order
|
||||||
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
|
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
|
||||||
}
|
}
|
||||||
@@ -163,14 +163,14 @@ class NewPostViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
open fun cancel() {
|
||||||
message = TextFieldValue("")
|
message = TextFieldValue("")
|
||||||
urlPreview = null
|
urlPreview = null
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
mentions = null
|
mentions = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findUrlInMessage(): String? {
|
open fun findUrlInMessage(): String? {
|
||||||
return message.text.split('\n').firstNotNullOfOrNull { paragraph ->
|
return message.text.split('\n').firstNotNullOfOrNull { paragraph ->
|
||||||
paragraph.split(' ').firstOrNull { word: String ->
|
paragraph.split(' ').firstOrNull { word: String ->
|
||||||
isValidURL(word) || noProtocolUrlValidator.matcher(word).matches()
|
isValidURL(word) || noProtocolUrlValidator.matcher(word).matches()
|
||||||
@@ -178,11 +178,11 @@ class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromReplyList(it: User) {
|
open fun removeFromReplyList(it: User) {
|
||||||
mentions = mentions?.minus(it)
|
mentions = mentions?.minus(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMessage(it: TextFieldValue) {
|
open fun updateMessage(it: TextFieldValue) {
|
||||||
message = it
|
message = it
|
||||||
urlPreview = findUrlInMessage()
|
urlPreview = findUrlInMessage()
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun autocompleteWithUser(item: User) {
|
open fun autocompleteWithUser(item: User) {
|
||||||
userSuggestionAnchor?.let {
|
userSuggestionAnchor?.let {
|
||||||
val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||||
val lastWordStart = it.end - lastWord.length
|
val lastWordStart = it.end - lastWord.length
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.buttons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewPollView
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FabColumn(account: Account) {
|
||||||
|
var isOpen by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var wantsToPoll by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var wantsToPost by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
if (isOpen) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
wantsToPoll = true
|
||||||
|
isOpen = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(45.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_poll),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(26.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
wantsToPost = true
|
||||||
|
isOpen = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(45.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_lists),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(26.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { isOpen = !isOpen },
|
||||||
|
modifier = Modifier.size(55.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_compose),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(26.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantsToPost) {
|
||||||
|
NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantsToPoll) {
|
||||||
|
NewPollView({ wantsToPoll = false }, account = account)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.buttons
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.ButtonDefaults
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.OutlinedButton
|
|
||||||
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.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NewNoteButton(account: Account) {
|
|
||||||
var wantsToPost by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wantsToPost) {
|
|
||||||
NewPostView({ wantsToPost = false }, account = account)
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { wantsToPost = true },
|
|
||||||
modifier = Modifier.size(55.dp),
|
|
||||||
shape = CircleShape,
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
|
||||||
contentPadding = PaddingValues(0.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_compose),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(26.dp),
|
|
||||||
tint = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import androidx.navigation.NavController
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TranslateableRichTextViewer(
|
fun TranslatableRichTextViewer(
|
||||||
content: String,
|
content: String,
|
||||||
canPreview: Boolean,
|
canPreview: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object GlobalFeedFilter : FeedFilter<Note>() {
|
object GlobalFeedFilter : FeedFilter<Note>() {
|
||||||
@@ -17,7 +18,7 @@ object GlobalFeedFilter : FeedFilter<Note>() {
|
|||||||
val notes = LocalCache.notes.values
|
val notes = LocalCache.notes.values
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) &&
|
(it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent) &&
|
||||||
it.replyTo.isNullOrEmpty()
|
it.replyTo.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
.filter {
|
.filter {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
||||||
@@ -15,7 +17,7 @@ object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
return LocalCache.notes.values
|
return LocalCache.notes.values
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent) &&
|
(it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is RepostEvent) &&
|
||||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ object HomeNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
val notes = LocalCache.notes.values
|
val notes = LocalCache.notes.values
|
||||||
.filter { it ->
|
.filter { it ->
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent) &&
|
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is PollNoteEvent) &&
|
||||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ object NotificationFeedFilter : FeedFilter<Note>() {
|
|||||||
it.replyTo?.any { it.author == loggedInUser } == true ||
|
it.replyTo?.any { it.author == loggedInUser } == true ||
|
||||||
loggedInUser in it.directlyCiteUsers()
|
loggedInUser in it.directlyCiteUsers()
|
||||||
}
|
}
|
||||||
|
.filter { it ->
|
||||||
|
it.event !is PollNoteEvent ||
|
||||||
|
it.replyTo?.any { it.author == account.userProfile() } == true ||
|
||||||
|
account.userProfile() in it.directlyCiteUsers()
|
||||||
|
}
|
||||||
.filter {
|
.filter {
|
||||||
it.event !is ReactionEvent ||
|
it.event !is ReactionEvent ||
|
||||||
it.replyTo?.lastOrNull()?.author == loggedInUser ||
|
it.replyTo?.lastOrNull()?.author == loggedInUser ||
|
||||||
|
|||||||
@@ -63,12 +63,8 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.*
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||||
@@ -490,7 +486,7 @@ fun NoteComposeInner(
|
|||||||
) {
|
) {
|
||||||
val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
|
val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
|
||||||
|
|
||||||
TranslateableRichTextViewer(
|
TranslatableRichTextViewer(
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.private_conversation_notification,
|
id = R.string.private_conversation_notification,
|
||||||
"@${note.author?.pubkeyNpub()}",
|
"@${note.author?.pubkeyNpub()}",
|
||||||
@@ -528,7 +524,7 @@ fun NoteComposeInner(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TranslateableRichTextViewer(
|
TranslatableRichTextViewer(
|
||||||
eventContent,
|
eventContent,
|
||||||
canPreview = canPreview && !makeItShort,
|
canPreview = canPreview && !makeItShort,
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
@@ -540,6 +536,27 @@ fun NoteComposeInner(
|
|||||||
|
|
||||||
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
TranslateableRichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
canPreview = canPreview && !makeItShort,
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
noteEvent.tags(),
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (noteEvent is PollNoteEvent) {
|
||||||
|
PollNote(
|
||||||
|
note,
|
||||||
|
canPreview = canPreview && !makeItShort,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!makeItShort) {
|
if (!makeItShort) {
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PollNote(
|
||||||
|
note: Note,
|
||||||
|
canPreview: Boolean,
|
||||||
|
backgroundColor: Color,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val pollViewModel = PollNoteViewModel()
|
||||||
|
pollViewModel.load(note)
|
||||||
|
|
||||||
|
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op ->
|
||||||
|
val optionTally = pollViewModel.optionVoteTally(poll_op.key)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
poll_op.value,
|
||||||
|
canPreview,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(250.dp)
|
||||||
|
.padding(0.dp) // padding outside border
|
||||||
|
.border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)))
|
||||||
|
.padding(4.dp), // padding between border and text
|
||||||
|
pollViewModel.pollEvent?.tags(),
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
|
||||||
|
ZapVote(note, accountViewModel, pollViewModel, poll_op.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// only show tallies after user has zapped note
|
||||||
|
if (note.isZappedBy(accountViewModel.userProfile())) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.width(250.dp),
|
||||||
|
color = if (
|
||||||
|
pollViewModel.consensusThreshold != null &&
|
||||||
|
optionTally >= pollViewModel.consensusThreshold!!
|
||||||
|
) {
|
||||||
|
Color.Green
|
||||||
|
} else { MaterialTheme.colors.primary },
|
||||||
|
progress = optionTally
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
fun ZapVote(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
pollViewModel: PollNoteViewModel,
|
||||||
|
pollOption: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
|
val zapsState by baseNote.live().zaps.observeAsState()
|
||||||
|
val zappedNote = zapsState?.note
|
||||||
|
|
||||||
|
var wantsToZap by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var zappingProgress by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(Modifier.size(20.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
role = Role.Button,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||||
|
onClick = {
|
||||||
|
if (!accountViewModel.isWriteable()) {
|
||||||
|
scope.launch {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (pollViewModel.isPollClosed) {
|
||||||
|
scope.launch {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.poll_is_closed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (pollViewModel.isVoteAmountAtomic()) {
|
||||||
|
// only allow one vote per option when min==max, i.e. atomic vote amount specified
|
||||||
|
if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) {
|
||||||
|
scope.launch {
|
||||||
|
Toast
|
||||||
|
.makeText(context, R.string.one_vote_per_user_on_atomic_votes, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return@combinedClickable
|
||||||
|
}
|
||||||
|
accountViewModel.zap(
|
||||||
|
baseNote,
|
||||||
|
pollViewModel.valueMaximum!!.toLong() * 1000,
|
||||||
|
pollOption,
|
||||||
|
"",
|
||||||
|
context,
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
zappingProgress = 0f
|
||||||
|
Toast
|
||||||
|
.makeText(context, it, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onProgress = {
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
zappingProgress = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
wantsToZap = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (wantsToZap) {
|
||||||
|
ZapVoteAmountChoicePopup(
|
||||||
|
baseNote,
|
||||||
|
accountViewModel,
|
||||||
|
pollViewModel,
|
||||||
|
pollOption,
|
||||||
|
onDismiss = {
|
||||||
|
wantsToZap = false
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onProgress = {
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
zappingProgress = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Bolt,
|
||||||
|
contentDescription = stringResource(R.string.zaps),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = BitcoinOrange
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Bolt,
|
||||||
|
contentDescription = stringResource(id = R.string.zaps),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only show tallies after a user has zapped note
|
||||||
|
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
|
||||||
|
Text(
|
||||||
|
showAmount(pollViewModel.zappedPollOptionAmount(pollOption)),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ZapVoteAmountChoicePopup(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
pollViewModel: PollNoteViewModel,
|
||||||
|
pollOption: Int,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onError: (text: String) -> Unit,
|
||||||
|
onProgress: (percent: Float) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var inputAmountText by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.error,
|
||||||
|
unfocusedBorderColor = Color.Red
|
||||||
|
)
|
||||||
|
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
focusedBorderColor = MaterialTheme.colors.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { onDismiss() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnClickOutside = true,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
val amount = pollViewModel.inputVoteAmountLong(inputAmountText)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = inputAmountText,
|
||||||
|
onValueChange = { inputAmountText = it },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.width(150.dp),
|
||||||
|
colors = if (pollViewModel.isValidInputVoteAmount(amount)) colorValid else colorInValid,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.poll_zap_amount),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = pollViewModel.voteAmountPlaceHolderText(context.getString(R.string.sats)),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val isValidInputAmount = pollViewModel.isValidInputVoteAmount(amount)
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
enabled = isValidInputAmount,
|
||||||
|
onClick = {
|
||||||
|
if (amount != null && isValidInputAmount) {
|
||||||
|
accountViewModel.zap(
|
||||||
|
baseNote,
|
||||||
|
amount * 1000,
|
||||||
|
pollOption,
|
||||||
|
"",
|
||||||
|
context,
|
||||||
|
onError,
|
||||||
|
onProgress
|
||||||
|
)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"⚡ ${showAmount(amount?.toBigDecimal()?.setScale(1))}",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (amount != null && isValidInputAmount) {
|
||||||
|
accountViewModel.zap(
|
||||||
|
baseNote,
|
||||||
|
amount * 1000,
|
||||||
|
pollOption,
|
||||||
|
"",
|
||||||
|
context,
|
||||||
|
onError,
|
||||||
|
onProgress
|
||||||
|
)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class PollNoteViewModel {
|
||||||
|
var account: Account? = null
|
||||||
|
private var pollNote: Note? = null
|
||||||
|
|
||||||
|
var pollEvent: PollNoteEvent? = null
|
||||||
|
private var pollOptions: Map<Int, String>? = null
|
||||||
|
var valueMaximum: Int? = null
|
||||||
|
private var valueMinimum: Int? = null
|
||||||
|
private var closedAt: Int? = null
|
||||||
|
var consensusThreshold: Float? = null
|
||||||
|
|
||||||
|
fun load(note: Note?) {
|
||||||
|
pollNote = note
|
||||||
|
pollEvent = pollNote?.event as PollNoteEvent
|
||||||
|
pollOptions = pollEvent?.pollOptions()
|
||||||
|
valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM)
|
||||||
|
valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM)
|
||||||
|
consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)
|
||||||
|
closedAt = pollEvent?.getTagInt(CLOSED_AT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
|
||||||
|
|
||||||
|
val isPollClosed: Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate
|
||||||
|
pollNote?.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000
|
||||||
|
} == true
|
||||||
|
|
||||||
|
fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) {
|
||||||
|
sats
|
||||||
|
} else if (valueMinimum == null) {
|
||||||
|
"1—$valueMaximum $sats"
|
||||||
|
} else if (valueMaximum == null) {
|
||||||
|
">$valueMinimum $sats"
|
||||||
|
} else {
|
||||||
|
"$valueMinimum—$valueMaximum $sats"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inputVoteAmountLong(textAmount: String) = if (textAmount.isEmpty()) { null } else {
|
||||||
|
try {
|
||||||
|
textAmount.toLong()
|
||||||
|
} catch (e: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isValidInputVoteAmount(amount: Long?): Boolean {
|
||||||
|
if (amount == null) {
|
||||||
|
return false
|
||||||
|
} else if (valueMinimum == null && valueMaximum == null) {
|
||||||
|
if (amount > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if (valueMinimum == null) {
|
||||||
|
if (amount > 0 && amount <= valueMaximum!!) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if (valueMaximum == null) {
|
||||||
|
if (amount >= valueMinimum!!) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((valueMinimum!! <= amount) && (amount <= valueMaximum!!)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun optionVoteTally(op: Int): Float {
|
||||||
|
val tally = zappedPollOptionAmount(op).toFloat().div(zappedVoteTotal())
|
||||||
|
return if (tally.isNaN()) { // catch div by 0
|
||||||
|
0f
|
||||||
|
} else { tally }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zappedVoteTotal(): Float {
|
||||||
|
var total = 0f
|
||||||
|
pollOptions?.keys?.forEach {
|
||||||
|
total += zappedPollOptionAmount(it).toFloat()
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPollOptionZappedBy(option: Int, user: User): Boolean {
|
||||||
|
if (pollNote?.zaps?.any { it.key.author == user } == true) {
|
||||||
|
pollNote!!.zaps.mapNotNull { it.value?.event }
|
||||||
|
.filterIsInstance<LnZapEvent>()
|
||||||
|
.map {
|
||||||
|
val zappedOption = it.zappedPollOption()
|
||||||
|
if (zappedOption == option) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun zappedPollOptionAmount(option: Int): BigDecimal {
|
||||||
|
return if (pollNote != null) {
|
||||||
|
pollNote!!.zaps.mapNotNull { it.value?.event }
|
||||||
|
.filterIsInstance<LnZapEvent>()
|
||||||
|
.mapNotNull {
|
||||||
|
val zappedOption = it.zappedPollOption()
|
||||||
|
if (zappedOption == option) {
|
||||||
|
it.amount
|
||||||
|
} else { null }
|
||||||
|
}.sumOf { it }
|
||||||
|
} else {
|
||||||
|
BigDecimal(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -347,6 +347,7 @@ fun ZapReaction(
|
|||||||
accountViewModel.zap(
|
accountViewModel.zap(
|
||||||
baseNote,
|
baseNote,
|
||||||
account.zapAmountChoices.first() * 1000,
|
account.zapAmountChoices.first() * 1000,
|
||||||
|
null,
|
||||||
"",
|
"",
|
||||||
context,
|
context,
|
||||||
onError = {
|
onError = {
|
||||||
@@ -547,6 +548,7 @@ fun ZapAmountChoicePopup(
|
|||||||
accountViewModel.zap(
|
accountViewModel.zap(
|
||||||
baseNote,
|
baseNote,
|
||||||
amountInSats * 1000,
|
amountInSats * 1000,
|
||||||
|
null,
|
||||||
"",
|
"",
|
||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
@@ -571,6 +573,7 @@ fun ZapAmountChoicePopup(
|
|||||||
accountViewModel.zap(
|
accountViewModel.zap(
|
||||||
baseNote,
|
baseNote,
|
||||||
amountInSats * 1000,
|
amountInSats * 1000,
|
||||||
|
null,
|
||||||
"",
|
"",
|
||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.SnackbarDefaults.backgroundColor
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
@@ -53,8 +54,10 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.*
|
||||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
|
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
|
||||||
@@ -344,7 +347,7 @@ fun NoteMaster(
|
|||||||
!noteForReports.hasAnyReports()
|
!noteForReports.hasAnyReports()
|
||||||
|
|
||||||
if (eventContent != null) {
|
if (eventContent != null) {
|
||||||
TranslateableRichTextViewer(
|
TranslatableRichTextViewer(
|
||||||
eventContent,
|
eventContent,
|
||||||
canPreview,
|
canPreview,
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
@@ -355,6 +358,16 @@ fun NoteMaster(
|
|||||||
)
|
)
|
||||||
|
|
||||||
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
||||||
|
|
||||||
|
if (noteEvent is PollNoteEvent) {
|
||||||
|
PollNote(
|
||||||
|
note,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||||||
account.delete(account.boostsTo(note))
|
account.delete(account.boostsTo(note))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) {
|
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) {
|
||||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||||
|
|
||||||
if (lud16.isNullOrBlank()) {
|
if (lud16.isNullOrBlank()) {
|
||||||
@@ -60,7 +60,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val zapRequest = account.createZapRequestFor(note)
|
val zapRequest = account.createZapRequestFor(note, pollOption)
|
||||||
|
|
||||||
onProgress(0.10f)
|
onProgress(0.10f)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.DrawerValue
|
import androidx.compose.material.DrawerValue
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@@ -22,7 +23,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.vitorpamplona.amethyst.buttons.NewChannelButton
|
import com.vitorpamplona.amethyst.buttons.NewChannelButton
|
||||||
import com.vitorpamplona.amethyst.buttons.NewNoteButton
|
import com.vitorpamplona.amethyst.ui.buttons.FabColumn
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.*
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
|
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
||||||
@@ -64,7 +66,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingButton(navController, accountStateViewModel)
|
FloatingButtons(navController, accountStateViewModel)
|
||||||
},
|
},
|
||||||
scaffoldState = scaffoldState
|
scaffoldState = scaffoldState
|
||||||
) {
|
) {
|
||||||
@@ -76,7 +78,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) {
|
fun FloatingButtons(navController: NavHostController, accountViewModel: AccountStateViewModel) {
|
||||||
val accountState by accountViewModel.accountContent.collectAsState()
|
val accountState by accountViewModel.accountContent.collectAsState()
|
||||||
|
|
||||||
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
||||||
@@ -89,7 +91,7 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt
|
|||||||
// Does nothing.
|
// Does nothing.
|
||||||
}
|
}
|
||||||
is AccountState.LoggedIn -> {
|
is AccountState.LoggedIn -> {
|
||||||
NewNoteButton(state.account)
|
FabColumn(state.account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -267,6 +267,22 @@
|
|||||||
<string name="wallet_connect_service_secret_placeholder">nsec / hex private key</string>
|
<string name="wallet_connect_service_secret_placeholder">nsec / hex private key</string>
|
||||||
|
|
||||||
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
|
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
|
||||||
|
<string name="post_poll">Post Poll</string>
|
||||||
|
<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_index">Option %s</string>
|
||||||
|
<string name="poll_option_description">Poll option description</string>
|
||||||
|
<string name="poll_heading_optional">Optional fields:</string>
|
||||||
|
<string name="poll_zap_value_min">Zap minimum</string>
|
||||||
|
<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_time">Close after</string>
|
||||||
|
<string name="poll_closing_time_days">days</string>
|
||||||
|
<string name="poll_is_closed">Poll is closed to new votes</string>
|
||||||
|
<string name="poll_zap_amount">Zap amount</string>
|
||||||
|
<string name="one_vote_per_user_on_atomic_votes">Only one vote per user is allowed on this type of poll</string>
|
||||||
|
|
||||||
<string name="looking_for_event">"Looking for Event %1$s"</string>
|
<string name="looking_for_event">"Looking for Event %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user