diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 178e5f264..5a36d7297 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -54,8 +54,14 @@ class Account( val liveLanguages: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this) + var userProfileCache: User? = null + 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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index cef63d463..1705175d5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -595,7 +595,7 @@ object LocalCache { // Already processed this event. 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 mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index f12698a33..7bf4537f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -53,13 +53,14 @@ open class Note(val idHex: String) { open fun idNote() = id().toNote() open fun idDisplayNote() = idNote().toShortenHex() - fun channel(): Channel? { - val channelHex = - (event as? ChannelMessageEvent)?.channel() - ?: (event as? ChannelMetadataEvent)?.channel() - ?: (event as? ChannelCreateEvent)?.id + fun channelHex(): HexKey? { + return (event as? ChannelMessageEvent)?.channel() + ?: (event as? ChannelMetadataEvent)?.channel() + ?: (event as? ChannelCreateEvent)?.id + } - return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } + fun channel(): Channel? { + return channelHex()?.let { LocalCache.checkGetOrCreateChannel(it) } } open fun address(): ATag? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 755fbf607..6d4fbadf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -76,7 +76,7 @@ abstract class NostrDataSource(val debugName: String) { is DeletionEvent -> LocalCache.consume(event) is LnZapEvent -> { - event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + event.zapRequest?.let { onEvent(it, subscriptionId, relay) } LocalCache.consume(event) } is LnZapRequestEvent -> LocalCache.consume(event) @@ -154,7 +154,7 @@ abstract class NostrDataSource(val debugName: String) { // Refreshes observers in batches. 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 // holding off new updates in case of heavy refresh routines. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 1812fb803..5f08c6a6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -4,7 +4,6 @@ import android.util.Log import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal class LnZapEvent( id: HexKey, @@ -14,25 +13,37 @@ class LnZapEvent( content: String, sig: HexKey ) : 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 - .filter { it.firstOrNull() == "e" } - .mapNotNull { it.getOrNull(1) } - - override fun zappedPollOption(): Int? = containedPost()?.tags - ?.filter { it.firstOrNull() == POLL_OPTION } - ?.getOrNull(0)?.getOrNull(1)?.toInt() - - override fun zappedAuthor() = tags - .filter { it.firstOrNull() == "p" } - .mapNotNull { it.getOrNull(1) } - - override fun zappedRequestAuthor(): String? = containedPost()?.pubKey() - - override fun amount(): BigDecimal? { - return amount + private fun containedPost(): LnZapRequestEvent? = try { + description()?.ifBlank { null }?.let { + fromJson(it, Client.lenient) + } as? LnZapRequestEvent + } catch (e: Exception) { + Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e) + null } + 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. val amount by lazy { try { @@ -42,29 +53,14 @@ class LnZapEvent( null } } + override fun message(): String { - return message - } - 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 + return content } - private fun lnInvoice(): String? = tags - .filter { it.firstOrNull() == "bolt11" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1) - private fun description(): String? = tags - .filter { it.firstOrNull() == "description" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) companion object { const val kind = 9735 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index b69e02781..ada0d609d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -16,7 +16,5 @@ interface LnZapEventInterface : EventInterface { fun amount(): BigDecimal? - fun containedPost(): Event? - fun message(): String } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 34fddc02a..eba7a18a9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -13,8 +13,10 @@ class LnZapRequestEvent( content: String, sig: HexKey ) : 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 { const val kind = 9734 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index a6bcc89b8..c91cd4c7a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -20,24 +20,17 @@ class PollNoteEvent( content: String, sig: HexKey ) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun pollOptions(): Map { - val map = mutableMapOf() - tags.filter { it.first() == POLL_OPTION } - .forEach { map[it[1].toInt()] = it[2] } - return map - } + fun pollOptions() = + tags.filter { it.size > 2 && it[0] == POLL_OPTION } + .associate { it[1].toInt() to it[2] } fun getTagInt(property: String): Int? { - val tagList = tags.filter { - it.firstOrNull() == property - } - val tag = tagList.getOrNull(0) - val s = tag?.getOrNull(1) + val number = tags.firstOrNull() { it.size > 1 && it[0] == property }?.get(1) - return if (s.isNullOrBlank() || s == "null") { + return if (number.isNullOrBlank() || number == "null") { null } else { - s.toInt() + number.toInt() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt index 89e1d98e6..c820d0ca2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -21,7 +21,7 @@ class PrivateDmEvent( * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used * 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. @@ -29,7 +29,7 @@ class PrivateDmEvent( * Nip-18 messages should refer to other events by inline references in the content like * `[](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? { return try { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index f89bb2977..13188520c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -14,8 +14,8 @@ class ReactionEvent( sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun originalPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + fun originalAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } companion object { const val kind = 7 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 42f33c3c5..d3bd9dd97 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -30,7 +30,7 @@ class ReportEvent( } fun reportedPost() = tags - .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } + .filter { it.size > 1 && it[0] == "e" } .map { ReportedKey( it[1], @@ -39,7 +39,7 @@ class ReportEvent( } fun reportedAuthor() = tags - .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } + .filter { it.size > 1 && it[0] == "p" } .map { ReportedKey( it[1], diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index e902c2f98..bd441642a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -9,7 +9,7 @@ object GlobalFeedFilter : FeedFilter() { lateinit var account: Account override fun feed(): List { - val followChannels = account.followingChannels() + val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 @@ -19,7 +19,7 @@ object GlobalFeedFilter : FeedFilter() { it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { - val channel = it.channel() + val channel = it.channelHex() // does not show events already in the public chat list (channel == null || channel !in followChannels) && // does not show people the user already follows @@ -38,7 +38,7 @@ object GlobalFeedFilter : FeedFilter() { it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { - val channel = it.channel() + val channel = it.channelHex() // does not show events already in the public chat list (channel == null || channel !in followChannels) && // does not show people the user already follows diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index fee45ca0e..bee998015 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -246,30 +246,32 @@ fun ChatroomMessageCompose( Row(verticalAlignment = Alignment.CenterVertically) { val event = note.event if (event is ChannelCreateEvent) { + val channelInfo = event.channelInfo() Text( text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.created)} " + ( - event.channelInfo().name + channelInfo.name ?: "" ) + " ${stringResource(R.string.with_description_of)} '" + ( - event.channelInfo().about + channelInfo.about ?: "" ) + "', ${stringResource(R.string.and_picture)} '" + ( - event.channelInfo().picture + channelInfo.picture ?: "" ) + "'" ) } else if (event is ChannelMetadataEvent) { + val channelInfo = event.channelInfo() Text( text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( - event.channelInfo().name + channelInfo.name ?: "" ) + "$', {stringResource(R.string.description_to)} '" + ( - event.channelInfo().about + channelInfo.about ?: "" ) + "', ${stringResource(R.string.and_picture_to)} '" + ( - event.channelInfo().picture + channelInfo.picture ?: "" ) + "'" ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index bf0fa2cdb..8deb5bcb1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -26,7 +26,9 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache 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.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers @@ -46,7 +48,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal } else { var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = messageSetCard) { + LaunchedEffect(key1 = messageSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = messageSetCard.createdAt() > NotificationCache.load(routeForLastRead) @@ -64,14 +66,29 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal Column( modifier = Modifier.background(backgroundColor).combinedClickable( onClick = { - if (noteEvent !is ChannelMessageEvent) { - navController.navigate("Note/${note.idHex}") { - launchSingleTop = true - } - } else { + if (noteEvent is ChannelMessageEvent) { note.channel()?.let { 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 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index b001cce84..537649905 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -61,7 +61,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun } else { var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = multiSetCard) { + LaunchedEffect(key1 = multiSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead) @@ -244,7 +244,7 @@ fun FastNoteAuthorPicture( val userState by author.live().metadata.observeAsState() val user = userState?.user ?: return - val showFollowingMark = userAccount.isFollowingCached(user) || user == userAccount + val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount UserPicture( userHex = user.pubkeyHex, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 53c570214..cebfe1fdd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -96,7 +96,6 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie user.nip05()?.let { nip05 -> if (nip05.split("@").size == 2) { - val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) Column(modifier = columnModifier) { Row(verticalAlignment = Alignment.CenterVertically) { 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) { Icon( tint = Color.Yellow, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index be5075c25..8400d174b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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) @@ -582,7 +582,9 @@ fun DisplayFollowingHashtagsInPost( var firstTag by remember { mutableStateOf(null) } LaunchedEffect(key1 = noteEvent) { - firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) + withContext(Dispatchers.IO) { + firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) + } } Column() { @@ -1108,12 +1110,14 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, } LaunchedEffect(key1 = note) { - state = DropDownParams( - accountViewModel.isFollowing(note.author), - accountViewModel.isInPrivateBookmarks(note), - accountViewModel.isInPublicBookmarks(note), - accountViewModel.isLoggedUser(note.author) - ) + withContext(Dispatchers.IO) { + state = DropDownParams( + accountViewModel.isFollowing(note.author), + accountViewModel.isInPrivateBookmarks(note), + accountViewModel.isInPublicBookmarks(note), + accountViewModel.isLoggedUser(note.author) + ) + } } DropdownMenu( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index da0f35936..26e83cf91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -48,9 +48,6 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - val zapsState by baseNote.live().zaps.observeAsState() val zappedNote = zapsState?.note ?: return @@ -72,7 +69,7 @@ fun PollNote( verticalAlignment = Alignment.CenterVertically, 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( baseNote, accountViewModel, @@ -90,7 +87,7 @@ fun PollNote( LinearProgressIndicator( modifier = Modifier.matchParentSize(), color = color, - progress = optionTally + progress = optionTally.toFloat() ) Row( @@ -101,7 +98,7 @@ fun PollNote( modifier = Modifier.padding(horizontal = 10.dp).width(40.dp) ) { Text( - text = "${(optionTally * 100).roundToInt()}%", + text = "${(optionTally.toFloat() * 100).roundToInt()}%", fontWeight = FontWeight.Bold ) } @@ -210,7 +207,7 @@ fun ZapVote( ) .show() } - } else if (zappedNote?.author == account.userProfile()) { + } else if (accountViewModel.isLoggedUser(zappedNote?.author)) { scope.launch { Toast .makeText( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index e5d3d4cd8..4ce21c372 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.* import java.math.BigDecimal +import java.math.RoundingMode import java.util.* class PollNoteViewModel { @@ -16,7 +17,9 @@ class PollNoteViewModel { var valueMaximum: Int? = null var valueMinimum: Int? = null private var closedAt: Int? = null - var consensusThreshold: Float? = null + var consensusThreshold: BigDecimal? = null + + var totalZapped: BigDecimal = BigDecimal.ZERO fun load(note: Note?) { pollNote = note @@ -24,8 +27,10 @@ class PollNoteViewModel { pollOptions = pollEvent?.pollOptions() valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) 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) + + totalZapped = totalZapped() } fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum @@ -73,47 +78,39 @@ class PollNoteViewModel { 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() + fun optionVoteTally(op: Int): BigDecimal { + return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { + zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP) + } else { + BigDecimal.ZERO } - 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() - .map { - val zappedOption = it.zappedPollOption() - if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex) { - return true - } + if (pollNote?.zaps?.any { it.key.author === user } == true) { + pollNote!!.zaps + .any { + val event = it.value?.event as? LnZapEvent + event?.zappedPollOption() == option && event.zappedRequestAuthor() == user.pubkeyHex } } return false } fun zappedPollOptionAmount(option: Int): BigDecimal { - return if (pollNote != null) { - pollNote!!.zaps.mapNotNull { it.value?.event } - .filterIsInstance() - .mapNotNull { - val zappedOption = it.zappedPollOption() - if (zappedOption == option) { - it.amount - } else { null } - }.sumOf { it } - } else { - BigDecimal(0) - } + return pollNote?.zaps?.values?.sumOf { + val event = it?.event as? LnZapEvent + if (event?.zappedPollOption() == option) { + event.amount ?: BigDecimal(0) + } else { + BigDecimal(0) + } + } ?: BigDecimal(0) + } + + fun totalZapped(): BigDecimal { + return pollNote?.zaps?.values?.sumOf { + (it?.event as? LnZapEvent)?.amount ?: BigDecimal(0) + } ?: BigDecimal(0) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index f9512cde8..7d0094295 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -43,7 +43,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = zapSetCard) { + LaunchedEffect(key1 = zapSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index e0293b5d2..49486609d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -19,6 +20,7 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.navigation.NavHostController 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.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { + val coroutineScope = rememberCoroutineScope() val navController = rememberNavController() val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) val sheetState = rememberModalBottomSheetState( @@ -64,6 +68,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun }, drawerContent = { DrawerContent(navController, scaffoldState, sheetState, accountViewModel) + BackHandler(enabled = scaffoldState.drawerState.isOpen) { + coroutineScope.launch { scaffoldState.drawerState.close() } + } }, floatingActionButton = { FloatingButtons(navController, accountStateViewModel) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 81296d243..dc5073f2a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -263,9 +263,30 @@ nsec / hex privát kulcs Hozzájárulás összege sats-ban + Szavazás Létrehozása + Szükséges mezők: + Zap-et kapják + Szavazás elsődleges leírása… + Szavazás %s megoszlása + Szavazás opcióinak leírása + Kiegészítő mezők: + Zap minimum + Zap maximum + Konszenzus + (0–100)% + Szavazás lezárása + napok + A szavazásra nem lehet már új szavazatot leadni + Zap összege + Az ilyen típusú szavazásokon felhasználónként csak egy szavazat engedélyezett + "%1$s esemény keresése" Nyilvános üzenet hozzáadása Köszönöm a kemény munkát! + + Létrehoz és Hozzáad + A szavazás létrehozója sajátjára nem szavazhat. + #zappoll diff --git a/build.gradle b/build.gradle index ee67b5b0c..b44afca7a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ buildscript { } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.0.0' 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.jvm' version '1.8.10' apply false } diff --git a/gradle.properties b/gradle.properties index c9ecf2f95..e4e68fdb1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.enableR8.fullMode=true \ No newline at end of file +android.enableR8.fullMode=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1070955d..289cef284 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jan 04 09:23:50 EST 2023 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 zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME