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