2023-01-11 18:31:20 +00:00
|
|
|
package com.vitorpamplona.amethyst.ui.components
|
|
|
|
|
2023-01-13 17:30:13 +00:00
|
|
|
import android.util.Patterns
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.foundation.layout.Column
|
2023-01-30 01:06:48 +00:00
|
|
|
import androidx.compose.foundation.layout.Row
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.foundation.layout.padding
|
2023-01-26 02:05:32 +00:00
|
|
|
import androidx.compose.material.LocalTextStyle
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.material.Text
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
import androidx.compose.ui.Modifier
|
2023-01-25 17:59:52 +00:00
|
|
|
import androidx.compose.ui.text.TextStyle
|
|
|
|
import androidx.compose.ui.text.style.TextDirection
|
2023-01-11 18:31:20 +00:00
|
|
|
import androidx.compose.ui.unit.dp
|
2023-01-26 02:05:32 +00:00
|
|
|
import androidx.compose.ui.unit.sp
|
2023-01-14 02:35:28 +00:00
|
|
|
import androidx.navigation.NavController
|
2023-01-11 18:31:20 +00:00
|
|
|
import com.google.accompanist.flowlayout.FlowRow
|
2023-01-16 15:51:10 +00:00
|
|
|
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
|
2023-01-11 18:31:20 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.LocalCache
|
2023-01-14 01:16:57 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.Note
|
2023-01-30 16:50:24 +00:00
|
|
|
import com.vitorpamplona.amethyst.model.toByteArray
|
|
|
|
import com.vitorpamplona.amethyst.model.toNote
|
|
|
|
import com.vitorpamplona.amethyst.service.Nip19
|
2023-01-16 15:51:10 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
2023-01-14 01:16:57 +00:00
|
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
2023-01-11 18:31:20 +00:00
|
|
|
import java.net.MalformedURLException
|
|
|
|
import java.net.URISyntaxException
|
|
|
|
import java.net.URL
|
|
|
|
import java.util.regex.Pattern
|
2023-01-30 16:50:24 +00:00
|
|
|
import nostr.postr.toNpub
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-01-16 17:57:23 +00:00
|
|
|
val imageExtension = Pattern.compile("(.*/)*.+\\.(png|jpg|gif|bmp|jpeg|webp|svg)$")
|
2023-01-13 03:40:39 +00:00
|
|
|
val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm)$")
|
2023-01-11 18:31:20 +00:00
|
|
|
val noProtocolUrlValidator = Pattern.compile("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$")
|
2023-01-14 02:35:28 +00:00
|
|
|
val tagIndex = Pattern.compile(".*\\#\\[([0-9]+)\\].*")
|
2023-01-11 18:31:20 +00:00
|
|
|
|
2023-01-13 17:30:13 +00:00
|
|
|
val mentionsPattern: Pattern = Pattern.compile("@([A-Za-z0-9_-]+)")
|
|
|
|
val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_-]+)")
|
|
|
|
val urlPattern: Pattern = Patterns.WEB_URL
|
|
|
|
|
2023-01-11 18:31:20 +00:00
|
|
|
fun isValidURL(url: String?): Boolean {
|
|
|
|
return try {
|
|
|
|
URL(url).toURI()
|
|
|
|
true
|
|
|
|
} catch (e: MalformedURLException) {
|
|
|
|
false
|
|
|
|
} catch (e: URISyntaxException) {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
2023-01-25 16:29:44 +00:00
|
|
|
fun RichTextViewer(content: String, tags: List<List<String>>?, navController: NavController) {
|
2023-01-11 18:31:20 +00:00
|
|
|
Column(modifier = Modifier.padding(top = 5.dp)) {
|
2023-01-30 01:06:48 +00:00
|
|
|
|
2023-01-11 18:31:20 +00:00
|
|
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
|
|
|
content.split('\n').forEach { paragraph ->
|
|
|
|
|
|
|
|
FlowRow() {
|
|
|
|
paragraph.split(' ').forEach { word: String ->
|
|
|
|
// Explicit URL
|
2023-01-13 17:30:13 +00:00
|
|
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
|
|
|
if (lnInvoice != null) {
|
|
|
|
InvoicePreview(lnInvoice)
|
|
|
|
} else if (isValidURL(word)) {
|
2023-01-11 18:31:20 +00:00
|
|
|
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
|
|
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
2023-01-16 17:57:23 +00:00
|
|
|
ZoomableImageView(word)
|
2023-01-13 03:40:39 +00:00
|
|
|
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
|
|
|
VideoView(word)
|
2023-01-11 18:31:20 +00:00
|
|
|
} else {
|
|
|
|
UrlPreview(word, word)
|
|
|
|
}
|
2023-01-18 14:18:26 +00:00
|
|
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
|
|
ClickableEmail(word)
|
2023-01-22 22:07:06 +00:00
|
|
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
2023-01-18 14:18:26 +00:00
|
|
|
ClickablePhone(word)
|
2023-01-11 18:31:20 +00:00
|
|
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
|
|
|
UrlPreview("https://$word", word)
|
|
|
|
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
2023-01-14 02:35:28 +00:00
|
|
|
TagLink(word, tags, navController)
|
2023-01-30 16:50:24 +00:00
|
|
|
} else if (isBechLink(word)) {
|
|
|
|
BechLink(word, navController)
|
2023-01-11 18:31:20 +00:00
|
|
|
} else {
|
2023-01-26 02:05:32 +00:00
|
|
|
Text(
|
|
|
|
text = "$word ",
|
|
|
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
|
|
|
)
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-30 16:50:24 +00:00
|
|
|
fun isBechLink(word: String): Boolean {
|
|
|
|
return word.startsWith("nostr:", true)
|
|
|
|
|| word.startsWith("npub1", true)
|
|
|
|
|| word.startsWith("note1", true)
|
|
|
|
|| word.startsWith("nprofile1", true)
|
|
|
|
|| word.startsWith("nevent1", true)
|
|
|
|
|| word.startsWith("@npub1", true)
|
|
|
|
|| word.startsWith("@note1", true)
|
|
|
|
|| word.startsWith("@nprofile1", true)
|
|
|
|
|| word.startsWith("@nevent1", true)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
fun BechLink(word: String, navController: NavController) {
|
|
|
|
val uri = if (word.startsWith("nostr", true)) {
|
|
|
|
word
|
|
|
|
} else if (word.startsWith("@")) {
|
|
|
|
word.replaceFirst("@", "nostr:")
|
|
|
|
} else {
|
|
|
|
"nostr:${word}"
|
|
|
|
}
|
|
|
|
|
|
|
|
val nip19Route = try {
|
|
|
|
Nip19().uriToRoute(uri)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nip19Route == null) {
|
|
|
|
Text(text = "$word ")
|
|
|
|
} else {
|
|
|
|
ClickableRoute(nip19Route, navController)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-01-11 18:31:20 +00:00
|
|
|
@Composable
|
2023-01-14 02:35:28 +00:00
|
|
|
fun TagLink(word: String, tags: List<List<String>>, navController: NavController) {
|
2023-01-11 18:31:20 +00:00
|
|
|
val matcher = tagIndex.matcher(word)
|
|
|
|
|
|
|
|
val index = try {
|
|
|
|
matcher.find()
|
|
|
|
matcher.group(1).toInt()
|
|
|
|
} catch (e: Exception) {
|
|
|
|
println("Couldn't link tag ${word}")
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index == null) {
|
|
|
|
return Text(text = "$word ")
|
|
|
|
}
|
|
|
|
|
2023-01-15 21:14:32 +00:00
|
|
|
if (index >= 0 && index < tags.size) {
|
2023-01-11 18:31:20 +00:00
|
|
|
if (tags[index][0] == "p") {
|
|
|
|
val user = LocalCache.users[tags[index][1]]
|
|
|
|
if (user != null) {
|
2023-01-16 15:51:10 +00:00
|
|
|
ClickableUserTag(user, navController)
|
|
|
|
} else {
|
2023-01-30 16:50:24 +00:00
|
|
|
Text(text = "${tags[index][1].toByteArray().toNpub().toShortenHex()} ")
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
} else if (tags[index][0] == "e") {
|
|
|
|
val note = LocalCache.notes[tags[index][1]]
|
|
|
|
if (note != null) {
|
2023-01-16 15:51:10 +00:00
|
|
|
ClickableNoteTag(note, navController)
|
|
|
|
} else {
|
2023-01-30 16:50:24 +00:00
|
|
|
Text(text = "${tags[index][1].toByteArray().toNote().toShortenHex()} ")
|
2023-01-11 18:31:20 +00:00
|
|
|
}
|
|
|
|
} else
|
|
|
|
Text(text = "$word ")
|
|
|
|
}
|
2023-01-16 15:51:10 +00:00
|
|
|
}
|
|
|
|
|