Moves LnInvoice, Tags, BechLinks and Previews to LaunchedEffect

This commit is contained in:
Vitor Pamplona 2023-04-07 16:58:25 -04:00
parent e1ce638d7f
commit dbf5267c5c
5 changed files with 233 additions and 162 deletions

View File

@ -5,10 +5,34 @@ import android.net.Uri
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDirection
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun MayBeWithdrawal(lnurlWord: String) {
var lnWithdrawal by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = lnurlWord) {
withContext(Dispatchers.IO) {
lnWithdrawal = LnWithdrawalUtil.findWithdrawal(lnurlWord)
}
}
lnWithdrawal?.let {
ClickableWithdrawal(withdrawalString = it)
}
?: Text(
text = "$lnurlWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@Composable
fun ClickableWithdrawal(withdrawalString: String) {

View File

@ -9,13 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -24,22 +19,48 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.text.NumberFormat
@Composable
fun InvoicePreview(lnInvoice: String) {
val amount = try {
LnInvoiceUtil.getAmountInSats(lnInvoice)
fun MayBeInvoicePreview(lnbcWord: String) {
var lnInvoice by remember { mutableStateOf<Pair<String, BigDecimal?>?>(null) }
LaunchedEffect(key1 = lnbcWord) {
withContext(Dispatchers.IO) {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
if (myInvoice != null) {
val myInvoiceAmount = try {
LnInvoiceUtil.getAmountInSats(myInvoice)
} catch (e: Exception) {
e.printStackTrace()
null
}
lnInvoice = Pair(myInvoice, myInvoiceAmount)
}
}
}
lnInvoice?.let {
InvoicePreview(it.first, it.second)
}
?: Text(
text = "$lnbcWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@Composable
fun InvoicePreview(lnInvoice: String, amount: BigDecimal?) {
val context = LocalContext.current
Column(

View File

@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.Patterns
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
@ -14,7 +13,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@ -34,12 +33,15 @@ import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
@ -114,7 +116,7 @@ fun RichTextViewer(
)
)
Column(modifier = modifier.animateContentSize()) {
Column(modifier = modifier) {
if (content.startsWith("# ") ||
content.contains("##") ||
content.contains("**") ||
@ -164,25 +166,9 @@ fun RichTextViewer(
UrlPreview(word, "$word ")
}
} else if (word.startsWith("lnbc", true)) {
val lnInvoice = LnInvoiceUtil.findInvoice(word)
if (lnInvoice != null) {
InvoicePreview(lnInvoice)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
MayBeInvoicePreview(word)
} else if (word.startsWith("lnurl", true)) {
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
if (lnWithdrawal != null) {
ClickableWithdrawal(withdrawalString = lnWithdrawal)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
MayBeWithdrawal(word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
@ -304,17 +290,27 @@ fun isBechLink(word: String): Boolean {
@Composable
fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val nip19Route = Nip19.uriToRoute(word)
var nip19Route by remember { mutableStateOf<Nip19.Return?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
if (nip19Route == null) {
Text(text = "$word ")
LaunchedEffect(key1 = word) {
withContext(Dispatchers.IO) {
Nip19.uriToRoute(word)?.let {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNotePair = Pair(note, it.additionalChars)
}
} else {
if (nip19Route.type == Nip19.Type.NOTE || nip19Route.type == Nip19.Type.EVENT || nip19Route.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateNote(nip19Route.hex)
if (note != null) {
nip19Route = nip19Route
}
}
}
}
if (canPreview) {
baseNotePair?.let {
NoteCompose(
baseNote = note,
baseNote = it.first,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
@ -330,23 +326,30 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV
isQuotedNote = true,
navController = navController
)
} else {
ClickableRoute(nip19Route, navController)
Text(
"${it.second} "
)
} ?: nip19Route?.let {
ClickableRoute(it, navController)
}
?: Text(text = "$word ")
} else {
ClickableRoute(nip19Route, navController)
}
} else {
ClickableRoute(nip19Route, navController)
nip19Route?.let {
ClickableRoute(it, navController)
}
?: Text(text = "$word ")
}
}
@Composable
fun HashTag(word: String, accountViewModel: AccountViewModel, navController: NavController) {
var tagSuffixPair by remember { mutableStateOf<Pair<String, String?>?>(null) }
LaunchedEffect(key1 = word) {
withContext(Dispatchers.IO) {
val hashtagMatcher = hashTagsPattern.matcher(word)
val (tag, suffix) = try {
val (myTag, mySuffix) = try {
hashtagMatcher.find()
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
} catch (e: Exception) {
@ -354,17 +357,23 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav
Pair(null, null)
}
if (tag != null) {
val hashtagIcon = checkForHashtagWithIcon(tag)
if (myTag != null) {
tagSuffixPair = Pair(myTag, mySuffix)
}
}
}
tagSuffixPair?.let { tagPair ->
val hashtagIcon = checkForHashtagWithIcon(tagPair.first)
ClickableText(
text = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("#$tag")
append("#${tagPair.first}")
}
},
onClick = { navController.navigate("Hashtag/$tag") }
onClick = { navController.navigate("Hashtag/${tagPair.first}") }
)
if (hashtagIcon != null) {
@ -387,7 +396,6 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {
if (hashtagIcon != null) {
Icon(
painter = painterResource(hashtagIcon.icon),
contentDescription = hashtagIcon.description,
@ -395,7 +403,6 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav
modifier = hashtagIcon.modifier
)
}
}
)
)
@ -405,17 +412,21 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav
inlineContent = inlineContent
)
}
Text(text = "$suffix ")
} else {
Text(text = "$word ")
tagPair.second?.ifBlank { null }?.let {
Text(text = "$it ")
}
} ?: Text(text = "$word ")
}
@Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val matcher = tagIndex.matcher(word)
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
val (index, extraCharacters) = try {
LaunchedEffect(key1 = word) {
withContext(Dispatchers.IO) {
val matcher = tagIndex.matcher(word)
val (index, suffix) = try {
matcher.find()
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
} catch (e: Exception) {
@ -423,26 +434,33 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
Pair(null, null)
}
if (index == null) {
return Text(text = "$word ")
if (index != null && index >= 0 && index < tags.size) {
val tag = tags[index]
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tags[index][1])?.let {
baseUserPair = Pair(it, suffix)
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tags[index][1])?.let {
baseNotePair = Pair(it, suffix)
}
}
}
}
}
}
if (index >= 0 && index < tags.size) {
if (tags[index][0] == "p") {
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
if (baseUser != null) {
ClickableUserTag(baseUser, navController)
Text(text = "$extraCharacters ")
} else {
// if here the tag is not a valid Nostr Hex
Text(text = "$word ")
baseUserPair?.let {
ClickableUserTag(it.first, navController)
Text(text = "${it.second} ")
}
} else if (tags[index][0] == "e") {
val note = LocalCache.checkGetOrCreateNote(tags[index][1])
if (note != null) {
baseNotePair?.let {
if (canPreview) {
NoteCompose(
baseNote = note,
baseNote = it.first,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
@ -458,16 +476,16 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
isQuotedNote = true,
navController = navController
)
} else {
ClickableNoteTag(note, navController)
Text(text = "$extraCharacters ")
it.second?.ifBlank { null }?.let {
Text(text = "$it ")
}
} else {
// if here the tag is not a valid Nostr Hex
ClickableNoteTag(it.first, navController)
Text(text = "${it.second} ")
}
}
if (baseNotePair == null && baseUserPair == null) {
Text(text = "$word ")
}
} else {
Text(text = "$word ")
}
}
}

View File

@ -65,7 +65,7 @@ import kotlin.math.ceil
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@OptIn(ExperimentalTime::class)
@Composable
fun NoteCompose(
baseNote: Note,
@ -130,6 +130,19 @@ fun NoteComposeInner(
var moreActionsExpanded by remember { mutableStateOf(false) }
var isAcceptable by remember { mutableStateOf(true) }
var canPreview by remember { mutableStateOf(true) }
LaunchedEffect(key1 = noteReportsState) {
withContext(Dispatchers.IO) {
canPreview = note?.author === account.userProfile() ||
(note?.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
isAcceptable = account.isAcceptable(noteForReports)
}
}
val noteEvent = note?.event
val baseChannel = note?.channel()
@ -141,7 +154,7 @@ fun NoteComposeInner(
),
isBoostedNote
)
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
} else if (!isAcceptable && !showHiddenNote) {
if (!account.isHidden(noteForReports.author!!)) {
HiddenNote(
account.getRelevantReports(noteForReports),
@ -343,11 +356,6 @@ fun NoteComposeInner(
Spacer(modifier = Modifier.height(3.dp))
if (!makeItShort && noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) {
val sortedMentions = noteEvent.mentions()
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowingCached(it) }
val replyingDirectlyTo = note.replyTo?.lastOrNull()
if (replyingDirectlyTo != null && unPackReply) {
NoteCompose(
@ -369,7 +377,7 @@ fun NoteComposeInner(
navController = navController
)
} else {
ReplyInformation(note.replyTo, sortedMentions, account, navController)
ReplyInformation(note.replyTo, noteEvent.mentions(), account, navController)
}
Spacer(modifier = Modifier.height(5.dp))
} else if (!makeItShort && noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) {
@ -501,10 +509,6 @@ fun NoteComposeInner(
} else {
val eventContent = accountViewModel.decrypt(note)
val canPreview = note.author == account.userProfile() ||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
if (eventContent != null) {
if (makeItShort && note.author == account.userProfile()) {
Text(
@ -524,7 +528,7 @@ fun NoteComposeInner(
navController
)
DisplayUncitedHashtags(noteEvent, eventContent, navController)
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController)
}
if (noteEvent is PollNoteEvent) {
@ -582,11 +586,10 @@ fun DisplayFollowingHashtagsInPost(
@Composable
fun DisplayUncitedHashtags(
noteEvent: EventInterface,
hashtags: List<String>,
eventContent: String,
navController: NavController
) {
val hashtags = noteEvent.hashtags()
if (hashtags.isNotEmpty()) {
FlowRow(
modifier = Modifier.padding(top = 5.dp)
@ -868,7 +871,11 @@ private fun RelayBadges(baseNote: Note) {
items(relaysToDisplay.size) {
val url = relaysToDisplay[it].removePrefix("wss://").removePrefix("ws://")
Box(Modifier.padding(1.dp).size(15.dp)) {
Box(
Modifier
.padding(1.dp)
.size(15.dp)
) {
RobohashFallbackAsyncImage(
robot = "https://$url/favicon.ico",
model = "https://$url/favicon.ico",

View File

@ -16,14 +16,15 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.*
@Composable
fun ReplyInformation(replyTo: List<Note>?, mentions: List<User>?, account: Account, navController: NavController) {
ReplyInformation(replyTo, mentions, account) {
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, navController: NavController) {
val sortedMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowingCached(it) }
ReplyInformation(replyTo, sortedMentions, account) {
navController.navigate("User/${it.pubkeyHex}")
}
}