mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-07-21 20:52:40 +02:00
- Migrates to the new Markdown Parser.
- Adds Note previews on Markdown - Adds Custom hashtag icons to markdown. - Adds URL preview boxes to markdown - Performance improvements.
This commit is contained in:
@ -68,15 +68,16 @@ fun ExpandableRichTextViewer(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var showFullText by remember {
|
var showFullText by
|
||||||
val cached = ShowFullTextCache.cache[id]
|
remember {
|
||||||
if (cached == null) {
|
val cached = ShowFullTextCache.cache[id]
|
||||||
ShowFullTextCache.cache.put(id, false)
|
if (cached == null) {
|
||||||
mutableStateOf(false)
|
ShowFullTextCache.cache.put(id, false)
|
||||||
} else {
|
mutableStateOf(false)
|
||||||
mutableStateOf(cached)
|
} else {
|
||||||
|
mutableStateOf(cached)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) }
|
val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) }
|
||||||
|
|
||||||
|
@ -1,209 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Patterns
|
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|
||||||
import com.vitorpamplona.quartz.encoders.ATag
|
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
|
||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
|
|
||||||
class MarkdownParser {
|
|
||||||
private fun getDisplayNameAndNIP19FromTag(
|
|
||||||
tag: String,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
): Pair<String, String>? {
|
|
||||||
val matcher = RichTextParser.tagIndex.matcher(tag)
|
|
||||||
val (index, suffix) =
|
|
||||||
try {
|
|
||||||
matcher.find()
|
|
||||||
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
Log.w("Tag Parser", "Couldn't link tag $tag", e)
|
|
||||||
Pair(null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index != null && index >= 0 && index < tags.lists.size) {
|
|
||||||
val tag = tags.lists[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 suspend fun getDisplayNameFromNip19(nip19: Nip19Bech32.Entity): Pair<String, String>? {
|
|
||||||
return when (nip19) {
|
|
||||||
is Nip19Bech32.NSec -> null
|
|
||||||
is Nip19Bech32.NPub -> {
|
|
||||||
LocalCache.getUserIfExists(nip19.hex)?.let {
|
|
||||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Nip19Bech32.NProfile -> {
|
|
||||||
LocalCache.getUserIfExists(nip19.hex)?.let {
|
|
||||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Nip19Bech32.Note -> {
|
|
||||||
LocalCache.getNoteIfExists(nip19.hex)?.let {
|
|
||||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Nip19Bech32.NEvent -> {
|
|
||||||
LocalCache.getNoteIfExists(nip19.hex)?.let {
|
|
||||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Nip19Bech32.NEmbed -> {
|
|
||||||
if (LocalCache.getNoteIfExists(nip19.event.id) == null) {
|
|
||||||
LocalCache.verifyAndConsume(nip19.event, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalCache.getNoteIfExists(nip19.event.id)?.let {
|
|
||||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Nip19Bech32.NRelay -> null
|
|
||||||
is Nip19Bech32.NAddress -> {
|
|
||||||
LocalCache.getAddressableNoteIfExists(nip19.atag)?.let {
|
|
||||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun returnNIP19References(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
): List<Nip19Bech32.Entity> {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
val listOfReferences = mutableListOf<Nip19Bech32.Entity>()
|
|
||||||
content.split('\n').forEach { paragraph ->
|
|
||||||
paragraph.split(' ').forEach { word: String ->
|
|
||||||
if (RichTextParser.startsWithNIP19Scheme(word)) {
|
|
||||||
val parsedNip19 = Nip19Bech32.uriToRoute(word)
|
|
||||||
parsedNip19?.let { listOfReferences.add(it.entity) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags?.lists?.forEach {
|
|
||||||
if (it[0] == "p" && it.size > 1) {
|
|
||||||
listOfReferences.add(Nip19Bech32.NProfile(it[1], listOfNotNull(it.getOrNull(2))))
|
|
||||||
} else if (it[0] == "e" && it.size > 1) {
|
|
||||||
listOfReferences.add(Nip19Bech32.NEvent(it[1], listOfNotNull(it.getOrNull(2)), null, null))
|
|
||||||
} else if (it[0] == "a" && it.size > 1) {
|
|
||||||
ATag.parseAtag(it[1], it.getOrNull(2))?.let { atag ->
|
|
||||||
listOfReferences.add(Nip19Bech32.NAddress(it[1], listOfNotNull(atag.relay), atag.pubKeyHex, atag.kind))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOfReferences
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun returnMarkdownWithSpecialContent(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
): String {
|
|
||||||
var returnContent = ""
|
|
||||||
content.split('\n').forEach { paragraph ->
|
|
||||||
paragraph.split(' ').forEach { word: String ->
|
|
||||||
if (RichTextParser.isValidURL(word)) {
|
|
||||||
if (RichTextParser.isImageUrl(word)) {
|
|
||||||
returnContent += " "
|
|
||||||
} else {
|
|
||||||
returnContent += "[$word]($word) "
|
|
||||||
}
|
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
||||||
returnContent += "[$word](mailto:$word) "
|
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
|
||||||
returnContent += "[$word](tel:$word) "
|
|
||||||
} else if (RichTextParser.startsWithNIP19Scheme(word)) {
|
|
||||||
val parsedNip19 = Nip19Bech32.uriToRoute(word)
|
|
||||||
returnContent +=
|
|
||||||
if (parsedNip19?.entity !== null) {
|
|
||||||
val pair = getDisplayNameFromNip19(parsedNip19.entity)
|
|
||||||
if (pair != null) {
|
|
||||||
val (displayName, nip19) = pair
|
|
||||||
"[$displayName](nostr:$nip19) "
|
|
||||||
} else {
|
|
||||||
"$word "
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"$word "
|
|
||||||
}
|
|
||||||
} else if (word.startsWith("#")) {
|
|
||||||
if (RichTextParser.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 (RichTextParser.hashTagsPattern.matcher(word).matches()) {
|
|
||||||
val hashtagMatcher = RichTextParser.hashTagsPattern.matcher(word)
|
|
||||||
|
|
||||||
val (myTag, mySuffix) =
|
|
||||||
try {
|
|
||||||
hashtagMatcher.find()
|
|
||||||
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
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 "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
returnContent += "\n"
|
|
||||||
}
|
|
||||||
return returnContent
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,12 +29,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
@ -51,9 +51,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.Placeholder
|
|
||||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextMeasurer
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@ -65,9 +62,6 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.em
|
import androidx.compose.ui.unit.em
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
|
||||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
|
||||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
|
||||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
|
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
|
||||||
@ -79,10 +73,8 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
|
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
|
import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
|
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
|
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
|
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||||
@ -93,33 +85,29 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
|
|
||||||
import com.vitorpamplona.amethyst.ui.uriToRoute
|
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
|
||||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
fun isMarkdown(content: String): Boolean {
|
fun isMarkdown(content: String): Boolean {
|
||||||
return content.startsWith("> ") ||
|
return content.startsWith("> ") ||
|
||||||
content.startsWith("# ") ||
|
content.startsWith("# ") ||
|
||||||
content.contains("##") ||
|
content.contains("##") ||
|
||||||
content.contains("__") ||
|
content.contains("__") ||
|
||||||
|
content.contains("**") ||
|
||||||
content.contains("```") ||
|
content.contains("```") ||
|
||||||
content.contains("](")
|
content.contains("](")
|
||||||
}
|
}
|
||||||
@ -137,7 +125,7 @@ fun RichTextViewer(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
if (remember(content) { isMarkdown(content) }) {
|
if (remember(content) { isMarkdown(content) }) {
|
||||||
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
|
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||||
} else {
|
} else {
|
||||||
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@ -346,17 +334,6 @@ fun RenderRegular(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// UrlPreviews and Images have a 5dp spacing down. This also adds the space to Text.
|
|
||||||
val lastElement = state.paragraphs.lastOrNull()?.words?.lastOrNull()
|
|
||||||
if (lastElement !is ImageSegment &&
|
|
||||||
lastElement !is LinkSegment &&
|
|
||||||
lastElement !is InvoiceSegment &&
|
|
||||||
lastElement !is CashuSegment
|
|
||||||
) {
|
|
||||||
Spacer(modifier = StdVertSpacer)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,186 +439,6 @@ fun RenderCustomEmoji(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val markdownParseOptions =
|
|
||||||
MarkdownParseOptions(
|
|
||||||
autolink = true,
|
|
||||||
isImage = { url -> RichTextParser.isImageOrVideoUrl(url) },
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RenderContentAsMarkdown(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
val uri = LocalUriHandler.current
|
|
||||||
val onClick =
|
|
||||||
remember {
|
|
||||||
{ link: String ->
|
|
||||||
val route = uriToRoute(link)
|
|
||||||
if (route != null) {
|
|
||||||
nav(route)
|
|
||||||
} else {
|
|
||||||
runCatching { uri.openUri(link) }
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProvideTextStyle(MarkdownTextStyle) {
|
|
||||||
Material3RichText(style = MaterialTheme.colorScheme.markdownStyle) {
|
|
||||||
RefreshableContent(content, tags, accountViewModel) {
|
|
||||||
Markdown(
|
|
||||||
content = it,
|
|
||||||
markdownParseOptions = markdownParseOptions,
|
|
||||||
onLinkClicked = onClick,
|
|
||||||
onMediaCompose = { title, destination ->
|
|
||||||
ZoomableContentView(
|
|
||||||
content =
|
|
||||||
remember(destination, tags) {
|
|
||||||
RichTextParser().parseMediaUrl(
|
|
||||||
destination,
|
|
||||||
tags ?: EmptyTagList,
|
|
||||||
title.ifEmpty { null } ?: content,
|
|
||||||
) ?: MediaUrlImage(url = destination, description = title.ifEmpty { null } ?: content)
|
|
||||||
},
|
|
||||||
roundedCorner = true,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RefreshableContent(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onCompose: @Composable (String) -> Unit,
|
|
||||||
) {
|
|
||||||
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
|
|
||||||
|
|
||||||
ObserverAllNIP19References(content, tags, accountViewModel) {
|
|
||||||
accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent ->
|
|
||||||
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
|
|
||||||
markdownWithSpecialContent = newMarkdownWithSpecialContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markdownWithSpecialContent?.let { onCompose(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ObserverAllNIP19References(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
var nip19References by remember(content) { mutableStateOf<List<Nip19Bech32.Entity>>(emptyList()) }
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = content) {
|
|
||||||
accountViewModel.returnNIP19References(content, tags) {
|
|
||||||
nip19References = it
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nip19References.forEach { ObserveNIP19(it, accountViewModel, onRefresh) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ObserveNIP19(
|
|
||||||
entity: Nip19Bech32.Entity,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
when (entity) {
|
|
||||||
is Nip19Bech32.NPub -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
|
||||||
is Nip19Bech32.NProfile -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
|
||||||
|
|
||||||
is Nip19Bech32.Note -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
|
||||||
is Nip19Bech32.NEvent -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
|
||||||
is Nip19Bech32.NEmbed -> ObserveNIP19Event(entity.event.id, accountViewModel, onRefresh)
|
|
||||||
|
|
||||||
is Nip19Bech32.NAddress -> ObserveNIP19Event(entity.atag, accountViewModel, onRefresh)
|
|
||||||
|
|
||||||
is Nip19Bech32.NSec -> {}
|
|
||||||
is Nip19Bech32.NRelay -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ObserveNIP19Event(
|
|
||||||
hex: HexKey,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
var baseNote by remember(hex) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(hex)) }
|
|
||||||
|
|
||||||
if (baseNote == null) {
|
|
||||||
LaunchedEffect(key1 = hex) {
|
|
||||||
accountViewModel.checkGetOrCreateNote(hex) { note ->
|
|
||||||
launch(Dispatchers.Main) { baseNote = note }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseNote?.let { note -> ObserveNote(note, onRefresh) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ObserveNote(
|
|
||||||
note: Note,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
val loadedNoteId by note.live().metadata.observeAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = loadedNoteId) {
|
|
||||||
if (loadedNoteId != null) {
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ObserveNIP19User(
|
|
||||||
hex: HexKey,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
var baseUser by remember(hex) { mutableStateOf<User?>(accountViewModel.getUserIfExists(hex)) }
|
|
||||||
|
|
||||||
if (baseUser == null) {
|
|
||||||
LaunchedEffect(key1 = hex) {
|
|
||||||
accountViewModel.checkGetOrCreateUser(hex)?.let { user ->
|
|
||||||
launch(Dispatchers.Main) { baseUser = user }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUser?.let { user -> ObserveUser(user, onRefresh) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ObserveUser(
|
|
||||||
user: User,
|
|
||||||
onRefresh: () -> Unit,
|
|
||||||
) {
|
|
||||||
val loadedUserMetaId by user.live().metadata.observeAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = loadedUserMetaId) {
|
|
||||||
if (loadedUserMetaId != null) {
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BechLink(
|
fun BechLink(
|
||||||
word: String,
|
word: String,
|
||||||
@ -683,7 +480,7 @@ fun BechLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisplayFullNote(
|
fun DisplayFullNote(
|
||||||
note: Note,
|
note: Note,
|
||||||
extraChars: String?,
|
extraChars: String?,
|
||||||
quotesLeft: Int,
|
quotesLeft: Int,
|
||||||
@ -752,13 +549,7 @@ fun HashTag(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
||||||
InlineTextContent(
|
InlineTextContent(inlinePlaceholder) {
|
||||||
Placeholder(
|
|
||||||
width = Font17SP,
|
|
||||||
height = Font17SP,
|
|
||||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = hashtagIcon.icon,
|
imageVector = hashtagIcon.icon,
|
||||||
contentDescription = hashtagIcon.description,
|
contentDescription = hashtagIcon.description,
|
||||||
|
@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.components.markdown
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.halilibo.richtext.ui.MediaRenderer
|
||||||
|
import com.halilibo.richtext.ui.string.InlineContent
|
||||||
|
import com.halilibo.richtext.ui.string.RichTextString
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
|
import com.vitorpamplona.amethyst.model.HashtagIcon
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.DisplayFullNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.DisplayUser
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
|
||||||
|
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||||
|
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||||
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class MarkdownMediaRenderer(
|
||||||
|
val startOfText: String,
|
||||||
|
val tags: ImmutableListOfLists<String>?,
|
||||||
|
val canPreview: Boolean,
|
||||||
|
val quotesLeft: Int,
|
||||||
|
val backgroundColor: MutableState<Color>,
|
||||||
|
val accountViewModel: AccountViewModel,
|
||||||
|
val nav: (String) -> Unit,
|
||||||
|
) : MediaRenderer {
|
||||||
|
val parser = RichTextParser()
|
||||||
|
|
||||||
|
override fun shouldRenderLinkPreview(
|
||||||
|
title: String?,
|
||||||
|
uri: String,
|
||||||
|
): Boolean {
|
||||||
|
return if (canPreview && uri.startsWith("http")) {
|
||||||
|
if (title.isNullOrBlank() || title == uri) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renderImage(
|
||||||
|
title: String?,
|
||||||
|
uri: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
if (canPreview) {
|
||||||
|
val content =
|
||||||
|
parser.parseMediaUrl(
|
||||||
|
fullUrl = uri,
|
||||||
|
eventTags = tags ?: EmptyTagList,
|
||||||
|
description = title?.ifEmpty { null } ?: startOfText,
|
||||||
|
) ?: MediaUrlImage(url = uri, description = title?.ifEmpty { null } ?: startOfText)
|
||||||
|
|
||||||
|
renderInlineFullWidth(richTextStringBuilder) {
|
||||||
|
ZoomableContentView(
|
||||||
|
content = content,
|
||||||
|
roundedCorner = true,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renderLinkPreview(
|
||||||
|
title: String?,
|
||||||
|
uri: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
val content = parser.parseMediaUrl(uri, eventTags = tags ?: EmptyTagList, startOfText)
|
||||||
|
|
||||||
|
if (canPreview) {
|
||||||
|
if (content != null) {
|
||||||
|
renderInlineFullWidth(richTextStringBuilder) {
|
||||||
|
ZoomableContentView(
|
||||||
|
content = content,
|
||||||
|
roundedCorner = true,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!accountViewModel.settings.showUrlPreview.value) {
|
||||||
|
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
|
||||||
|
} else {
|
||||||
|
renderInlineFullWidth(richTextStringBuilder) {
|
||||||
|
LoadUrlPreview(uri, title ?: uri, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renderNostrUri(
|
||||||
|
uri: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
// This should be fast, so it is ok.
|
||||||
|
val loadedLink =
|
||||||
|
accountViewModel.bechLinkCache.cached(uri)
|
||||||
|
?: runBlocking {
|
||||||
|
accountViewModel.bechLinkCache.update(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseNote = loadedLink?.baseNote
|
||||||
|
|
||||||
|
if (canPreview && quotesLeft > 0 && baseNote != null) {
|
||||||
|
renderInlineFullWidth(richTextStringBuilder) {
|
||||||
|
Row {
|
||||||
|
DisplayFullNote(
|
||||||
|
note = baseNote,
|
||||||
|
extraChars = loadedLink.nip19.additionalChars?.ifBlank { null },
|
||||||
|
quotesLeft = quotesLeft,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (loadedLink?.nip19 != null) {
|
||||||
|
when (val entity = loadedLink.nip19.entity) {
|
||||||
|
is Nip19Bech32.NPub -> renderObservableUser(entity.hex, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NProfile -> renderObservableUser(entity.hex, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.Note -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NEvent -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NEmbed -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NAddress -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NRelay -> renderShortNostrURI(uri, richTextStringBuilder)
|
||||||
|
is Nip19Bech32.NSec -> renderShortNostrURI(uri, richTextStringBuilder)
|
||||||
|
else -> renderShortNostrURI(uri, richTextStringBuilder)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderShortNostrURI(uri, richTextStringBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renderHashtag(
|
||||||
|
tag: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
val tagWithoutHash = tag.removePrefix("#")
|
||||||
|
renderAsCompleteLink(tag, "nostr:Hashtag?id=$tagWithoutHash}", richTextStringBuilder)
|
||||||
|
|
||||||
|
val hashtagIcon: HashtagIcon? = checkForHashtagWithIcon(tagWithoutHash)
|
||||||
|
if (hashtagIcon != null) {
|
||||||
|
renderInline(richTextStringBuilder) {
|
||||||
|
Box(Size17Modifier) {
|
||||||
|
Icon(
|
||||||
|
imageVector = hashtagIcon.icon,
|
||||||
|
contentDescription = hashtagIcon.description,
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
modifier = hashtagIcon.modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderObservableUser(
|
||||||
|
userHex: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
renderInline(richTextStringBuilder) {
|
||||||
|
DisplayUser(userHex, null, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderObservableShortNoteUri(
|
||||||
|
loadedLink: LoadedBechLink,
|
||||||
|
uri: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
loadedLink.baseNote?.let { renderNoteObserver(it, richTextStringBuilder) }
|
||||||
|
renderShortNostrURI(uri, richTextStringBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderNoteObserver(
|
||||||
|
baseNote: Note,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
renderInvisible(richTextStringBuilder) {
|
||||||
|
// Preloads note if not loaded yet.
|
||||||
|
baseNote.live().metadata.observeAsState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderShortNostrURI(
|
||||||
|
uri: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
val nip19 = "@" + uri.removePrefix("nostr:")
|
||||||
|
|
||||||
|
renderAsCompleteLink(
|
||||||
|
title =
|
||||||
|
if (nip19.length > 16) {
|
||||||
|
nip19.replaceRange(8, nip19.length - 8, ":")
|
||||||
|
} else {
|
||||||
|
nip19
|
||||||
|
},
|
||||||
|
destination = uri,
|
||||||
|
richTextStringBuilder = richTextStringBuilder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderInvisible(
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
innerComposable: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
richTextStringBuilder.appendInlineContent(
|
||||||
|
content =
|
||||||
|
InlineContent(
|
||||||
|
initialSize = {
|
||||||
|
IntSize(0.dp.roundToPx(), 0.dp.roundToPx())
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
innerComposable()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderInline(
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
innerComposable: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
richTextStringBuilder.appendInlineContent(
|
||||||
|
content =
|
||||||
|
InlineContent(
|
||||||
|
initialSize = {
|
||||||
|
IntSize(Font17SP.roundToPx(), Font17SP.roundToPx())
|
||||||
|
},
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||||
|
) {
|
||||||
|
innerComposable()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderInlineFullWidth(
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
innerComposable: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
richTextStringBuilder.appendInlineContentFullWidth(
|
||||||
|
content =
|
||||||
|
InlineContent(
|
||||||
|
initialSize = {
|
||||||
|
IntSize(Font17SP.roundToPx(), Font17SP.roundToPx())
|
||||||
|
},
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||||
|
) {
|
||||||
|
innerComposable()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAsCompleteLink(
|
||||||
|
title: String,
|
||||||
|
destination: String,
|
||||||
|
richTextStringBuilder: RichTextString.Builder,
|
||||||
|
) {
|
||||||
|
richTextStringBuilder.pushFormat(
|
||||||
|
RichTextString.Format.Link(destination = destination),
|
||||||
|
)
|
||||||
|
richTextStringBuilder.append(title)
|
||||||
|
richTextStringBuilder.pop()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.components.markdown
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||||
|
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||||
|
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||||
|
import com.halilibo.richtext.ui.material3.RichText
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
|
||||||
|
import com.vitorpamplona.amethyst.ui.uriToRoute
|
||||||
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderContentAsMarkdown(
|
||||||
|
content: String,
|
||||||
|
tags: ImmutableListOfLists<String>?,
|
||||||
|
canPreview: Boolean,
|
||||||
|
quotesLeft: Int,
|
||||||
|
backgroundColor: MutableState<Color>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
val onClick =
|
||||||
|
remember {
|
||||||
|
{ link: String ->
|
||||||
|
val route = uriToRoute(link)
|
||||||
|
if (route != null) {
|
||||||
|
nav(route)
|
||||||
|
} else {
|
||||||
|
runCatching { uri.openUri(link) }
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProvideTextStyle(MarkdownTextStyle) {
|
||||||
|
val astNode =
|
||||||
|
remember(content) {
|
||||||
|
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
val renderer =
|
||||||
|
remember(content) {
|
||||||
|
MarkdownMediaRenderer(
|
||||||
|
content.take(100),
|
||||||
|
tags,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RichText(
|
||||||
|
style = MaterialTheme.colorScheme.markdownStyle,
|
||||||
|
linkClickHandler = onClick,
|
||||||
|
renderer = renderer,
|
||||||
|
) {
|
||||||
|
BasicMarkdown(astNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,7 @@ import androidx.compose.material3.ButtonDefaults
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -82,9 +83,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||||
|
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||||
|
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
import com.halilibo.richtext.ui.material3.RichText
|
||||||
import com.halilibo.richtext.ui.resolveDefaults
|
import com.halilibo.richtext.ui.resolveDefaults
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
@ -140,12 +143,18 @@ fun AccountBackupDialog(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
Material3RichText(
|
val content1 = stringResource(R.string.account_backup_tips2_md)
|
||||||
|
|
||||||
|
val astNode1 =
|
||||||
|
remember {
|
||||||
|
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||||
|
}
|
||||||
|
|
||||||
|
RichText(
|
||||||
style = RichTextStyle().resolveDefaults(),
|
style = RichTextStyle().resolveDefaults(),
|
||||||
|
renderer = null,
|
||||||
) {
|
) {
|
||||||
Markdown(
|
BasicMarkdown(astNode1)
|
||||||
content = stringResource(R.string.account_backup_tips2_md),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
@ -154,12 +163,18 @@ fun AccountBackupDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
|
|
||||||
Material3RichText(
|
val content = stringResource(R.string.account_backup_tips3_md)
|
||||||
|
|
||||||
|
val astNode =
|
||||||
|
remember {
|
||||||
|
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
RichText(
|
||||||
style = RichTextStyle().resolveDefaults(),
|
style = RichTextStyle().resolveDefaults(),
|
||||||
|
renderer = null,
|
||||||
) {
|
) {
|
||||||
Markdown(
|
BasicMarkdown(astNode)
|
||||||
content = stringResource(R.string.account_backup_tips3_md),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
@ -59,7 +59,6 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
|||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
import com.vitorpamplona.amethyst.ui.components.MarkdownParser
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
|
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
|
||||||
@ -79,7 +78,6 @@ import com.vitorpamplona.quartz.events.DraftEvent
|
|||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import com.vitorpamplona.quartz.events.EventInterface
|
import com.vitorpamplona.quartz.events.EventInterface
|
||||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||||
import com.vitorpamplona.quartz.events.Participant
|
import com.vitorpamplona.quartz.events.Participant
|
||||||
@ -1015,26 +1013,6 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun returnNIP19References(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
onNewReferences: (List<Nip19Bech32.Entity>) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
onNewReferences(MarkdownParser().returnNIP19References(content, tags))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun returnMarkdownWithSpecialContent(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>?,
|
|
||||||
onNewContent: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
onNewContent(MarkdownParser().returnMarkdownWithSpecialContent(content, tags))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkIsOnline(
|
fun checkIsOnline(
|
||||||
media: String?,
|
media: String?,
|
||||||
onDone: (Boolean) -> Unit,
|
onDone: (Boolean) -> Unit,
|
||||||
|
@ -36,6 +36,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -47,8 +48,10 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||||
|
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||||
|
import com.halilibo.richtext.ui.material3.RichText
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
@ -112,12 +115,18 @@ fun ConnectOrbotDialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Material3RichText(
|
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
|
||||||
|
|
||||||
|
val astNode1 =
|
||||||
|
remember {
|
||||||
|
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||||
|
}
|
||||||
|
|
||||||
|
RichText(
|
||||||
style = myMarkDownStyle,
|
style = myMarkDownStyle,
|
||||||
|
renderer = null,
|
||||||
) {
|
) {
|
||||||
Markdown(
|
BasicMarkdown(astNode1)
|
||||||
content = stringResource(R.string.connect_through_your_orbot_setup_markdown),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,8 @@ import androidx.compose.material3.Shapes
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
val Shapes =
|
val Shapes =
|
||||||
@ -231,3 +233,10 @@ val liveStreamTag =
|
|||||||
val chatAuthorBox = Modifier.size(20.dp)
|
val chatAuthorBox = Modifier.size(20.dp)
|
||||||
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
||||||
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
||||||
|
|
||||||
|
val inlinePlaceholder =
|
||||||
|
Placeholder(
|
||||||
|
width = Font17SP,
|
||||||
|
height = Font17SP,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||||
|
)
|
||||||
|
@ -59,7 +59,7 @@ val Font18SP = 18.sp
|
|||||||
|
|
||||||
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
|
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
|
||||||
|
|
||||||
val DefaultParagraphSpacing: TextUnit = 16.sp
|
val DefaultParagraphSpacing: TextUnit = 18.sp
|
||||||
|
|
||||||
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
|
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
|
||||||
when (level) {
|
when (level) {
|
||||||
|
@ -26,7 +26,7 @@ kotlinxCollectionsImmutable = "0.3.7"
|
|||||||
languageId = "17.0.5"
|
languageId = "17.0.5"
|
||||||
lazysodiumAndroid = "5.1.0"
|
lazysodiumAndroid = "5.1.0"
|
||||||
lightcompressor = "1.3.2"
|
lightcompressor = "1.3.2"
|
||||||
markdown = "48702a8ced"
|
markdown = "077a2cde64"
|
||||||
media3 = "1.3.0"
|
media3 = "1.3.0"
|
||||||
mockk = "1.13.10"
|
mockk = "1.13.10"
|
||||||
navigationCompose = "2.7.7"
|
navigationCompose = "2.7.7"
|
||||||
|
Reference in New Issue
Block a user