- Removing outdated version of the FlowRow

- Improving performance of custom emojis
- Improving performance of DropDown menus
This commit is contained in:
Vitor Pamplona
2023-06-02 20:30:49 -04:00
parent 42428c5e0f
commit 05aa12ebac
24 changed files with 541 additions and 407 deletions
-2
View File
@@ -154,8 +154,6 @@ dependencies {
// create blurhash // create blurhash
implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0' implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0'
// Rendering clickable text
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// Permission to upload pictures: // Permission to upload pictures:
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
@@ -10,7 +10,7 @@ fun TranslatableRichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.rounded.Warning import androidx.compose.material.icons.rounded.Warning
@@ -291,7 +290,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
} else { } else {
UrlPreview(myUrlPreview, myUrlPreview) UrlPreview(myUrlPreview, myUrlPreview)
} }
} else if (isBechLink(myUrlPreview)) { } else if (startsWithNIP19Scheme(myUrlPreview)) {
BechLink( BechLink(
myUrlPreview, myUrlPreview,
true, true,
@@ -47,6 +47,9 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadChannel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -311,11 +314,51 @@ fun CreateTextWithEmoji(
overflow: TextOverflow = TextOverflow.Clip, overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val emojis = remember { var emojiList by remember(text) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
LaunchedEffect(key1 = text) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList = assembleAnnotatedList(text, emojis)
if (newEmojiList.isNotEmpty()) {
emojiList = newEmojiList.toImmutableList()
}
}
}
} }
CreateTextWithEmoji(text, emojis, color, textAlign, fontWeight, fontSize, maxLines, overflow, modifier) val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
if (emojiList.isEmpty()) {
Text(
text = text,
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize,
maxLines = maxLines,
overflow = overflow,
modifier = modifier
)
} else {
val style = LocalTextStyle.current.merge(
TextStyle(
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize
)
).toSpanStyle()
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
}
} }
@Composable @Composable
@@ -330,13 +373,26 @@ fun CreateTextWithEmoji(
overflow: TextOverflow = TextOverflow.Clip, overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var emojiList by remember(text) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
LaunchedEffect(key1 = text) {
if (emojis.isNotEmpty()) {
launch(Dispatchers.Default) {
val newEmojiList = assembleAnnotatedList(text, emojis)
if (newEmojiList.isNotEmpty()) {
emojiList = newEmojiList.toImmutableList()
}
}
}
}
val textColor = color.takeOrElse { val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse { LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current) LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
} }
} }
if (emojis.isEmpty()) { if (emojiList.isEmpty()) {
Text( Text(
text = text, text = text,
color = textColor, color = textColor,
@@ -348,10 +404,6 @@ fun CreateTextWithEmoji(
modifier = modifier modifier = modifier
) )
} else { } else {
val myList = remember {
assembleAnnotatedList(text, emojis)
}
val style = LocalTextStyle.current.merge( val style = LocalTextStyle.current.merge(
TextStyle( TextStyle(
color = textColor, color = textColor,
@@ -361,7 +413,7 @@ fun CreateTextWithEmoji(
) )
).toSpanStyle() ).toSpanStyle()
InLineIconRenderer(myList, style, maxLines, overflow, modifier) InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
} }
} }
@@ -372,22 +424,30 @@ fun CreateClickableTextWithEmoji(
style: TextStyle, style: TextStyle,
onClick: (Int) -> Unit onClick: (Int) -> Unit
) { ) {
val emojis = remember(tags) { var emojiList by remember(clickablePart) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
LaunchedEffect(key1 = clickablePart) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList = assembleAnnotatedList(clickablePart, emojis)
if (newEmojiList.isNotEmpty()) {
emojiList = newEmojiList.toImmutableList()
}
}
}
} }
if (emojis.isEmpty()) { if (emojiList.isEmpty()) {
ClickableText( ClickableText(
AnnotatedString(clickablePart), AnnotatedString(clickablePart),
style = style, style = style,
onClick = onClick onClick = onClick
) )
} else { } else {
val myList = remember { ClickableInLineIconRenderer(emojiList, style.toSpanStyle()) {
assembleAnnotatedList(clickablePart, emojis)
}
ClickableInLineIconRenderer(myList, style.toSpanStyle()) {
onClick(it) onClick(it)
} }
} }
@@ -403,30 +463,38 @@ fun CreateClickableTextWithEmoji(
route: String, route: String,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val emojis = remember(tags) { var emojiLists by remember(clickablePart) {
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap() mutableStateOf<Pair<ImmutableList<Renderable>, ImmutableList<Renderable>>?>(null)
} }
if (emojis.isEmpty()) { LaunchedEffect(key1 = clickablePart) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList1 = assembleAnnotatedList(clickablePart, emojis)
val newEmojiList2 = assembleAnnotatedList(suffix, emojis)
if (newEmojiList1.isNotEmpty() || newEmojiList2.isNotEmpty()) {
emojiLists = Pair(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList())
}
}
}
}
if (emojiLists == null) {
CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, nav) CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, nav)
} else { } else {
val myList = remember { ClickableInLineIconRenderer(emojiLists!!.first, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
assembleAnnotatedList(clickablePart, emojis)
}
ClickableInLineIconRenderer(myList, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
nav(route) nav(route)
} }
val myList2 = remember { InLineIconRenderer(emojiLists!!.second, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle())
assembleAnnotatedList(suffix, emojis)
}
InLineIconRenderer(myList2, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle())
} }
} }
fun assembleAnnotatedList(text: String, emojis: Map<String, String>): List<Renderable> { suspend fun assembleAnnotatedList(text: String, emojis: Map<String, String>): List<Renderable> {
return NIP30Parser().buildArray(text).map { return NIP30Parser().buildArray(text).map {
val url = emojis[it] val url = emojis[it]
if (url != null) { if (url != null) {
@@ -35,7 +35,7 @@ fun ExpandableRichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.util.Log import android.util.Log
import android.util.LruCache
import android.util.Patterns import android.util.Patterns
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@@ -26,7 +27,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.lnurl.LnWithdrawalUtil
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -58,6 +57,7 @@ import java.net.MalformedURLException
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URL import java.net.URL
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.time.ExperimentalTime
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg") val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3") val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
@@ -98,15 +98,13 @@ fun RichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isMarkdown = remember(content) { isMarkdown(content) }
Column(modifier = modifier) { Column(modifier = modifier) {
if (isMarkdown) { if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, backgroundColor, tags, nav) RenderContentAsMarkdown(content, backgroundColor, tags, nav)
} else { } else {
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav) RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav)
@@ -114,63 +112,33 @@ fun RichTextViewer(
} }
} }
@Stable val urlSetCache = LruCache<String, RichTextViewerState>(200)
class RichTextViewerState(
val content: String, @Immutable
data class RichTextViewerState(
val urlSet: ImmutableSet<String>, val urlSet: ImmutableSet<String>,
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>, val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
val imageList: ImmutableList<ZoomableUrlContent>, val imageList: ImmutableList<ZoomableUrlContent>,
val customEmoji: ImmutableMap<String, String> val customEmoji: ImmutableMap<String, String>
) )
@OptIn(ExperimentalTime::class, ExperimentalLayoutApi::class)
@Composable @Composable
private fun RenderRegular( private fun RenderRegular(
content: String, content: String,
tags: List<List<String>>?, tags: List<List<String>>,
canPreview: Boolean, canPreview: Boolean,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
var state by remember(content) { val state by remember(content) {
mutableStateOf( if (urlSetCache[content] != null) {
RichTextViewerState( mutableStateOf(urlSetCache[content])
content, } else {
persistentSetOf(), val newUrls = parseUrls(content, tags)
persistentMapOf(), urlSetCache.put(content, newUrls)
persistentListOf(), mutableStateOf(newUrls)
persistentMapOf()
)
)
}
LaunchedEffect(key1 = content) {
launch(Dispatchers.IO) {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
val imagesForPager = urlSet.mapNotNull { fullUrl ->
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlImage(fullUrl)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlVideo(fullUrl)
} else {
null
}
}.associateBy { it.url }
val imageList = imagesForPager.values.toList()
val emojiMap = tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (urlSet.isNotEmpty() || emojiMap.isNotEmpty()) {
state = RichTextViewerState(
content,
urlSet.toImmutableSet(),
imagesForPager.toImmutableMap(),
imageList.toImmutableList(),
emojiMap.toImmutableMap()
)
}
} }
} }
@@ -185,12 +153,62 @@ private fun RenderRegular(
} }
} }
s.forEach { word: String -> s.forEach { word: String ->
RenderWord(word, state, canPreview, backgroundColor, accountViewModel, nav, tags) RenderWord(
word,
state,
canPreview,
backgroundColor,
accountViewModel,
nav,
tags
)
} }
} }
} }
} }
private fun parseUrls(
content: String,
tags: List<List<String>>
): RichTextViewerState {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
val imagesForPager = urlSet.mapNotNull { fullUrl ->
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlImage(fullUrl)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlVideo(fullUrl)
} else {
null
}
}.associateBy { it.url }
val imageList = imagesForPager.values.toList()
val emojiMap =
tags.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
return if (urlSet.isNotEmpty() || emojiMap.isNotEmpty()) {
RichTextViewerState(
urlSet.toImmutableSet(),
imagesForPager.toImmutableMap(),
imageList.toImmutableList(),
emojiMap.toImmutableMap()
)
} else {
RichTextViewerState(
persistentSetOf(),
persistentMapOf(),
persistentListOf(),
persistentMapOf()
)
}
}
enum class WordType {
IMAGE, LINK, EMOJI, INVOICE, WITHDRAW, EMAIL, PHONE, BECH, HASH_INDEX, HASHTAG, SCHEMELESS_URL, OTHER
}
@Composable @Composable
private fun RenderWord( private fun RenderWord(
word: String, word: String,
@@ -199,150 +217,167 @@ private fun RenderWord(
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
tags: List<List<String>>? tags: List<List<String>>
) {
val type = remember(word) {
if (state.imagesForPager[word] != null) {
WordType.IMAGE
} else if (state.urlSet.contains(word)) {
WordType.LINK
} else if (state.customEmoji.any { word.contains(it.key) }) {
WordType.EMOJI
} else if (word.startsWith("lnbc", true)) {
WordType.INVOICE
} else if (word.startsWith("lnurl", true)) {
WordType.WITHDRAW
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
WordType.EMAIL
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
WordType.PHONE
} else if (startsWithNIP19Scheme(word)) {
WordType.BECH
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches()) {
if (tags.isNotEmpty()) {
WordType.HASH_INDEX
} else {
WordType.OTHER
}
} else if (hashTagsPattern.matcher(word).matches()) {
WordType.HASHTAG
} else {
WordType.OTHER
}
} else if (noProtocolUrlValidator.matcher(word).matches()) {
WordType.SCHEMELESS_URL
} else {
WordType.OTHER
}
}
if (canPreview) {
RenderWordWithPreview(type, word, state, tags, backgroundColor, accountViewModel, nav)
} else {
RenderWordWithoutPreview(type, word, state, tags, backgroundColor, accountViewModel, nav)
}
}
@Composable
private fun RenderWordWithoutPreview(
type: WordType,
word: String,
state: RichTextViewerState,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
val wordSpace = remember(word) { val wordSpace = remember(word) {
"$word " "$word "
} }
if (canPreview) { when (type) {
// Explicit URL // Don't preview Images
val img = state.imagesForPager[word] WordType.IMAGE -> UrlPreview(word, wordSpace)
if (img != null) { WordType.LINK -> UrlPreview(word, wordSpace)
ZoomableContentView(img, state.imageList) WordType.EMOJI -> RenderCustomEmoji(word, state)
} else if (state.urlSet.contains(word)) { // Don't offer to pay invoices
UrlPreview(word, wordSpace) WordType.INVOICE -> NormalWord(wordSpace)
} else if (state.customEmoji.any { word.contains(it.key) }) { // Don't offer to withdraw
RenderCustomEmoji(word, state.customEmoji) WordType.WITHDRAW -> NormalWord(wordSpace)
} else if (word.startsWith("lnbc", true)) { WordType.EMAIL -> ClickableEmail(word)
MayBeInvoicePreview(word) WordType.PHONE -> ClickablePhone(word)
} else if (word.startsWith("lnurl", true)) { WordType.BECH -> BechLink(word, false, backgroundColor, accountViewModel, nav)
MayBeWithdrawal(word) WordType.HASHTAG -> HashTag(word, nav)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { WordType.HASH_INDEX -> TagLink(word, tags, false, backgroundColor, accountViewModel, nav)
ClickableEmail(word) WordType.SCHEMELESS_URL -> NoProtocolUrlRenderer(word)
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { WordType.OTHER -> NormalWord(wordSpace)
ClickablePhone(word)
} else if (isBechLink(word)) {
BechLink(
word,
canPreview,
backgroundColor,
accountViewModel,
nav
)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
nav
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, nav)
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (noProtocolUrlValidator.matcher(word).matches()) {
val matcher = noProtocolUrlValidator.matcher(word)
matcher.find()
val url = matcher.group(1) // url
val additionalChars = matcher.group(4) ?: "" // additional chars
if (url != null) {
ClickableUrl(url, "https://$url")
Text("$additionalChars ")
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else {
if (state.urlSet.contains(word)) {
ClickableUrl(wordSpace, word)
} else if (word.startsWith("lnurl", true)) {
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
if (lnWithdrawal != null) {
ClickableWithdrawal(withdrawalString = lnWithdrawal)
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (state.customEmoji.any { word.contains(it.key) }) {
RenderCustomEmoji(word, state.customEmoji)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word)
} else if (isBechLink(word)) {
BechLink(
word,
canPreview,
backgroundColor,
accountViewModel,
nav
)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
nav
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, nav)
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (noProtocolUrlValidator.matcher(word).matches()) {
val matcher = noProtocolUrlValidator.matcher(word)
matcher.find()
val url = matcher.group(1) // url
val additionalChars = matcher.group(4) ?: "" // additional chars
if (url != null) {
ClickableUrl(url, "https://$url")
Text("$additionalChars ")
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} }
} }
@Composable @Composable
fun RenderCustomEmoji(word: String, customEmoji: Map<String, String>) { private fun RenderWordWithPreview(
type: WordType,
word: String,
state: RichTextViewerState,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val wordSpace = remember(word) {
"$word "
}
when (type) {
WordType.IMAGE -> ZoomableContentView(word, state)
WordType.LINK -> UrlPreview(word, wordSpace)
WordType.EMOJI -> RenderCustomEmoji(word, state)
WordType.INVOICE -> MayBeInvoicePreview(word)
WordType.WITHDRAW -> MayBeWithdrawal(word)
WordType.EMAIL -> ClickableEmail(word)
WordType.PHONE -> ClickablePhone(word)
WordType.BECH -> BechLink(word, true, backgroundColor, accountViewModel, nav)
WordType.HASHTAG -> HashTag(word, nav)
WordType.HASH_INDEX -> TagLink(word, tags, true, backgroundColor, accountViewModel, nav)
WordType.SCHEMELESS_URL -> NoProtocolUrlRenderer(word)
WordType.OTHER -> NormalWord(wordSpace)
}
}
@Composable
private fun ZoomableContentView(word: String, state: RichTextViewerState) {
state.imagesForPager[word]?.let {
ZoomableContentView(it, state.imageList)
}
}
@Composable
private fun NormalWord(word: String) {
Text(
text = word,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@Composable
private fun NoProtocolUrlRenderer(word: String) {
val wordSpace = remember(word) {
"$word "
}
var linkedUrl by remember(word) {
mutableStateOf<Pair<String, String>?>(null)
}
LaunchedEffect(key1 = word) {
launch(Dispatchers.Default) {
val matcher = noProtocolUrlValidator.matcher(word)
if (matcher.find()) {
val url = matcher.group(1) // url
val additionalChars = matcher.group(4) ?: "" // additional chars
linkedUrl = Pair(url, additionalChars)
}
}
}
if (linkedUrl != null) {
ClickableUrl(linkedUrl!!.first, "https://${linkedUrl!!.first}")
Text("${linkedUrl!!.second} ")
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
@Composable
fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = "$word ", text = remember { "$word " },
emojis = customEmoji emojis = state.customEmoji
) )
} }
@@ -382,19 +417,38 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
) )
) )
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(null) } val uri = LocalUriHandler.current
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) } val onClick = { link: String ->
var refresh by remember(content) { mutableStateOf(0) } val route = uriToRoute(link)
if (route != null) {
LaunchedEffect(key1 = content) { nav(route)
launch(Dispatchers.IO) { } else {
nip19References = returnNIP19References(content, tags) runCatching { uri.openUri(link) }
markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
} }
Unit
} }
LaunchedEffect(key1 = refresh) { MaterialRichText(
launch(Dispatchers.IO) { style = myMarkDownStyle
) {
RefreshableContent(content, tags) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default,
onLinkClicked = onClick
)
}
}
}
@Composable
private fun RefreshableContent(content: String, tags: List<List<String>>?, onCompose: @Composable (String) -> Unit) {
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
val scope = rememberCoroutineScope()
ObserverAllNIP19References(content, tags) {
scope.launch(Dispatchers.IO) {
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent markdownWithSpecialContent = newMarkdownWithSpecialContent
@@ -402,30 +456,22 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
} }
} }
nip19References.forEach { markdownWithSpecialContent?.let {
ObserveNIP19(it) { onCompose(it)
refresh++ }
}
@Composable
fun ObserverAllNIP19References(content: String, tags: List<List<String>>?, onRefresh: () -> Unit) {
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
LaunchedEffect(key1 = content) {
launch(Dispatchers.IO) {
nip19References = returnNIP19References(content, tags)
} }
} }
val uri = LocalUriHandler.current nip19References.forEach {
ObserveNIP19(it, onRefresh)
markdownWithSpecialContent?.let {
MaterialRichText(
style = myMarkDownStyle
) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default
) { link ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
}
}
}
} }
} }
@@ -444,7 +490,7 @@ fun ObserveNIP19(
@Composable @Composable
private fun ObserveNIP19Event( private fun ObserveNIP19Event(
it: Nip19.Return, it: Nip19.Return,
onRefresh: () -> Unit onRefresh: suspend () -> Unit
) { ) {
var baseNote by remember(it) { mutableStateOf<Note?>(null) } var baseNote by remember(it) { mutableStateOf<Note?>(null) }
@@ -560,7 +606,7 @@ private fun returnNIP19References(content: String, tags: List<List<String>>?): L
val listOfReferences = mutableListOf<Nip19.Return>() val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph -> content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String -> paragraph.split(' ').forEach { word: String ->
if (isBechLink(word)) { if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word) val parsedNip19 = Nip19.uriToRoute(word)
parsedNip19?.let { parsedNip19?.let {
listOfReferences.add(it) listOfReferences.add(it)
@@ -587,12 +633,17 @@ private fun returnMarkdownWithSpecialContent(content: String, tags: List<List<St
content.split('\n').forEach { paragraph -> content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String -> paragraph.split(' ').forEach { word: String ->
if (isValidURL(word)) { if (isValidURL(word)) {
returnContent += "[$word]($word) " val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
returnContent += "$word "
} else {
returnContent += "[$word]($word) "
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
returnContent += "[$word](mailto:$word) " returnContent += "[$word](mailto:$word) "
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
returnContent += "[$word](tel:$word) " returnContent += "[$word](tel:$word) "
} else if (isBechLink(word)) { } else if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word) val parsedNip19 = Nip19.uriToRoute(word)
returnContent += if (parsedNip19 !== null) { returnContent += if (parsedNip19 !== null) {
val pair = getDisplayNameFromNip19(parsedNip19) val pair = getDisplayNameFromNip19(parsedNip19)
@@ -645,7 +696,7 @@ private fun isArabic(text: String): Boolean {
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
} }
fun isBechLink(word: String): Boolean { fun startsWithNIP19Scheme(word: String): Boolean {
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@") val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) } return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
@@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -65,7 +67,6 @@ import androidx.compose.ui.window.DialogProperties
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.imageLoader import coil.imageLoader
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.BlurHashRequester import com.vitorpamplona.amethyst.service.BlurHashRequester
@@ -208,6 +209,7 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LocalImageView( private fun LocalImageView(
content: ZoomableLocalImage, content: ZoomableLocalImage,
@@ -271,6 +273,7 @@ private fun LocalImageView(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun UrlImageView( private fun UrlImageView(
content: ZoomableUrlImage, content: ZoomableUrlImage,
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -17,7 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -53,6 +54,7 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: St
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun HiddenNote( fun HiddenNote(
reports: Set<Note>, reports: Set<Note>,
@@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
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.layout.size
@@ -25,7 +27,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
@@ -35,7 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by boostSetCard.note.live().metadata.observeAsState() val noteState by boostSetCard.note.live().metadata.observeAsState()
@@ -7,6 +7,8 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -48,7 +50,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -364,12 +365,13 @@ private fun RenderRegularTextNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val tags = remember { note.event?.tags() } val tags = remember { note.event?.tags() ?: emptyList() }
val eventContent = remember { accountViewModel.decrypt(note) } val eventContent = remember { accountViewModel.decrypt(note) }
val modifier = remember { Modifier.padding(top = 5.dp) } val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) { if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning( SensitivityWarning(
hasSensitiveContent = hasSensitiveContent, hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel accountViewModel = accountViewModel
@@ -495,6 +497,7 @@ data class RelayBadgesState(
val noteRelaysSimple: List<String> val noteRelaysSimple: List<String>
) )
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun RelayBadges(baseNote: Note) { private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState() val noteRelaysState by baseNote.live().relays.observeAsState()
@@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
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.layout.size
@@ -25,7 +27,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
@@ -35,7 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by likeSetCard.note.live().metadata.observeAsState() val noteState by likeSetCard.note.live().metadata.observeAsState()
@@ -6,6 +6,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -38,7 +40,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -249,6 +250,7 @@ private fun RenderBoostGallery(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun AuthorGalleryZaps( fun AuthorGalleryZaps(
authorNotes: ImmutableMap<Note, Note>, authorNotes: ImmutableMap<Note, Note>,
@@ -366,7 +368,7 @@ private fun AuthorPictureAndComment(
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = it, content = it,
canPreview = true, canPreview = true,
tags = null, tags = remember { emptyList() },
modifier = remember { Modifier.fillMaxWidth() }, modifier = remember { Modifier.fillMaxWidth() },
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -376,6 +378,7 @@ private fun AuthorPictureAndComment(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun AuthorGallery( fun AuthorGallery(
authorNotes: ImmutableList<Note>, authorNotes: ImmutableList<Note>,
@@ -12,6 +12,8 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -80,7 +82,6 @@ import androidx.core.graphics.get
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil.compose.AsyncImagePainter
import coil.request.SuccessResult import coil.request.SuccessResult
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
@@ -139,7 +140,6 @@ import java.math.BigDecimal
import java.net.URL import java.net.URL
import java.util.Locale import java.util.Locale
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -156,12 +156,10 @@ fun NoteCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
println("NormalNote START NoteCompose")
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event } val noteEvent = remember(noteState) { noteState?.note?.event }
if (noteEvent == null) { if (noteEvent == null) {
println("NormalNote Rendering Blank")
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
BlankNote( BlankNote(
@@ -190,7 +188,6 @@ fun NoteCompose(
nav nav
) )
} }
println("NormalNote END NoteCompose")
} }
@Composable @Composable
@@ -371,8 +368,6 @@ fun NormalNote(
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote) FileStorageHeaderDisplay(baseNote)
} else { } else {
println("NormalNote LoadedNoteCompose")
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -395,8 +390,6 @@ fun NormalNote(
} }
} }
println("NormalNote LoadedNoteCompose Launched Effect")
val primaryColor = MaterialTheme.colors.newItemBackgroundColor val primaryColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background val defaultBackgroundColor = MaterialTheme.colors.background
@@ -412,8 +405,6 @@ fun NormalNote(
} }
} }
println("NormalNote LoadedNoteCompose Background Color")
val columnModifier = remember(backgroundColor) { val columnModifier = remember(backgroundColor) {
modifier modifier
.combinedClickable( .combinedClickable(
@@ -429,8 +420,6 @@ fun NormalNote(
.background(backgroundColor) .background(backgroundColor)
} }
println("NormalNote LoadedNoteCompose Modifier")
Column(modifier = columnModifier) { Column(modifier = columnModifier) {
Row( Row(
modifier = remember { modifier = remember {
@@ -442,14 +431,9 @@ fun NormalNote(
) )
} }
) { ) {
println("NormalNote Before FirstUserInfo") if (!isBoostedNote && !isQuotedNote) {
val (value, elapsed) = measureTimedValue { DrawAuthorImages(baseNote, accountViewModel, nav)
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
}
} }
println("AAA $elapsed DrawAuthorImages")
println("NormalNote FirstUserInfo")
Column( Column(
modifier = remember { modifier = remember {
@@ -471,7 +455,6 @@ fun NormalNote(
nav nav
) )
} }
println("NormalNote SecondUserInfo")
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@@ -601,7 +584,7 @@ private fun RenderTextEvent(
accountViewModel = accountViewModel accountViewModel = accountViewModel
) { ) {
val modifier = remember(note.event) { Modifier.fillMaxWidth() } val modifier = remember(note.event) { Modifier.fillMaxWidth() }
val tags = remember(note.event) { note.event?.tags() } val tags = remember(note.event) { note.event?.tags() ?: emptyList() }
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = eventContent, content = eventContent,
@@ -737,7 +720,7 @@ private fun RenderPrivateMessage(
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() } val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
val tags = remember(note.event?.id()) { note.event?.tags() } val tags = remember(note.event?.id()) { note.event?.tags() ?: emptyList() }
if (eventContent != null) { if (eventContent != null) {
if (makeItShort && isAuthorTheLoggedUser) { if (makeItShort && isAuthorTheLoggedUser) {
@@ -812,6 +795,7 @@ fun RenderPeopleList(
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplayPeopleList( fun DisplayPeopleList(
baseNote: Note, baseNote: Note,
@@ -900,6 +884,7 @@ fun DisplayPeopleList(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun RenderBadgeAward( private fun RenderBadgeAward(
note: Note, note: Note,
@@ -1039,6 +1024,7 @@ private fun RenderPinListEvent(
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun PinListHeader( fun PinListHeader(
baseNote: Note, baseNote: Note,
@@ -1087,7 +1073,7 @@ fun PinListHeader(
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = pin, content = pin,
canPreview = true, canPreview = true,
tags = emptyList(), tags = remember { emptyList() },
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -1197,7 +1183,7 @@ private fun RenderReport(
content = content, content = content,
canPreview = true, canPreview = true,
modifier = remember { Modifier }, modifier = remember { Modifier },
tags = emptyList(), tags = remember { emptyList() },
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -1311,7 +1297,6 @@ private fun FirstUserInfoRow(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val eventNote = remember { baseNote.event } ?: return val eventNote = remember { baseNote.event } ?: return
val time = remember { baseNote.createdAt() } ?: return val time = remember { baseNote.createdAt() } ?: return
val padding = remember { val padding = remember {
@@ -1339,24 +1324,34 @@ private fun FirstUserInfoRow(
TimeAgo(time) TimeAgo(time)
IconButton( MoreOptionsButton(baseNote, accountViewModel)
modifier = Modifier.size(24.dp), }
onClick = { moreActionsExpanded = true } }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu( @Composable
baseNote, private fun MoreOptionsButton(
moreActionsExpanded, baseNote: Note,
{ moreActionsExpanded = false }, accountViewModel: AccountViewModel
accountViewModel ) {
) var moreActionsExpanded by remember { mutableStateOf(false) }
}
IconButton(
modifier = Modifier.size(24.dp),
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu(
baseNote,
moreActionsExpanded,
{ moreActionsExpanded = false },
accountViewModel
)
} }
} }
@@ -1390,27 +1385,17 @@ private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel,
Column(modifier) { Column(modifier) {
// Draws the boosted picture outside the boosted card. // Draws the boosted picture outside the boosted card.
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) { Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
val (value1, elapsed1) = measureTimedValue { NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
} }
println("AAA $elapsed1 NoteAuthorPicture") if (baseNote.event is ChannelMessageEvent && baseChannelHex != null) {
LoadChannel(baseChannelHex) { channel ->
val (value2, elapsed2) = measureTimedValue { ChannelNotePicture(channel)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
} }
} }
println("AAA $elapsed2 RepostNoteAuthorPicture")
val (value3, elapsed3) = measureTimedValue {
if (baseNote.event is ChannelMessageEvent && baseChannelHex != null) {
LoadChannel(baseChannelHex) { channel ->
ChannelNotePicture(channel)
}
}
}
println("AAA $elapsed3 ChannelNotePicture")
} }
if (baseNote.event is RepostEvent) { if (baseNote.event is RepostEvent) {
@@ -1492,9 +1477,7 @@ private fun RepostNoteAuthorPicture(
val baseRepost = remember { baseNote.replyTo?.lastOrNull() } val baseRepost = remember { baseNote.replyTo?.lastOrNull() }
val modifier = remember { val modifier = remember {
Modifier Modifier.size(30.dp)
.width(30.dp)
.height(30.dp)
} }
baseRepost?.let { baseRepost?.let {
@@ -1503,7 +1486,7 @@ private fun RepostNoteAuthorPicture(
baseNote = it, baseNote = it,
nav = nav, nav = nav,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
size = 35.dp, size = 30.dp,
pictureModifier = Modifier.border( pictureModifier = Modifier.border(
2.dp, 2.dp,
MaterialTheme.colors.background, MaterialTheme.colors.background,
@@ -1514,6 +1497,7 @@ private fun RepostNoteAuthorPicture(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplayHighlight( fun DisplayHighlight(
highlight: String, highlight: String,
@@ -1598,15 +1582,16 @@ fun DisplayFollowingHashtagsInPost(
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val followingTags = remember(accountState) {
accountState?.account?.followingTagSet() ?: emptySet()
}
var firstTag by remember { mutableStateOf<String?>(null) } var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteEvent.id(), followingTags) { LaunchedEffect(key1 = accountState) {
withContext(Dispatchers.IO) { launch(Dispatchers.Default) {
firstTag = noteEvent.firstIsTaggedHashes(followingTags) val followingTags = accountState?.account?.followingTagSet() ?: emptySet()
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
if (firstTag != newFirstTag) {
firstTag = newFirstTag
}
} }
} }
@@ -1630,6 +1615,7 @@ fun DisplayFollowingHashtagsInPost(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplayUncitedHashtags( fun DisplayUncitedHashtags(
hashtags: List<String>, hashtags: List<String>,
@@ -2159,6 +2145,7 @@ private fun RelayBadges(baseNote: Note) {
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@Stable @Stable
private fun VerticalRelayPanelWithFlow( private fun VerticalRelayPanelWithFlow(
@@ -2229,30 +2216,19 @@ fun NoteAuthorPicture(
} }
} }
val boxModifier = remember { if (author == null) {
Modifier val nullModifier = remember {
.width(size) modifier.size(size).clip(shape = CircleShape)
.height(size)
}
val nullModifier = remember {
modifier
.width(size)
.height(size)
.clip(shape = CircleShape)
}
Box(boxModifier) {
if (author == null) {
RobohashAsyncImage(
robot = "authornotfound",
robotSize = size,
contentDescription = stringResource(R.string.unknown_author),
modifier = nullModifier.background(MaterialTheme.colors.background)
)
} else {
UserPicture(author!!, size, accountViewModel, modifier, onClick)
} }
RobohashAsyncImage(
robot = "authornotfound",
robotSize = size,
contentDescription = stringResource(R.string.unknown_author),
modifier = nullModifier.background(MaterialTheme.colors.background)
)
} else {
UserPicture(author!!, size, accountViewModel, modifier, onClick)
} }
} }
@@ -2316,6 +2292,7 @@ fun UserPicture(
} }
} }
@OptIn(ExperimentalTime::class)
@Composable @Composable
fun UserPicture( fun UserPicture(
userHex: String, userHex: String,
@@ -2401,6 +2378,7 @@ private fun FollowingIcon(iconSize: Dp) {
} }
} }
@Immutable
data class DropDownParams( data class DropDownParams(
val isFollowingAuthor: Boolean, val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean, val isPrivateBookmarkNote: Boolean,
@@ -2417,32 +2395,29 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
val actContext = LocalContext.current val actContext = LocalContext.current
var reportDialogShowing by remember { mutableStateOf(false) } var reportDialogShowing by remember { mutableStateOf(false) }
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
var state by remember { var state by remember {
mutableStateOf<DropDownParams>( mutableStateOf<DropDownParams>(
DropDownParams(false, false, false, false, false, null) DropDownParams(
) isFollowingAuthor = false,
} isPrivateBookmarkNote = false,
isPublicBookmarkNote = false,
LaunchedEffect(key1 = note, key2 = bookmarkState, key3 = accountState) { isLoggedUser = false,
withContext(Dispatchers.IO) { isSensitive = false,
state = DropDownParams( showSensitiveContent = null
isFollowingAuthor = accountViewModel.isFollowing(note.author),
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = accountState?.account?.showSensitiveContent
) )
} )
} }
DropdownMenu( DropdownMenu(
expanded = popupExpanded, expanded = popupExpanded,
onDismissRequest = onDismiss onDismissRequest = onDismiss
) { ) {
WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
if (!state.isFollowingAuthor) { if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
accountViewModel.follow( accountViewModel.follow(
@@ -2512,7 +2487,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
Text("Block / Report") Text("Block / Report")
} }
} }
// if (state.isSensitive) {
Divider() Divider()
if (state.showSensitiveContent == null || state.showSensitiveContent == true) { if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) { DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) {
@@ -2529,7 +2503,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
Text(stringResource(R.string.content_warning_see_warnings)) Text(stringResource(R.string.content_warning_see_warnings))
} }
} }
// }
} }
if (reportDialogShowing) { if (reportDialogShowing) {
@@ -2539,3 +2512,25 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
} }
} }
} }
@Composable
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
val followState by accountViewModel.userProfile().live().follows.observeAsState()
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = accountState) {
launch(Dispatchers.IO) {
onNew(
DropDownParams(
isFollowingAuthor = accountViewModel.isFollowing(note.author),
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = accountState?.account?.showSensitiveContent
)
)
}
}
}
@@ -110,7 +110,7 @@ private fun OptionNote(
backgroundColor: Color, backgroundColor: Color,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val tags = remember { baseNote.event?.tags() } val tags = remember { baseNote.event?.tags() ?: emptyList() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -164,7 +164,7 @@ private fun RenderOptionAfterVote(
totalRatio: Float, totalRatio: Float,
color: Color, color: Color,
canPreview: Boolean, canPreview: Boolean,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -231,7 +231,7 @@ private fun RenderOptionAfterVote(
private fun RenderOptionBeforeVote( private fun RenderOptionBeforeVote(
description: String, description: String,
canPreview: Boolean, canPreview: Boolean,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -1,5 +1,7 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@@ -14,7 +16,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
@@ -45,6 +46,7 @@ fun ReplyInformation(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun ReplyInformation( private fun ReplyInformation(
replyTo: List<Note>?, replyTo: List<Note>?,
@@ -166,6 +168,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ReplyInformationChannel( fun ReplyInformationChannel(
replyTo: List<Note>?, replyTo: List<Note>?,
@@ -16,7 +16,9 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@Composable @Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) { fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val author = remember(noteState) { noteState?.note?.author } ?: return val author = remember(noteState) {
noteState?.note?.author
} ?: return
UsernameDisplay(author, weight) UsernameDisplay(author, weight)
} }
@@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
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.layout.size
@@ -26,7 +28,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
@@ -37,7 +38,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by zapSetCard.note.live().metadata.observeAsState() val noteState by zapSetCard.note.live().metadata.observeAsState()
@@ -4,6 +4,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
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.layout.size
@@ -23,7 +25,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
@@ -33,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
@@ -34,7 +34,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -166,17 +165,14 @@ private fun FeedLoaded(
state = listState state = listState
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val (value, elapsed) = measureTimedValue { NoteCompose(
NoteCompose( item,
item, routeForLastRead = routeForLastRead,
routeForLastRead = routeForLastRead, modifier = baseModifier,
modifier = baseModifier, isBoostedNote = false,
isBoostedNote = false, accountViewModel = accountViewModel,
accountViewModel = accountViewModel, nav = nav
nav = nav )
)
}
println("AAA NoteCompose $elapsed ${item.event?.content()}")
} }
} }
} }
@@ -381,6 +381,10 @@ fun NoteMaster(
if (eventContent != null) { if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
val tags = remember(note) {
note.event?.tags() ?: emptyList()
}
SensitivityWarning( SensitivityWarning(
hasSensitiveContent = hasSensitiveContent, hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel accountViewModel = accountViewModel
@@ -389,7 +393,7 @@ fun NoteMaster(
eventContent, eventContent,
canPreview, canPreview,
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
note.event?.tags(), tags,
MaterialTheme.colors.background, MaterialTheme.colors.background,
accountViewModel, accountViewModel,
nav nav
@@ -699,7 +699,7 @@ private fun DrawAdditionalInfo(baseUser: User, accountViewModel: AccountViewMode
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = it, content = it,
canPreview = false, canPreview = false,
tags = null, tags = remember { emptyList() },
backgroundColor = MaterialTheme.colors.background, backgroundColor = MaterialTheme.colors.background,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -10,6 +10,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -54,7 +56,6 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@@ -313,6 +314,7 @@ private fun RenderVideoOrPictureNote(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun RelayBadges(baseNote: Note) { private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState() val noteRelaysState by baseNote.live().relays.observeAsState()
@@ -27,7 +27,7 @@ object LanguageTranslatorService {
val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE) val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE)
private val translators = private val translators =
object : LruCache<TranslatorOptions, Translator>(10) { object : LruCache<TranslatorOptions, Translator>(20) {
override fun create(options: TranslatorOptions): Translator { override fun create(options: TranslatorOptions): Translator {
return Translation.getClient(options) return Translation.getClient(options)
} }
@@ -43,7 +43,7 @@ fun TranslatableRichTextViewer(
content: String, content: String,
canPreview: Boolean, canPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<List<String>>?, tags: List<List<String>>,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -55,7 +55,7 @@ fun TranslatableRichTextViewer(
var showOriginal by remember { mutableStateOf(false) } var showOriginal by remember { mutableStateOf(false) }
var langSettingsPopupExpanded by remember { mutableStateOf(false) } var langSettingsPopupExpanded by remember { mutableStateOf(false) }
WatchLanguageChanges(content, accountViewModel) { result, newShowOriginal -> TranslateAndWatchLanguageChanges(content, accountViewModel) { result, newShowOriginal ->
if (translatedTextState.result != result.result || if (translatedTextState.result != result.result ||
translatedTextState.sourceLang != result.sourceLang || translatedTextState.sourceLang != result.sourceLang ||
translatedTextState.targetLang != result.targetLang translatedTextState.targetLang != result.targetLang
@@ -227,7 +227,7 @@ fun TranslatableRichTextViewer(
} }
@Composable @Composable
fun WatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (ResultOrError, Boolean) -> Unit) { fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (ResultOrError, Boolean) -> Unit) {
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState() val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return val account = remember(accountState) { accountState?.account } ?: return