- 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
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:
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
@@ -10,7 +10,7 @@ fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
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.Visibility
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.Bolt
import androidx.compose.material.icons.rounded.Warning
@@ -291,7 +290,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
} else {
UrlPreview(myUrlPreview, myUrlPreview)
}
} else if (isBechLink(myUrlPreview)) {
} else if (startsWithNIP19Scheme(myUrlPreview)) {
BechLink(
myUrlPreview,
true,
@@ -47,6 +47,9 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19
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.launch
@@ -311,11 +314,51 @@ fun CreateTextWithEmoji(
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier
) {
val emojis = remember {
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
var emojiList by remember(text) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
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
@@ -330,13 +373,26 @@ fun CreateTextWithEmoji(
overflow: TextOverflow = TextOverflow.Clip,
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 {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
if (emojis.isEmpty()) {
if (emojiList.isEmpty()) {
Text(
text = text,
color = textColor,
@@ -348,10 +404,6 @@ fun CreateTextWithEmoji(
modifier = modifier
)
} else {
val myList = remember {
assembleAnnotatedList(text, emojis)
}
val style = LocalTextStyle.current.merge(
TextStyle(
color = textColor,
@@ -361,7 +413,7 @@ fun CreateTextWithEmoji(
)
).toSpanStyle()
InLineIconRenderer(myList, style, maxLines, overflow, modifier)
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
}
}
@@ -372,22 +424,30 @@ fun CreateClickableTextWithEmoji(
style: TextStyle,
onClick: (Int) -> Unit
) {
val emojis = remember(tags) {
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
var emojiList by remember(clickablePart) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
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(
AnnotatedString(clickablePart),
style = style,
onClick = onClick
)
} else {
val myList = remember {
assembleAnnotatedList(clickablePart, emojis)
}
ClickableInLineIconRenderer(myList, style.toSpanStyle()) {
ClickableInLineIconRenderer(emojiList, style.toSpanStyle()) {
onClick(it)
}
}
@@ -403,30 +463,38 @@ fun CreateClickableTextWithEmoji(
route: String,
nav: (String) -> Unit
) {
val emojis = remember(tags) {
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
var emojiLists by remember(clickablePart) {
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)
} else {
val myList = remember {
assembleAnnotatedList(clickablePart, emojis)
}
ClickableInLineIconRenderer(myList, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
ClickableInLineIconRenderer(emojiLists!!.first, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
nav(route)
}
val myList2 = remember {
assembleAnnotatedList(suffix, emojis)
}
InLineIconRenderer(myList2, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle())
InLineIconRenderer(emojiLists!!.second, 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 {
val url = emojis[it]
if (url != null) {
@@ -35,7 +35,7 @@ fun ExpandableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.LruCache
import android.util.Patterns
import androidx.compose.foundation.background
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.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
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.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -58,6 +57,7 @@ import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.regex.Pattern
import kotlin.time.ExperimentalTime
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
@@ -98,15 +98,13 @@ fun RichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isMarkdown = remember(content) { isMarkdown(content) }
Column(modifier = modifier) {
if (isMarkdown) {
if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, backgroundColor, tags, nav)
} else {
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav)
@@ -114,63 +112,33 @@ fun RichTextViewer(
}
}
@Stable
class RichTextViewerState(
val content: String,
val urlSetCache = LruCache<String, RichTextViewerState>(200)
@Immutable
data class RichTextViewerState(
val urlSet: ImmutableSet<String>,
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
val imageList: ImmutableList<ZoomableUrlContent>,
val customEmoji: ImmutableMap<String, String>
)
@OptIn(ExperimentalTime::class, ExperimentalLayoutApi::class)
@Composable
private fun RenderRegular(
content: String,
tags: List<List<String>>?,
tags: List<List<String>>,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var state by remember(content) {
mutableStateOf(
RichTextViewerState(
content,
persistentSetOf(),
persistentMapOf(),
persistentListOf(),
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()
)
}
val state by remember(content) {
if (urlSetCache[content] != null) {
mutableStateOf(urlSetCache[content])
} else {
val newUrls = parseUrls(content, tags)
urlSetCache.put(content, newUrls)
mutableStateOf(newUrls)
}
}
@@ -185,12 +153,62 @@ private fun RenderRegular(
}
}
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
private fun RenderWord(
word: String,
@@ -199,150 +217,167 @@ private fun RenderWord(
backgroundColor: Color,
accountViewModel: AccountViewModel,
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) {
"$word "
}
if (canPreview) {
// Explicit URL
val img = state.imagesForPager[word]
if (img != null) {
ZoomableContentView(img, state.imageList)
} else if (state.urlSet.contains(word)) {
UrlPreview(word, wordSpace)
} else if (state.customEmoji.any { word.contains(it.key) }) {
RenderCustomEmoji(word, state.customEmoji)
} else if (word.startsWith("lnbc", true)) {
MayBeInvoicePreview(word)
} else if (word.startsWith("lnurl", true)) {
MayBeWithdrawal(word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
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)
)
}
when (type) {
// Don't preview Images
WordType.IMAGE -> UrlPreview(word, wordSpace)
WordType.LINK -> UrlPreview(word, wordSpace)
WordType.EMOJI -> RenderCustomEmoji(word, state)
// Don't offer to pay invoices
WordType.INVOICE -> NormalWord(wordSpace)
// Don't offer to withdraw
WordType.WITHDRAW -> NormalWord(wordSpace)
WordType.EMAIL -> ClickableEmail(word)
WordType.PHONE -> ClickablePhone(word)
WordType.BECH -> BechLink(word, false, backgroundColor, accountViewModel, nav)
WordType.HASHTAG -> HashTag(word, nav)
WordType.HASH_INDEX -> TagLink(word, tags, false, backgroundColor, accountViewModel, nav)
WordType.SCHEMELESS_URL -> NoProtocolUrlRenderer(word)
WordType.OTHER -> NormalWord(wordSpace)
}
}
@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(
text = "$word ",
emojis = customEmoji
text = remember { "$word " },
emojis = state.customEmoji
)
}
@@ -382,19 +417,38 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
)
)
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(null) }
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
var refresh by remember(content) { mutableStateOf(0) }
LaunchedEffect(key1 = content) {
launch(Dispatchers.IO) {
nip19References = returnNIP19References(content, tags)
markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
val uri = LocalUriHandler.current
val onClick = { link: String ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
}
Unit
}
LaunchedEffect(key1 = refresh) {
launch(Dispatchers.IO) {
MaterialRichText(
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)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
@@ -402,30 +456,22 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
}
}
nip19References.forEach {
ObserveNIP19(it) {
refresh++
markdownWithSpecialContent?.let {
onCompose(it)
}
}
@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
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) }
}
}
}
nip19References.forEach {
ObserveNIP19(it, onRefresh)
}
}
@@ -444,7 +490,7 @@ fun ObserveNIP19(
@Composable
private fun ObserveNIP19Event(
it: Nip19.Return,
onRefresh: () -> Unit
onRefresh: suspend () -> Unit
) {
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>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (isBechLink(word)) {
if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
parsedNip19?.let {
listOfReferences.add(it)
@@ -587,12 +633,17 @@ private fun returnMarkdownWithSpecialContent(content: String, tags: List<List<St
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
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()) {
returnContent += "[$word](mailto:$word) "
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
returnContent += "[$word](tel:$word) "
} else if (isBechLink(word)) {
} else if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
returnContent += if (parsedNip19 !== null) {
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' }
}
fun isBechLink(word: String): Boolean {
fun startsWithNIP19Scheme(word: String): Boolean {
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
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.BoxWithConstraints
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -65,7 +67,6 @@ import androidx.compose.ui.window.DialogProperties
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.imageLoader
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.BlurHashRequester
@@ -208,6 +209,7 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocalImageView(
content: ZoomableLocalImage,
@@ -271,6 +273,7 @@ private fun LocalImageView(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun UrlImageView(
content: ZoomableUrlImage,
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
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.Row
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.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
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
fun HiddenNote(
reports: Set<Note>,
@@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
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.padding
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.res.painterResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
@@ -35,7 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
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.Box
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.Spacer
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.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
@@ -364,12 +365,13 @@ private fun RenderRegularTextNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val tags = remember { note.event?.tags() }
val tags = remember { note.event?.tags() ?: emptyList() }
val eventContent = remember { accountViewModel.decrypt(note) }
val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
@@ -495,6 +497,7 @@ data class RelayBadgesState(
val noteRelaysSimple: List<String>
)
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RelayBadges(baseNote: Note) {
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.layout.Box
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.padding
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.res.painterResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
@@ -35,7 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
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.layout.Box
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.Spacer
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.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
@@ -249,6 +250,7 @@ private fun RenderBoostGallery(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AuthorGalleryZaps(
authorNotes: ImmutableMap<Note, Note>,
@@ -366,7 +368,7 @@ private fun AuthorPictureAndComment(
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = null,
tags = remember { emptyList() },
modifier = remember { Modifier.fillMaxWidth() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
@@ -376,6 +378,7 @@ private fun AuthorPictureAndComment(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AuthorGallery(
authorNotes: ImmutableList<Note>,
@@ -12,6 +12,8 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Row
import androidx.compose.foundation.layout.Spacer
@@ -80,7 +82,6 @@ import androidx.core.graphics.get
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.request.SuccessResult
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
@@ -139,7 +140,6 @@ import java.math.BigDecimal
import java.net.URL
import java.util.Locale
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -156,12 +156,10 @@ fun NoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("NormalNote START NoteCompose")
val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event }
if (noteEvent == null) {
println("NormalNote Rendering Blank")
var popupExpanded by remember { mutableStateOf(false) }
BlankNote(
@@ -190,7 +188,6 @@ fun NoteCompose(
nav
)
}
println("NormalNote END NoteCompose")
}
@Composable
@@ -371,8 +368,6 @@ fun NormalNote(
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote)
} else {
println("NormalNote LoadedNoteCompose")
var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope()
@@ -395,8 +390,6 @@ fun NormalNote(
}
}
println("NormalNote LoadedNoteCompose Launched Effect")
val primaryColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background
@@ -412,8 +405,6 @@ fun NormalNote(
}
}
println("NormalNote LoadedNoteCompose Background Color")
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
@@ -429,8 +420,6 @@ fun NormalNote(
.background(backgroundColor)
}
println("NormalNote LoadedNoteCompose Modifier")
Column(modifier = columnModifier) {
Row(
modifier = remember {
@@ -442,14 +431,9 @@ fun NormalNote(
)
}
) {
println("NormalNote Before FirstUserInfo")
val (value, elapsed) = measureTimedValue {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
}
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, accountViewModel, nav)
}
println("AAA $elapsed DrawAuthorImages")
println("NormalNote FirstUserInfo")
Column(
modifier = remember {
@@ -471,7 +455,6 @@ fun NormalNote(
nav
)
}
println("NormalNote SecondUserInfo")
Spacer(modifier = Modifier.height(2.dp))
@@ -601,7 +584,7 @@ private fun RenderTextEvent(
accountViewModel = accountViewModel
) {
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(
content = eventContent,
@@ -737,7 +720,7 @@ private fun RenderPrivateMessage(
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
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 (makeItShort && isAuthorTheLoggedUser) {
@@ -812,6 +795,7 @@ fun RenderPeopleList(
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayPeopleList(
baseNote: Note,
@@ -900,6 +884,7 @@ fun DisplayPeopleList(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RenderBadgeAward(
note: Note,
@@ -1039,6 +1024,7 @@ private fun RenderPinListEvent(
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun PinListHeader(
baseNote: Note,
@@ -1087,7 +1073,7 @@ fun PinListHeader(
TranslatableRichTextViewer(
content = pin,
canPreview = true,
tags = emptyList(),
tags = remember { emptyList() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
@@ -1197,7 +1183,7 @@ private fun RenderReport(
content = content,
canPreview = true,
modifier = remember { Modifier },
tags = emptyList(),
tags = remember { emptyList() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
@@ -1311,7 +1297,6 @@ private fun FirstUserInfoRow(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val eventNote = remember { baseNote.event } ?: return
val time = remember { baseNote.createdAt() } ?: return
val padding = remember {
@@ -1339,24 +1324,34 @@ private fun FirstUserInfoRow(
TimeAgo(time)
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)
)
MoreOptionsButton(baseNote, accountViewModel)
}
}
NoteDropDownMenu(
baseNote,
moreActionsExpanded,
{ moreActionsExpanded = false },
accountViewModel
)
}
@Composable
private fun MoreOptionsButton(
baseNote: Note,
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) {
// Draws the boosted picture outside the boosted card.
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")
val (value2, elapsed2) = measureTimedValue {
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
if (baseNote.event is ChannelMessageEvent && baseChannelHex != null) {
LoadChannel(baseChannelHex) { channel ->
ChannelNotePicture(channel)
}
}
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) {
@@ -1492,9 +1477,7 @@ private fun RepostNoteAuthorPicture(
val baseRepost = remember { baseNote.replyTo?.lastOrNull() }
val modifier = remember {
Modifier
.width(30.dp)
.height(30.dp)
Modifier.size(30.dp)
}
baseRepost?.let {
@@ -1503,7 +1486,7 @@ private fun RepostNoteAuthorPicture(
baseNote = it,
nav = nav,
accountViewModel = accountViewModel,
size = 35.dp,
size = 30.dp,
pictureModifier = Modifier.border(
2.dp,
MaterialTheme.colors.background,
@@ -1514,6 +1497,7 @@ private fun RepostNoteAuthorPicture(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayHighlight(
highlight: String,
@@ -1598,15 +1582,16 @@ fun DisplayFollowingHashtagsInPost(
nav: (String) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val followingTags = remember(accountState) {
accountState?.account?.followingTagSet() ?: emptySet()
}
var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteEvent.id(), followingTags) {
withContext(Dispatchers.IO) {
firstTag = noteEvent.firstIsTaggedHashes(followingTags)
LaunchedEffect(key1 = accountState) {
launch(Dispatchers.Default) {
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
fun DisplayUncitedHashtags(
hashtags: List<String>,
@@ -2159,6 +2145,7 @@ private fun RelayBadges(baseNote: Note) {
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
@@ -2229,30 +2216,19 @@ fun NoteAuthorPicture(
}
}
val boxModifier = remember {
Modifier
.width(size)
.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)
if (author == null) {
val nullModifier = remember {
modifier.size(size).clip(shape = CircleShape)
}
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
fun UserPicture(
userHex: String,
@@ -2401,6 +2378,7 @@ private fun FollowingIcon(iconSize: Dp) {
}
}
@Immutable
data class DropDownParams(
val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean,
@@ -2417,32 +2395,29 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
val actContext = LocalContext.current
var reportDialogShowing by remember { mutableStateOf(false) }
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
var state by remember {
mutableStateOf<DropDownParams>(
DropDownParams(false, false, false, false, false, null)
)
}
LaunchedEffect(key1 = note, key2 = bookmarkState, key3 = accountState) {
withContext(Dispatchers.IO) {
state = 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
DropDownParams(
isFollowingAuthor = false,
isPrivateBookmarkNote = false,
isPublicBookmarkNote = false,
isLoggedUser = false,
isSensitive = false,
showSensitiveContent = null
)
}
)
}
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = onDismiss
) {
WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = {
accountViewModel.follow(
@@ -2512,7 +2487,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
Text("Block / Report")
}
}
// if (state.isSensitive) {
Divider()
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
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))
}
}
// }
}
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,
nav: (String) -> Unit
) {
val tags = remember { baseNote.event?.tags() }
val tags = remember { baseNote.event?.tags() ?: emptyList() }
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -164,7 +164,7 @@ private fun RenderOptionAfterVote(
totalRatio: Float,
color: Color,
canPreview: Boolean,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -231,7 +231,7 @@ private fun RenderOptionAfterVote(
private fun RenderOptionBeforeVote(
description: String,
canPreview: Boolean,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -1,5 +1,7 @@
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.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
@@ -14,7 +16,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
@@ -45,6 +46,7 @@ fun ReplyInformation(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ReplyInformation(
replyTo: List<Note>?,
@@ -166,6 +168,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ReplyInformationChannel(
replyTo: List<Note>?,
@@ -16,7 +16,9 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
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)
}
@@ -5,6 +5,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
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.padding
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.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
@@ -37,7 +38,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
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.layout.Box
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.padding
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.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
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.withContext
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
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.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -166,17 +165,14 @@ private fun FeedLoaded(
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val (value, elapsed) = measureTimedValue {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
}
println("AAA NoteCompose $elapsed ${item.event?.content()}")
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@@ -381,6 +381,10 @@ fun NoteMaster(
if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
val tags = remember(note) {
note.event?.tags() ?: emptyList()
}
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
@@ -389,7 +393,7 @@ fun NoteMaster(
eventContent,
canPreview,
Modifier.fillMaxWidth(),
note.event?.tags(),
tags,
MaterialTheme.colors.background,
accountViewModel,
nav
@@ -699,7 +699,7 @@ private fun DrawAdditionalInfo(baseUser: User, accountViewModel: AccountViewMode
TranslatableRichTextViewer(
content = it,
canPreview = false,
tags = null,
tags = remember { emptyList() },
backgroundColor = MaterialTheme.colors.background,
accountViewModel = accountViewModel,
nav = nav
@@ -10,6 +10,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Row
import androidx.compose.foundation.layout.Spacer
@@ -54,7 +56,6 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -313,6 +314,7 @@ private fun RenderVideoOrPictureNote(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RelayBadges(baseNote: Note) {
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)
private val translators =
object : LruCache<TranslatorOptions, Translator>(10) {
object : LruCache<TranslatorOptions, Translator>(20) {
override fun create(options: TranslatorOptions): Translator {
return Translation.getClient(options)
}
@@ -43,7 +43,7 @@ fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
tags: List<List<String>>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -55,7 +55,7 @@ fun TranslatableRichTextViewer(
var showOriginal 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 ||
translatedTextState.sourceLang != result.sourceLang ||
translatedTextState.targetLang != result.targetLang
@@ -227,7 +227,7 @@ fun TranslatableRichTextViewer(
}
@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 account = remember(accountState) { accountState?.account } ?: return