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 10c3d07d2b..040471c105 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,7 +22,7 @@ class Note(val idHex: String) { // These fields are always available. // They are immutable val id = Hex.decode(idHex) - val idDisplayHex = id.toShortenHex() + val idDisplayNote = id.toNote().toShortenHex() // These fields are only available after the Text Note event is received. // They are immutable after that. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 8c4069bfb1..6b3d2364f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -15,10 +15,11 @@ import kotlinx.coroutines.launch import nostr.postr.events.ContactListEvent import nostr.postr.events.Event import nostr.postr.events.MetadataEvent +import nostr.postr.toNpub class User(val pubkey: ByteArray) { val pubkeyHex = pubkey.toHexKey() - val pubkeyDisplayHex = pubkey.toShortenHex() + val pubkeyDisplayHex = pubkey.toNpub().toShortenHex() var info = UserMetadata() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index f186ceb743..f525882e56 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -36,6 +36,7 @@ class Nip19 { } } } catch (e: Throwable) { + println("Trying to Decode NIP19: ${uri}") e.printStackTrace() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt new file mode 100644 index 0000000000..7162d3c4d8 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -0,0 +1,63 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.text.AnnotatedString +import androidx.navigation.NavController +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.model.toNote +import com.vitorpamplona.amethyst.service.Nip19 +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent +import com.vitorpamplona.amethyst.ui.note.toShortenHex +import nostr.postr.toNpub + +@Composable +fun ClickableRoute( + nip19: Nip19.Return, + navController: NavController +) { + if (nip19.type == Nip19.Type.USER) { + val userBase = LocalCache.getOrCreateUser(nip19.hex.toByteArray()) + val userState by userBase.live.observeAsState() + val user = userState?.user ?: return + + val route = "User/${nip19.hex}" + val text = user.toBestDisplayName() + + ClickableText( + text = AnnotatedString("@${text} "), + onClick = { navController.navigate(route) }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else { + val noteBase = LocalCache.getOrCreateNote(nip19.hex) + val noteState by noteBase.live.observeAsState() + val note = noteState?.note ?: return + + if (note.event is ChannelCreateEvent) { + ClickableText( + text = AnnotatedString("@${note.idDisplayNote} "), + onClick = { navController.navigate("Channel/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else if (note.channel != null) { + ClickableText( + text = AnnotatedString("@${note.channel} "), + onClick = { navController.navigate("Channel/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else { + ClickableText( + text = AnnotatedString("@${note.idDisplayNote} "), + onClick = { navController.navigate("Note/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 3fbec9426d..601fb51a5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -17,12 +17,16 @@ import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.model.toNote +import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL import java.util.regex.Pattern +import nostr.postr.toNpub val imageExtension = Pattern.compile("(.*/)*.+\\.(png|jpg|gif|bmp|jpeg|webp|svg)$") val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm)$") @@ -74,6 +78,8 @@ fun RichTextViewer(content: String, tags: List>?, navController: Na UrlPreview("https://$word", word) } else if (tagIndex.matcher(word).matches() && tags != null) { TagLink(word, tags, navController) + } else if (isBechLink(word)) { + BechLink(word, navController) } else { Text( text = "$word ", @@ -87,6 +93,42 @@ fun RichTextViewer(content: String, tags: List>?, navController: Na } } +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) + } +} + + @Composable fun TagLink(word: String, tags: List>, navController: NavController) { val matcher = tagIndex.matcher(word) @@ -109,14 +151,14 @@ fun TagLink(word: String, tags: List>, navController: NavController if (user != null) { ClickableUserTag(user, navController) } else { - Text(text = "${tags[index][1].toShortenHex()} ") + Text(text = "${tags[index][1].toByteArray().toNpub().toShortenHex()} ") } } else if (tags[index][0] == "e") { val note = LocalCache.notes[tags[index][1]] if (note != null) { ClickableNoteTag(note, navController) } else { - Text(text = "${tags[index][1].toShortenHex()} ") + Text(text = "${tags[index][1].toByteArray().toNote().toShortenHex()} ") } } else Text(text = "$word ")