Merge branch 'main' into quote_problem

This commit is contained in:
Vitor Pamplona
2023-04-19 10:38:20 -04:00
committed by GitHub
25 changed files with 184 additions and 141 deletions
@@ -54,8 +54,14 @@ class Account(
val liveLanguages: AccountLiveData = AccountLiveData(this) val liveLanguages: AccountLiveData = AccountLiveData(this)
val saveable: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this)
var userProfileCache: User? = null
fun userProfile(): User { fun userProfile(): User {
return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) return userProfileCache ?: run {
val myUser: User = LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey())
userProfileCache = myUser
myUser
}
} }
fun followingChannels(): List<Channel> { fun followingChannels(): List<Channel> {
@@ -595,7 +595,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
@@ -53,13 +53,14 @@ open class Note(val idHex: String) {
open fun idNote() = id().toNote() open fun idNote() = id().toNote()
open fun idDisplayNote() = idNote().toShortenHex() open fun idDisplayNote() = idNote().toShortenHex()
fun channel(): Channel? { fun channelHex(): HexKey? {
val channelHex = return (event as? ChannelMessageEvent)?.channel()
(event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel() ?: (event as? ChannelCreateEvent)?.id
?: (event as? ChannelCreateEvent)?.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } fun channel(): Channel? {
return channelHex()?.let { LocalCache.checkGetOrCreateChannel(it) }
} }
open fun address(): ATag? = null open fun address(): ATag? = null
@@ -76,7 +76,7 @@ abstract class NostrDataSource(val debugName: String) {
is DeletionEvent -> LocalCache.consume(event) is DeletionEvent -> LocalCache.consume(event)
is LnZapEvent -> { is LnZapEvent -> {
event.containedPost()?.let { onEvent(it, subscriptionId, relay) } event.zapRequest?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(event) LocalCache.consume(event)
} }
is LnZapRequestEvent -> LocalCache.consume(event) is LnZapRequestEvent -> LocalCache.consume(event)
@@ -154,7 +154,7 @@ abstract class NostrDataSource(val debugName: String) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(250, Dispatchers.IO) { private val bundler = BundledUpdate(250, Dispatchers.IO) {
println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
// adds the time to perform the refresh into this delay // adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines. // holding off new updates in case of heavy refresh routines.
@@ -4,7 +4,6 @@ import android.util.Log
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal
class LnZapEvent( class LnZapEvent(
id: HexKey, id: HexKey,
@@ -14,25 +13,37 @@ class LnZapEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
// This event is also kept in LocalCache (same object)
@Transient val zapRequest: LnZapRequestEvent?
override fun zappedPost() = tags private fun containedPost(): LnZapRequestEvent? = try {
.filter { it.firstOrNull() == "e" } description()?.ifBlank { null }?.let {
.mapNotNull { it.getOrNull(1) } fromJson(it, Client.lenient)
} as? LnZapRequestEvent
override fun zappedPollOption(): Int? = containedPost()?.tags } catch (e: Exception) {
?.filter { it.firstOrNull() == POLL_OPTION } Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
?.getOrNull(0)?.getOrNull(1)?.toInt() null
override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) }
override fun zappedRequestAuthor(): String? = containedPost()?.pubKey()
override fun amount(): BigDecimal? {
return amount
} }
init {
zapRequest = containedPost()
}
override fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun zappedPollOption(): Int? = try {
zapRequest?.tags?.firstOrNull { it.size > 1 && it[0] == POLL_OPTION }?.get(1)?.toInt()
} catch (e: Exception) {
Log.e("LnZapEvent", "ZappedPollOption failed to parse", e)
null
}
override fun zappedRequestAuthor(): String? = zapRequest?.pubKey()
override fun amount() = amount
// Keeps this as a field because it's a heavier function used everywhere. // Keeps this as a field because it's a heavier function used everywhere.
val amount by lazy { val amount by lazy {
try { try {
@@ -42,29 +53,14 @@ class LnZapEvent(
null null
} }
} }
override fun message(): String { override fun message(): String {
return message return content
}
val message = content
override fun containedPost(): Event? = try {
description()?.ifBlank { null }?.let {
fromJson(it, Client.lenient)
}
} catch (e: Exception) {
Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
null
} }
private fun lnInvoice(): String? = tags private fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1)
.filter { it.firstOrNull() == "bolt11" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
private fun description(): String? = tags private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
.filter { it.firstOrNull() == "description" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
companion object { companion object {
const val kind = 9735 const val kind = 9735
@@ -16,7 +16,5 @@ interface LnZapEventInterface : EventInterface {
fun amount(): BigDecimal? fun amount(): BigDecimal?
fun containedPost(): Event?
fun message(): String fun message(): String
} }
@@ -13,8 +13,10 @@ class LnZapRequestEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
companion object { companion object {
const val kind = 9734 const val kind = 9734
@@ -20,24 +20,17 @@ class PollNoteEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun pollOptions(): Map<Int, String> { fun pollOptions() =
val map = mutableMapOf<Int, String>() tags.filter { it.size > 2 && it[0] == POLL_OPTION }
tags.filter { it.first() == POLL_OPTION } .associate { it[1].toInt() to it[2] }
.forEach { map[it[1].toInt()] = it[2] }
return map
}
fun getTagInt(property: String): Int? { fun getTagInt(property: String): Int? {
val tagList = tags.filter { val number = tags.firstOrNull() { it.size > 1 && it[0] == property }?.get(1)
it.firstOrNull() == property
}
val tag = tagList.getOrNull(0)
val s = tag?.getOrNull(1)
return if (s.isNullOrBlank() || s == "null") { return if (number.isNullOrBlank() || number == "null") {
null null
} else { } else {
s.toInt() number.toInt()
} }
} }
@@ -21,7 +21,7 @@ class PrivateDmEvent(
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
* for initial messages. * for initial messages.
*/ */
fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one
/** /**
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
@@ -29,7 +29,7 @@ class PrivateDmEvent(
* Nip-18 messages should refer to other events by inline references in the content like * Nip-18 messages should refer to other events by inline references in the content like
* `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506).
*/ */
fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
return try { return try {
@@ -14,8 +14,8 @@ class ReactionEvent(
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun originalPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
companion object { companion object {
const val kind = 7 const val kind = 7
@@ -30,7 +30,7 @@ class ReportEvent(
} }
fun reportedPost() = tags fun reportedPost() = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } .filter { it.size > 1 && it[0] == "e" }
.map { .map {
ReportedKey( ReportedKey(
it[1], it[1],
@@ -39,7 +39,7 @@ class ReportEvent(
} }
fun reportedAuthor() = tags fun reportedAuthor() = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } .filter { it.size > 1 && it[0] == "p" }
.map { .map {
ReportedKey( ReportedKey(
it[1], it[1],
@@ -9,7 +9,7 @@ object GlobalFeedFilter : FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
override fun feed(): List<Note> { override fun feed(): List<Note> {
val followChannels = account.followingChannels() val followChannels = account.followingChannels
val followUsers = account.followingKeySet() val followUsers = account.followingKeySet()
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
@@ -19,7 +19,7 @@ object GlobalFeedFilter : FeedFilter<Note>() {
it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty() it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty()
} }
.filter { .filter {
val channel = it.channel() val channel = it.channelHex()
// does not show events already in the public chat list // does not show events already in the public chat list
(channel == null || channel !in followChannels) && (channel == null || channel !in followChannels) &&
// does not show people the user already follows // does not show people the user already follows
@@ -38,7 +38,7 @@ object GlobalFeedFilter : FeedFilter<Note>() {
it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty() it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty()
} }
.filter { .filter {
val channel = it.channel() val channel = it.channelHex()
// does not show events already in the public chat list // does not show events already in the public chat list
(channel == null || channel !in followChannels) && (channel == null || channel !in followChannels) &&
// does not show people the user already follows // does not show people the user already follows
@@ -246,30 +246,32 @@ fun ChatroomMessageCompose(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val event = note.event val event = note.event
if (event is ChannelCreateEvent) { if (event is ChannelCreateEvent) {
val channelInfo = event.channelInfo()
Text( Text(
text = note.author?.toBestDisplayName() text = note.author?.toBestDisplayName()
.toString() + " ${stringResource(R.string.created)} " + ( .toString() + " ${stringResource(R.string.created)} " + (
event.channelInfo().name channelInfo.name
?: "" ?: ""
) + " ${stringResource(R.string.with_description_of)} '" + ( ) + " ${stringResource(R.string.with_description_of)} '" + (
event.channelInfo().about channelInfo.about
?: "" ?: ""
) + "', ${stringResource(R.string.and_picture)} '" + ( ) + "', ${stringResource(R.string.and_picture)} '" + (
event.channelInfo().picture channelInfo.picture
?: "" ?: ""
) + "'" ) + "'"
) )
} else if (event is ChannelMetadataEvent) { } else if (event is ChannelMetadataEvent) {
val channelInfo = event.channelInfo()
Text( Text(
text = note.author?.toBestDisplayName() text = note.author?.toBestDisplayName()
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (
event.channelInfo().name channelInfo.name
?: "" ?: ""
) + "$', {stringResource(R.string.description_to)} '" + ( ) + "$', {stringResource(R.string.description_to)} '" + (
event.channelInfo().about channelInfo.about
?: "" ?: ""
) + "', ${stringResource(R.string.and_picture_to)} '" + ( ) + "', ${stringResource(R.string.and_picture_to)} '" + (
event.channelInfo().picture channelInfo.picture
?: "" ?: ""
) + "'" ) + "'"
) )
@@ -26,7 +26,9 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -46,7 +48,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
} else { } else {
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = messageSetCard) { LaunchedEffect(key1 = messageSetCard.createdAt()) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
isNew = isNew =
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead) messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
@@ -64,14 +66,29 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
Column( Column(
modifier = Modifier.background(backgroundColor).combinedClickable( modifier = Modifier.background(backgroundColor).combinedClickable(
onClick = { onClick = {
if (noteEvent !is ChannelMessageEvent) { if (noteEvent is ChannelMessageEvent) {
navController.navigate("Note/${note.idHex}") {
launchSingleTop = true
}
} else {
note.channel()?.let { note.channel()?.let {
navController.navigate("Channel/${it.idHex}") navController.navigate("Channel/${it.idHex}")
} }
} else if (noteEvent is PrivateDmEvent) {
val replyAuthorBase =
(note.event as? PrivateDmEvent)
?.recipientPubKey()
?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!!
if (replyAuthorBase != null) {
if (note.author == accountViewModel.userProfile()) {
userToComposeOn = replyAuthorBase
}
}
navController.navigate("Room/${userToComposeOn.pubkeyHex}")
} else {
navController.navigate("Note/${note.idHex}") {
launchSingleTop = true
}
} }
}, },
onLongClick = { popupExpanded = true } onLongClick = { popupExpanded = true }
@@ -61,7 +61,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
} else { } else {
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = multiSetCard) { LaunchedEffect(key1 = multiSetCard.createdAt()) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead) isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead)
@@ -244,7 +244,7 @@ fun FastNoteAuthorPicture(
val userState by author.live().metadata.observeAsState() val userState by author.live().metadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val showFollowingMark = userAccount.isFollowingCached(user) || user == userAccount val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
UserPicture( UserPicture(
userHex = user.pubkeyHex, userHex = user.pubkeyHex,
@@ -96,7 +96,6 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
user.nip05()?.let { nip05 -> user.nip05()?.let { nip05 ->
if (nip05.split("@").size == 2) { if (nip05.split("@").size == 2) {
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
Column(modifier = columnModifier) { Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (nip05.split("@")[0] != "_") { if (nip05.split("@")[0] != "_") {
@@ -108,6 +107,7 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
) )
} }
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
if (nip05Verified == null) { if (nip05Verified == null) {
Icon( Icon(
tint = Color.Yellow, tint = Color.Yellow,
@@ -96,7 +96,7 @@ fun NoteCompose(
) )
} }
Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}") Log.d("Time", "Note Compose in $elapsed for ${baseNote.idHex} ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -582,7 +582,9 @@ fun DisplayFollowingHashtagsInPost(
var firstTag by remember { mutableStateOf<String?>(null) } var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteEvent) { LaunchedEffect(key1 = noteEvent) {
firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) withContext(Dispatchers.IO) {
firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet())
}
} }
Column() { Column() {
@@ -1108,12 +1110,14 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
} }
LaunchedEffect(key1 = note) { LaunchedEffect(key1 = note) {
state = DropDownParams( withContext(Dispatchers.IO) {
accountViewModel.isFollowing(note.author), state = DropDownParams(
accountViewModel.isInPrivateBookmarks(note), accountViewModel.isFollowing(note.author),
accountViewModel.isInPublicBookmarks(note), accountViewModel.isInPrivateBookmarks(note),
accountViewModel.isLoggedUser(note.author) accountViewModel.isInPublicBookmarks(note),
) accountViewModel.isLoggedUser(note.author)
)
}
} }
DropdownMenu( DropdownMenu(
@@ -48,9 +48,6 @@ fun PollNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note ?: return val zappedNote = zapsState?.note ?: return
@@ -72,7 +69,7 @@ fun PollNote(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 3.dp) modifier = Modifier.padding(vertical = 3.dp)
) { ) {
if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile())) { if (accountViewModel.isLoggedUser(zappedNote.author) || zappedNote.isZappedBy(accountViewModel.userProfile())) {
ZapVote( ZapVote(
baseNote, baseNote,
accountViewModel, accountViewModel,
@@ -90,7 +87,7 @@ fun PollNote(
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
color = color, color = color,
progress = optionTally progress = optionTally.toFloat()
) )
Row( Row(
@@ -101,7 +98,7 @@ fun PollNote(
modifier = Modifier.padding(horizontal = 10.dp).width(40.dp) modifier = Modifier.padding(horizontal = 10.dp).width(40.dp)
) { ) {
Text( Text(
text = "${(optionTally * 100).roundToInt()}%", text = "${(optionTally.toFloat() * 100).roundToInt()}%",
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
@@ -210,7 +207,7 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (zappedNote?.author == account.userProfile()) { } else if (accountViewModel.isLoggedUser(zappedNote?.author)) {
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode
import java.util.* import java.util.*
class PollNoteViewModel { class PollNoteViewModel {
@@ -16,7 +17,9 @@ class PollNoteViewModel {
var valueMaximum: Int? = null var valueMaximum: Int? = null
var valueMinimum: Int? = null var valueMinimum: Int? = null
private var closedAt: Int? = null private var closedAt: Int? = null
var consensusThreshold: Float? = null var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO
fun load(note: Note?) { fun load(note: Note?) {
pollNote = note pollNote = note
@@ -24,8 +27,10 @@ class PollNoteViewModel {
pollOptions = pollEvent?.pollOptions() pollOptions = pollEvent?.pollOptions()
valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM)
valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM)
consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100) consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
closedAt = pollEvent?.getTagInt(CLOSED_AT) closedAt = pollEvent?.getTagInt(CLOSED_AT)
totalZapped = totalZapped()
} }
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -73,47 +78,39 @@ class PollNoteViewModel {
return false return false
} }
fun optionVoteTally(op: Int): Float { fun optionVoteTally(op: Int): BigDecimal {
val tally = zappedPollOptionAmount(op).toFloat().div(zappedVoteTotal()) return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
return if (tally.isNaN()) { // catch div by 0 zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP)
0f } else {
} else { tally } BigDecimal.ZERO
}
private fun zappedVoteTotal(): Float {
var total = 0f
pollOptions?.keys?.forEach {
total += zappedPollOptionAmount(it).toFloat()
} }
return total
} }
fun isPollOptionZappedBy(option: Int, user: User): Boolean { fun isPollOptionZappedBy(option: Int, user: User): Boolean {
if (pollNote?.zaps?.any { it.key.author == user } == true) { if (pollNote?.zaps?.any { it.key.author === user } == true) {
pollNote!!.zaps.mapNotNull { it.value?.event } pollNote!!.zaps
.filterIsInstance<LnZapEvent>() .any {
.map { val event = it.value?.event as? LnZapEvent
val zappedOption = it.zappedPollOption() event?.zappedPollOption() == option && event.zappedRequestAuthor() == user.pubkeyHex
if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex) {
return true
}
} }
} }
return false return false
} }
fun zappedPollOptionAmount(option: Int): BigDecimal { fun zappedPollOptionAmount(option: Int): BigDecimal {
return if (pollNote != null) { return pollNote?.zaps?.values?.sumOf {
pollNote!!.zaps.mapNotNull { it.value?.event } val event = it?.event as? LnZapEvent
.filterIsInstance<LnZapEvent>() if (event?.zappedPollOption() == option) {
.mapNotNull { event.amount ?: BigDecimal(0)
val zappedOption = it.zappedPollOption() } else {
if (zappedOption == option) { BigDecimal(0)
it.amount }
} else { null } } ?: BigDecimal(0)
}.sumOf { it } }
} else {
BigDecimal(0) fun totalZapped(): BigDecimal {
} return pollNote?.zaps?.values?.sumOf {
(it?.event as? LnZapEvent)?.amount ?: BigDecimal(0)
} ?: BigDecimal(0)
} }
} }
@@ -43,7 +43,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = zapSetCard) { LaunchedEffect(key1 = zapSetCard.createdAt()) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead) isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead)
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.activity.compose.BackHandler
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -19,6 +20,7 @@ import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@@ -34,10 +36,12 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.currentRoute import com.vitorpamplona.amethyst.ui.navigation.currentRoute
import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountState
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) {
val coroutineScope = rememberCoroutineScope()
val navController = rememberNavController() val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
@@ -64,6 +68,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}, },
drawerContent = { drawerContent = {
DrawerContent(navController, scaffoldState, sheetState, accountViewModel) DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
BackHandler(enabled = scaffoldState.drawerState.isOpen) {
coroutineScope.launch { scaffoldState.drawerState.close() }
}
}, },
floatingActionButton = { floatingActionButton = {
FloatingButtons(navController, accountStateViewModel) FloatingButtons(navController, accountStateViewModel)
+21
View File
@@ -263,9 +263,30 @@
<string name="wallet_connect_service_secret_placeholder">nsec / hex privát kulcs</string> <string name="wallet_connect_service_secret_placeholder">nsec / hex privát kulcs</string>
<string name="pledge_amount_in_sats">Hozzájárulás összege sats-ban</string> <string name="pledge_amount_in_sats">Hozzájárulás összege sats-ban</string>
<string name="post_poll">Szavazás Létrehozása</string>
<string name="poll_heading_required">Szükséges mezők:</string>
<string name="poll_zap_recipients">Zap-et kapják</string>
<string name="poll_primary_description">Szavazás elsődleges leírása…</string>
<string name="poll_option_index">Szavazás %s megoszlása</string>
<string name="poll_option_description">Szavazás opcióinak leírása</string>
<string name="poll_heading_optional">Kiegészítő mezők:</string>
<string name="poll_zap_value_min">Zap minimum</string>
<string name="poll_zap_value_max">Zap maximum</string>
<string name="poll_consensus_threshold">Konszenzus</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Szavazás lezárása</string>
<string name="poll_closing_time_days">napok</string>
<string name="poll_is_closed">A szavazásra nem lehet már új szavazatot leadni</string>
<string name="poll_zap_amount">Zap összege</string>
<string name="one_vote_per_user_on_atomic_votes">Az ilyen típusú szavazásokon felhasználónként csak egy szavazat engedélyezett</string>
<string name="looking_for_event">"%1$s esemény keresése"</string> <string name="looking_for_event">"%1$s esemény keresése"</string>
<string name="custom_zaps_add_a_message">Nyilvános üzenet hozzáadása</string> <string name="custom_zaps_add_a_message">Nyilvános üzenet hozzáadása</string>
<string name="custom_zaps_add_a_message_example">Köszönöm a kemény munkát!</string> <string name="custom_zaps_add_a_message_example">Köszönöm a kemény munkát!</string>
<string name="lightning_create_and_add_invoice">Létrehoz és Hozzáad</string>
<string name="poll_author_no_vote">A szavazás létrehozója sajátjára nem szavazhat.</string>
<string name="poll_hashtag" translatable="false">#zappoll</string>
</resources> </resources>
+2 -2
View File
@@ -10,8 +10,8 @@ buildscript {
} }
}// Top-level build file where you can add configuration options common to all sub-projects/modules. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.4.2' apply false id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '7.4.2' apply false id 'com.android.library' version '8.0.0' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
} }
+3 -1
View File
@@ -21,4 +21,6 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.enableR8.fullMode=true android.enableR8.fullMode=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
+1 -1
View File
@@ -1,6 +1,6 @@
#Wed Jan 04 09:23:50 EST 2023 #Wed Jan 04 09:23:50 EST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME