From 4589e6f3290ae291dabd17e6161a1bbbb7262131 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 23 Mar 2023 10:49:01 -0400 Subject: [PATCH] Adding Bounty information to Note --- .../com/vitorpamplona/amethyst/model/Note.kt | 14 ++ .../amethyst/service/model/Event.kt | 9 ++ .../amethyst/service/model/EventInterface.kt | 3 + .../ui/note/NIP05VerificationDisplay.kt | 95 ++++++------ .../amethyst/ui/note/NoteCompose.kt | 146 ++++++++++++++---- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/screen/ThreadFeedView.kt | 16 +- 7 files changed, 213 insertions(+), 76 deletions(-) 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 150880f764..4a29fd40bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -235,6 +235,20 @@ open class Note(val idHex: String) { }.sumOf { it } } + fun pledgedAmountByOthers(): BigDecimal { + return replies + .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } + .mapNotNull { + try { + BigDecimal(it.event?.content()) + } catch (e: Exception) { + null + // do nothing if it can't convert to bigdecimal + } + } + .sumOf { it } + } + fun hasAnyReports(): Boolean { val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 0cf58ef082..d5594a068c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -9,6 +9,7 @@ import fr.acinq.secp256k1.Secp256k1 import nostr.postr.Utils import nostr.postr.toHex import java.lang.reflect.Type +import java.math.BigDecimal import java.security.MessageDigest import java.util.* @@ -55,6 +56,14 @@ open class Event( override fun isTaggedHashes(hashtags: Set) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags } override fun firstIsTaggedHashes(hashtags: Set) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1) + override fun getReward(): BigDecimal? { + return try { + tags.filter { it.firstOrNull() == "reward" }.mapNotNull { BigDecimal(it.getOrNull(1)) }.firstOrNull() + } catch (e: Exception) { + null + } + } + /** * Checks if the ID is correct and then if the pubKey's secret key signed the event. */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 7552058486..f91027df6d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey +import java.math.BigDecimal interface EventInterface { fun id(): HexKey @@ -29,4 +30,6 @@ interface EventInterface { fun isTaggedHashes(hashtag: Set): Boolean fun firstIsTaggedHashes(hashtag: Set): String? fun hashtags(): List + + fun getReward(): BigDecimal? } 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 e5e538c985..53c5702141 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 @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -76,18 +77,18 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State if (nip05.split("@").size == 2) { val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) - Row(verticalAlignment = Alignment.CenterVertically) { - if (nip05.split("@")[0] != "_") { - Text( - text = AnnotatedString(nip05.split("@")[0]), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + Column(modifier = columnModifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (nip05.split("@")[0] != "_") { + Text( + text = AnnotatedString(nip05.split("@")[0]), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (nip05Verified == null) { + Icon( + tint = Color.Yellow, + imageVector = Icons.Default.Downloading, + contentDescription = "Downloading", + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } else if (nip05Verified == true) { + Icon( + painter = painterResource(R.drawable.ic_verified), + "NIP-05 Verified", + tint = Nip05.copy(0.52f), + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } else { + Icon( + tint = Color.Red, + imageVector = Icons.Default.Report, + contentDescription = "Invalid Nip05", + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } + + ClickableText( + text = AnnotatedString(nip05.split("@")[1]), + onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)), maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Visible ) } - - if (nip05Verified == null) { - Icon( - tint = Color.Yellow, - imageVector = Icons.Default.Downloading, - contentDescription = "Downloading", - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } else if (nip05Verified == true) { - Icon( - painter = painterResource(R.drawable.ic_verified), - "NIP-05 Verified", - tint = Nip05.copy(0.52f), - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } else { - Icon( - tint = Color.Red, - imageVector = Icons.Default.Report, - contentDescription = "Invalid Nip05", - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } - - ClickableText( - text = AnnotatedString(nip05.split("@")[1]), - onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)), - maxLines = 1, - overflow = TextOverflow.Visible - ) } } } 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 6d962f3d2a..af9a25b52b 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 @@ -10,8 +10,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Bolt import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -42,6 +44,7 @@ import coil.compose.AsyncImage import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -50,6 +53,7 @@ import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent 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.EventInterface import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent @@ -65,9 +69,11 @@ import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Following import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.math.BigDecimal @OptIn(ExperimentalFoundationApi::class) @Composable @@ -273,15 +279,8 @@ fun NoteCompose( fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) - } - - val firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) - if (firstTag != null) { - ClickableText( - text = AnnotatedString(" #$firstTag"), - onClick = { navController.navigate("Hashtag/$firstTag") }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) - ) + } else { + DisplayFollowingHashtagsInPost(noteEvent, account, navController) } Text( @@ -306,7 +305,14 @@ fun NoteCompose( } if (note.author != null && !makeItShort && !isQuotedNote) { - ObserveDisplayNip05Status(note.author!!) + Row(verticalAlignment = Alignment.CenterVertically) { + ObserveDisplayNip05Status(note.author!!, Modifier.weight(1f)) + + val baseReward = noteEvent.getReward() + if (baseReward != null) { + DisplayReward(baseReward, baseNote, navController) + } + } } Spacer(modifier = Modifier.height(3.dp)) @@ -483,24 +489,7 @@ fun NoteCompose( navController ) - val hashtags = noteEvent.hashtags() - if (hashtags.isNotEmpty()) { - FlowRow() { - hashtags.forEach { - if (!eventContent.contains(it, true)) { - ClickableText( - text = AnnotatedString("#$it "), - onClick = { navController.navigate("Hashtag/$it") }, - style = LocalTextStyle.current.copy( - color = MaterialTheme.colors.primary.copy( - alpha = 0.52f - ) - ) - ) - } - } - } - } + DisplayUncitedHashtags(noteEvent, eventContent, navController) } if (!makeItShort) { @@ -520,6 +509,107 @@ fun NoteCompose( } } +@Composable +fun DisplayFollowingHashtagsInPost( + noteEvent: EventInterface, + account: Account, + navController: NavController +) { + Column() { + Row(verticalAlignment = Alignment.CenterVertically) { + val firstTag = + noteEvent.firstIsTaggedHashes(account.followingTagSet()) + if (firstTag != null) { + ClickableText( + text = AnnotatedString(" #$firstTag"), + onClick = { navController.navigate("Hashtag/$firstTag") }, + style = LocalTextStyle.current.copy( + color = MaterialTheme.colors.primary.copy( + alpha = 0.52f + ) + ) + ) + } + } + } +} + +@Composable +fun DisplayUncitedHashtags( + noteEvent: EventInterface, + eventContent: String, + navController: NavController +) { + val hashtags = noteEvent.hashtags() + if (hashtags.isNotEmpty()) { + FlowRow() { + hashtags.forEach { + if (!eventContent.contains(it, true)) { + ClickableText( + text = AnnotatedString("#$it "), + onClick = { navController.navigate("Hashtag/$it") }, + style = LocalTextStyle.current.copy( + color = MaterialTheme.colors.primary.copy( + alpha = 0.52f + ) + ) + ) + } + } + } + } +} + +@Composable +fun DisplayReward( + baseReward: BigDecimal, + baseNote: Note, + navController: NavController +) { + Column() { + Row(verticalAlignment = Alignment.CenterVertically) { + ClickableText( + text = AnnotatedString("#bounty"), + onClick = { navController.navigate("Hashtag/bounty") }, + style = LocalTextStyle.current.copy( + color = MaterialTheme.colors.primary.copy( + alpha = 0.52f + ) + ) + ) + + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(R.string.zaps), + modifier = Modifier.size(20.dp), + tint = BitcoinOrange + ) + + val repliesState by baseNote.live().replies.observeAsState() + val replyNote = repliesState?.note + + var rewardAmount by remember { + mutableStateOf( + baseReward + ) + } + + LaunchedEffect(key1 = repliesState) { + withContext(Dispatchers.IO) { + replyNote?.pledgedAmountByOthers()?.let { + rewardAmount = baseReward.add(it) + } + } + } + + Text( + showAmount(rewardAmount), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + } +} + @Composable fun BadgeDisplay(baseNote: Note) { val background = MaterialTheme.colors.background diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 1a6819e425..b6a8a54964 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -79,7 +79,11 @@ private fun lightenColor(color: Color, amount: Float): Color { val externalLinkForNote = { note: Note -> if (note is AddressableNote) { - "https://habla.news/a/${note.address().toNAddr()}" + if (note.event?.getReward() != null) { + "https://nostrbounties.com/b/${note.address().toNAddr()}" + } else { + "https://habla.news/a/${note.address().toNAddr()}" + } } else { "https://snort.social/e/${note.idNote()}" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index d7173f8199..138952bd92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -57,6 +57,9 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.note.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.BlankNote +import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost +import com.vitorpamplona.amethyst.ui.note.DisplayReward +import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose @@ -250,6 +253,8 @@ fun NoteMaster( Row(verticalAlignment = Alignment.CenterVertically) { NoteUsernameDisplay(baseNote, Modifier.weight(1f)) + DisplayFollowingHashtagsInPost(noteEvent, account, navController) + Text( timeAgo(note.createdAt(), context = context), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), @@ -271,7 +276,14 @@ fun NoteMaster( } } - ObserveDisplayNip05Status(baseNote) + Row(verticalAlignment = Alignment.CenterVertically) { + ObserveDisplayNip05Status(baseNote, Modifier.weight(1f)) + + val baseReward = noteEvent.getReward() + if (baseReward != null) { + DisplayReward(baseReward, baseNote, navController) + } + } } } @@ -341,6 +353,8 @@ fun NoteMaster( accountViewModel, navController ) + + DisplayUncitedHashtags(noteEvent, eventContent, navController) } ReactionsRow(note, accountViewModel)