diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index f09b0735c..9a20166c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -46,35 +46,7 @@ class MainActivity : FragmentActivity() { val uri = intent?.data?.toString() - val startingPage = if (uri.equals("nostr:Notifications", true)) { - Route.Notification.route.replace("{scrollToTop}", "true") - } else { - val nip19 = Nip19.uriToRoute(uri) - when (nip19?.type) { - Nip19.Type.USER -> "User/${nip19.hex}" - Nip19.Type.NOTE -> "Note/${nip19.hex}" - Nip19.Type.EVENT -> { - if (nip19.kind == PrivateDmEvent.kind) { - "Room/${nip19.author}" - } else if (nip19.kind == ChannelMessageEvent.kind || nip19.kind == ChannelCreateEvent.kind || nip19.kind == ChannelMetadataEvent.kind) { - "Channel/${nip19.hex}" - } else { - "Event/${nip19.hex}" - } - } - - Nip19.Type.ADDRESS -> "Note/${nip19.hex}" - else -> null - } - } ?: try { - uri?.let { - Nip47.parse(it) - val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) - Route.Home.base + "?nip47=" + encodedUri - } - } catch (e: Exception) { - null - } + val startingPage = uriToRoute(uri) // Initializes video cache. VideoCache.init(this.applicationContext) @@ -150,3 +122,39 @@ class GetMediaActivityResultContract : ActivityResultContracts.GetContent() { } } } + +fun uriToRoute(uri: String?): String? { + return if (uri.equals("nostr:Notifications", true)) { + Route.Notification.route.replace("{scrollToTop}", "true") + } else { + if (uri?.startsWith("nostr:Hashtag?id=") == true) { + Route.Hashtag.route.replace("{id}", uri.removePrefix("nostr:Hashtag?id=")) + } else { + val nip19 = Nip19.uriToRoute(uri) + when (nip19?.type) { + Nip19.Type.USER -> "User/${nip19.hex}" + Nip19.Type.NOTE -> "Note/${nip19.hex}" + Nip19.Type.EVENT -> { + if (nip19.kind == PrivateDmEvent.kind) { + "Room/${nip19.author}" + } else if (nip19.kind == ChannelMessageEvent.kind || nip19.kind == ChannelCreateEvent.kind || nip19.kind == ChannelMetadataEvent.kind) { + "Channel/${nip19.hex}" + } else { + "Event/${nip19.hex}" + } + } + + Nip19.Type.ADDRESS -> "Note/${nip19.hex}" + else -> null + } + } ?: try { + uri?.let { + Nip47.parse(it) + val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) + Route.Home.base + "?nip47=" + encodedUri + } + } catch (e: Exception) { + null + } + } +} 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 b2f1b8674..d745aeecf 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 @@ -19,10 +19,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -43,6 +43,7 @@ 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 com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -99,7 +100,7 @@ fun RichTextViewer( Column(modifier = modifier) { if (isMarkdown) { - RenderContentAsMarkdown(content, backgroundColor) + RenderContentAsMarkdown(content, backgroundColor, tags, navController) } else { RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, navController) } @@ -310,7 +311,7 @@ fun RenderCustomEmoji(word: String, customEmoji: Map) { } @Composable -private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { +private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: List>?, navController: NavController) { val myMarkDownStyle = richTextDefaults.copy( codeBlockStyle = richTextDefaults.codeBlockStyle?.copy( textStyle = TextStyle( @@ -334,7 +335,6 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { ), stringStyle = richTextDefaults.stringStyle?.copy( linkStyle = SpanStyle( - textDecoration = TextDecoration.Underline, color = MaterialTheme.colors.primary ), codeStyle = SpanStyle( @@ -352,14 +352,14 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { LaunchedEffect(key1 = content) { withContext(Dispatchers.IO) { - nip19References = returnNIP19References(content) - markdownWithSpecialContent = returnMarkdownWithSpecialContent(content) + nip19References = returnNIP19References(content, tags) + markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) } } LaunchedEffect(key1 = refresh) { withContext(Dispatchers.IO) { - val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content) + val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { markdownWithSpecialContent = newMarkdownWithSpecialContent } @@ -389,17 +389,23 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { baseNote?.let { val noteState by it.live().metadata.observeAsState() if (noteState?.note?.event != null) { - refresh++ + LaunchedEffect(key1 = noteState) { + refresh++ + } } } baseUser?.let { val userState by it.live().metadata.observeAsState() if (userState?.user?.info != null) { - refresh++ + LaunchedEffect(key1 = userState) { + refresh++ + } } } } + val uri = LocalUriHandler.current + markdownWithSpecialContent?.let { MaterialRichText( style = myMarkDownStyle @@ -407,26 +413,70 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { Markdown( content = it, markdownParseOptions = MarkdownParseOptions.Default - ) + ) { link -> + val route = uriToRoute(link) + if (route != null) { + navController.navigate(route) + } else { + runCatching { uri.openUri(link) } + } + } } } } -private fun getDisplayNameFromNip19(nip19: Nip19.Return): String? { - if (nip19.type == Nip19.Type.USER) { - return LocalCache.users[nip19.hex]?.toBestDisplayName() - } else if (nip19.type == Nip19.Type.NOTE) { - return LocalCache.notes[nip19.hex]?.idDisplayNote() - } else if (nip19.type == Nip19.Type.ADDRESS) { - return LocalCache.addressables[nip19.hex]?.idDisplayNote() - } else if (nip19.type == Nip19.Type.EVENT) { - return LocalCache.notes[nip19.hex]?.idDisplayNote() ?: LocalCache.addressables[nip19.hex]?.idDisplayNote() - } else { - return null +private fun getDisplayNameAndNIP19FromTag(tag: String, tags: List>): Pair? { + val matcher = tagIndex.matcher(tag) + val (index, suffix) = try { + matcher.find() + Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") + } catch (e: Exception) { + Log.w("Tag Parser", "Couldn't link tag $tag", e) + Pair(null, null) } + + if (index != null && index >= 0 && index < tags.size) { + val tag = tags[index] + + if (tag.size > 1) { + if (tag[0] == "p") { + LocalCache.checkGetOrCreateUser(tag[1])?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } + } else if (tag[0] == "e" || tag[0] == "a") { + LocalCache.checkGetOrCreateNote(tag[1])?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + } + } + + return null } -private fun returnNIP19References(content: String): List { +private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair? { + if (nip19.type == Nip19.Type.USER) { + LocalCache.users[nip19.hex]?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } + } else if (nip19.type == Nip19.Type.NOTE) { + LocalCache.notes[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } else if (nip19.type == Nip19.Type.ADDRESS) { + LocalCache.addressables[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } else if (nip19.type == Nip19.Type.EVENT) { + LocalCache.notes[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + + return null +} + +private fun returnNIP19References(content: String, tags: List>?): List { val listOfReferences = mutableListOf() content.split('\n').forEach { paragraph -> paragraph.split(' ').forEach { word: String -> @@ -438,10 +488,21 @@ private fun returnNIP19References(content: String): List { } } } + + tags?.forEach { + if (it[0] == "p" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, "")) + } else if (it[0] == "e" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, "")) + } else if (it[0] == "a" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, "")) + } + } + return listOfReferences } -private fun returnMarkdownWithSpecialContent(content: String): String { +private fun returnMarkdownWithSpecialContent(content: String, tags: List>?): String { var returnContent = "" content.split('\n').forEach { paragraph -> paragraph.split(' ').forEach { word: String -> @@ -454,15 +515,43 @@ private fun returnMarkdownWithSpecialContent(content: String): String { } else if (isBechLink(word)) { val parsedNip19 = Nip19.uriToRoute(word) returnContent += if (parsedNip19 !== null) { - val displayName = getDisplayNameFromNip19(parsedNip19) - if (displayName != null) { - "[@$displayName](nostr://$word) " + val pair = getDisplayNameFromNip19(parsedNip19) + if (pair != null) { + val (displayName, nip19) = pair + "[$displayName](nostr:$nip19) " } else { "$word " } } else { "$word " } + } else if (word.startsWith("#")) { + if (tagIndex.matcher(word).matches() && tags != null) { + val pair = getDisplayNameAndNIP19FromTag(word, tags) + if (pair != null) { + returnContent += "[${pair.first}](nostr:${pair.second}) " + } else { + returnContent += "$word " + } + } else if (hashTagsPattern.matcher(word).matches()) { + val hashtagMatcher = hashTagsPattern.matcher(word) + + val (myTag, mySuffix) = try { + hashtagMatcher.find() + Pair(hashtagMatcher.group(1), hashtagMatcher.group(2)) + } catch (e: Exception) { + Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) + Pair(null, null) + } + + if (myTag != null) { + returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix " + } else { + returnContent += "$word " + } + } else { + returnContent += "$word " + } } else { returnContent += "$word " }