- Adds hashtags and #[n] rendering to markdown

- Shortcuts the link to Amethyst's router.
This commit is contained in:
Vitor Pamplona
2023-05-21 20:54:42 -04:00
parent a0797f7104
commit b219b3631c
2 changed files with 152 additions and 55 deletions
@@ -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
}
}
}
@@ -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<String, String>) {
}
@Composable
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) {
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: List<List<String>>?, 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<List<String>>): Pair<String, String>? {
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<Nip19.Return> {
private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>? {
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<String>>?): List<Nip19.Return> {
val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
@@ -438,10 +488,21 @@ private fun returnNIP19References(content: String): List<Nip19.Return> {
}
}
}
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<List<String>>?): 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 "
}