- Removing outdated version of the FlowRow
- Improving performance of custom emojis - Improving performance of DropDown menus
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user