Performance improvements additional MainThread checks.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -21,6 +22,8 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class HashtagIcon(
|
||||
val icon: Int,
|
||||
val description: String,
|
||||
|
||||
@@ -51,7 +51,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun getUserIfExists(key: String): User? {
|
||||
return users[key]
|
||||
return users.get(key)
|
||||
}
|
||||
|
||||
fun getNoteIfExists(key: String): Note? {
|
||||
@@ -230,7 +230,7 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
@@ -302,7 +302,7 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
@@ -77,11 +77,20 @@ open class Note(val idHex: String) {
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
fun channelHex(): HexKey? {
|
||||
return (event as? ChannelMessageEvent)?.channel()
|
||||
?: (event as? ChannelMetadataEvent)?.channel()
|
||||
?: (event as? ChannelCreateEvent)?.id
|
||||
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
|
||||
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
|
||||
return if (event is ChannelMessageEvent ||
|
||||
event is ChannelMetadataEvent ||
|
||||
event is ChannelCreateEvent ||
|
||||
event is LiveActivitiesChatMessageEvent ||
|
||||
event is LiveActivitiesEvent
|
||||
) {
|
||||
(event as? ChannelMessageEvent)?.channel()
|
||||
?: (event as? ChannelMetadataEvent)?.channel()
|
||||
?: (event as? ChannelCreateEvent)?.id
|
||||
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
|
||||
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
open fun address(): ATag? = null
|
||||
|
||||
@@ -49,6 +49,8 @@ object BlurHashDecoder {
|
||||
* By default it is true.
|
||||
*/
|
||||
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ data class CashuToken(
|
||||
|
||||
class CashuProcessor {
|
||||
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
val base64token = cashuToken.replace("cashuA", "")
|
||||
val cashu = JsonParser.parseString(String(Base64.getDecoder().decode(base64token)))
|
||||
@@ -44,6 +46,8 @@ class CashuProcessor {
|
||||
}
|
||||
|
||||
fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
checkNotInMainThread()
|
||||
|
||||
runCatching {
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lnaddress = lud16,
|
||||
|
||||
@@ -31,7 +31,7 @@ class Nip05Verifier() {
|
||||
}
|
||||
|
||||
private suspend fun fetchNip05JsonSuspend(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
// checkNotInMainThread()
|
||||
checkNotInMainThread()
|
||||
|
||||
val url = assembleUrl(nip05)
|
||||
|
||||
@@ -49,6 +49,8 @@ class Nip05Verifier() {
|
||||
|
||||
HttpClient.getHttpClient().newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
checkNotInMainThread()
|
||||
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
@@ -71,13 +73,15 @@ class Nip05Verifier() {
|
||||
|
||||
fun verifyNip05(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
// check fails on tests
|
||||
// checkNotInMainThread()
|
||||
checkNotInMainThread()
|
||||
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
fetchNip05Json(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
checkNotInMainThread()
|
||||
|
||||
// NIP05 usernames are case insensitive, but JSON properties are not
|
||||
// converts the json to lowercase and then tries to access the username via a
|
||||
// lowercase version of the username.
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -29,6 +30,7 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
@@ -246,7 +248,11 @@ private fun DisplayOwnerInformation(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadUser(baseUserHex = userHex) {
|
||||
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
|
||||
Crossfade(it) {
|
||||
if (it != null) {
|
||||
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +311,7 @@ fun loadRelayInfo(
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
checkNotInMainThread()
|
||||
response.use {
|
||||
val body = it.body.string()
|
||||
try {
|
||||
|
||||
@@ -126,20 +126,22 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
startActivity(context, intent, null)
|
||||
} else {
|
||||
if (lud16 != null) {
|
||||
CashuProcessor().melt(
|
||||
token,
|
||||
lud16,
|
||||
onSuccess = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
CashuProcessor().melt(
|
||||
token,
|
||||
lud16,
|
||||
onSuccess = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(context, "No Lightning Address set", Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -25,13 +26,16 @@ fun MayBeWithdrawal(lnurlWord: String) {
|
||||
}
|
||||
}
|
||||
|
||||
lnWithdrawal?.let {
|
||||
ClickableWithdrawal(withdrawalString = it)
|
||||
Crossfade(targetState = lnWithdrawal) {
|
||||
if (it != null) {
|
||||
ClickableWithdrawal(withdrawalString = it)
|
||||
} else {
|
||||
Text(
|
||||
text = "$lnurlWord ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
}
|
||||
?: Text(
|
||||
text = "$lnurlWord ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -27,7 +28,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.NumberFormat
|
||||
|
||||
@Composable
|
||||
@@ -35,7 +36,7 @@ fun MayBeInvoicePreview(lnbcWord: String) {
|
||||
var lnInvoice by remember { mutableStateOf<Pair<String, String?>?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = lnbcWord) {
|
||||
withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
|
||||
if (myInvoice != null) {
|
||||
val myInvoiceAmount = try {
|
||||
@@ -50,13 +51,16 @@ fun MayBeInvoicePreview(lnbcWord: String) {
|
||||
}
|
||||
}
|
||||
|
||||
lnInvoice?.let {
|
||||
InvoicePreview(it.first, it.second)
|
||||
Crossfade(targetState = lnInvoice) {
|
||||
if (it != null) {
|
||||
InvoicePreview(it.first, it.second)
|
||||
} else {
|
||||
Text(
|
||||
text = "$lnbcWord ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
}
|
||||
?: Text(
|
||||
text = "$lnbcWord ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
||||
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
|
||||
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration
|
||||
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import id.zelory.compressor.Compressor
|
||||
import id.zelory.compressor.constraint.default
|
||||
import java.io.File
|
||||
@@ -24,6 +25,8 @@ class MediaCompressor {
|
||||
onReady: (Uri, String?, Long?) -> Unit,
|
||||
onError: (String) -> Unit
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (contentType?.startsWith("video", true) == true) {
|
||||
VideoCompressor.start(
|
||||
context = applicationContext, // => This is required
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.util.Log
|
||||
import android.util.LruCache
|
||||
import android.util.Patterns
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
@@ -17,26 +16,19 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.HeadingStyle
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.model.HashtagIcon
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -45,10 +37,11 @@ import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.uriToRoute
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
@@ -89,38 +82,6 @@ fun isValidURL(url: String?): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
|
||||
when (level) {
|
||||
0 -> textStyle.copy(
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
1 -> textStyle.copy(
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
2 -> textStyle.copy(
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
3 -> textStyle.copy(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
4 -> textStyle.copy(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
5 -> textStyle.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
else -> textStyle
|
||||
}
|
||||
}
|
||||
|
||||
internal val DefaultParagraphSpacing: TextUnit = 12.sp
|
||||
val richTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
fun isMarkdown(content: String): Boolean {
|
||||
return content.startsWith("> ") ||
|
||||
content.startsWith("# ") ||
|
||||
@@ -399,6 +360,9 @@ private fun NormalWord(word: String) {
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UrlWithExtraChars(val url: String, val extraChars: String)
|
||||
|
||||
@Composable
|
||||
private fun NoProtocolUrlRenderer(word: String) {
|
||||
val wordSpace = remember(word) {
|
||||
@@ -406,7 +370,7 @@ private fun NoProtocolUrlRenderer(word: String) {
|
||||
}
|
||||
|
||||
var linkedUrl by remember(word) {
|
||||
mutableStateOf<Pair<String, String>?>(null)
|
||||
mutableStateOf<UrlWithExtraChars?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = word) {
|
||||
@@ -416,19 +380,30 @@ private fun NoProtocolUrlRenderer(word: String) {
|
||||
val url = matcher.group(1) // url
|
||||
val additionalChars = matcher.group(4) ?: "" // additional chars
|
||||
|
||||
linkedUrl = Pair(url, additionalChars)
|
||||
launch(Dispatchers.Main) {
|
||||
linkedUrl = UrlWithExtraChars(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)
|
||||
)
|
||||
Crossfade(targetState = linkedUrl) {
|
||||
if (it != null) {
|
||||
RenderUrl(it)
|
||||
} else {
|
||||
Text(
|
||||
text = wordSpace,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderUrl(it: UrlWithExtraChars) {
|
||||
Row() {
|
||||
ClickableUrl(it.url, "https://${it.url}")
|
||||
Text("${it.extraChars} ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,14 +418,16 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
|
||||
@Composable
|
||||
private fun RenderContentAsMarkdown(content: String, backgroundColor: MutableState<Color>, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
|
||||
val uri = LocalUriHandler.current
|
||||
val onClick = { link: String ->
|
||||
val route = uriToRoute(link)
|
||||
if (route != null) {
|
||||
nav(route)
|
||||
} else {
|
||||
runCatching { uri.openUri(link) }
|
||||
val onClick = remember {
|
||||
{ link: String ->
|
||||
val route = uriToRoute(link)
|
||||
if (route != null) {
|
||||
nav(route)
|
||||
} else {
|
||||
runCatching { uri.openUri(link) }
|
||||
}
|
||||
Unit
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
ProvideTextStyle(MarkdownTextStyle) {
|
||||
@@ -522,7 +499,7 @@ private fun ObserveNIP19Event(
|
||||
launch(Dispatchers.IO) {
|
||||
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
||||
baseNote = note
|
||||
launch(Dispatchers.Main) { baseNote = note }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,7 +536,7 @@ private fun ObserveNIP19User(
|
||||
launch(Dispatchers.IO) {
|
||||
if (it.type == Nip19.Type.USER) {
|
||||
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
|
||||
baseUser = user
|
||||
launch(Dispatchers.Main) { baseUser = user }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,21 +738,25 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
|
||||
}
|
||||
}
|
||||
|
||||
if (canPreview) {
|
||||
loadedLink?.let { loadedLink ->
|
||||
loadedLink.baseNote?.let {
|
||||
DisplayFullNote(it, accountViewModel, backgroundColor, nav, loadedLink)
|
||||
} ?: run {
|
||||
ClickableRoute(loadedLink.nip19, nav)
|
||||
Crossfade(targetState = loadedLink) {
|
||||
if (canPreview && it?.baseNote != null) {
|
||||
Row() {
|
||||
DisplayFullNote(it.baseNote, accountViewModel, backgroundColor, nav, it)
|
||||
}
|
||||
} ?: run {
|
||||
Text(text = remember { "$word " })
|
||||
}
|
||||
} else {
|
||||
loadedLink?.let {
|
||||
ClickableRoute(it.nip19, nav)
|
||||
} ?: run {
|
||||
Text(text = remember { "$word " })
|
||||
} else if (it?.nip19 != null) {
|
||||
Row() {
|
||||
ClickableRoute(it.nip19, nav)
|
||||
}
|
||||
} else {
|
||||
val text = remember {
|
||||
if (word.length > 16) {
|
||||
word.replaceRange(8, word.length - 8, ":")
|
||||
} else {
|
||||
"$word "
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = text, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,9 +793,11 @@ private fun DisplayFullNote(
|
||||
}
|
||||
}
|
||||
|
||||
data class HashWordWithExtra(val hashtag: String, val extras: String?)
|
||||
|
||||
@Composable
|
||||
fun HashTag(word: String, nav: (String) -> Unit) {
|
||||
var tagSuffixPair by remember { mutableStateOf<Pair<String, String?>?>(null) }
|
||||
var tagSuffixPair by remember { mutableStateOf<HashWordWithExtra?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = word) {
|
||||
launch(Dispatchers.IO) {
|
||||
@@ -830,67 +813,80 @@ fun HashTag(word: String, nav: (String) -> Unit) {
|
||||
|
||||
if (myTag != null) {
|
||||
launch(Dispatchers.Main) {
|
||||
tagSuffixPair = Pair(myTag, mySuffix)
|
||||
tagSuffixPair = HashWordWithExtra(myTag, mySuffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagSuffixPair?.let { tagPair ->
|
||||
val hashtagIcon = remember(tagPair.first) { checkForHashtagWithIcon(tagPair.first) }
|
||||
ClickableText(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
) {
|
||||
append("#${tagPair.first}")
|
||||
}
|
||||
},
|
||||
onClick = { nav("Hashtag/${tagPair.first}") }
|
||||
)
|
||||
|
||||
if (hashtagIcon != null) {
|
||||
val myId = "inlineContent"
|
||||
val emptytext = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
) {
|
||||
append("")
|
||||
appendInlineContent(myId, "[icon]")
|
||||
}
|
||||
Crossfade(targetState = tagSuffixPair) {
|
||||
if (it != null) {
|
||||
Row() {
|
||||
RenderHashtag(it, nav)
|
||||
}
|
||||
val inlineContent = mapOf(
|
||||
Pair(
|
||||
myId,
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
width = 17.sp,
|
||||
height = 17.sp,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(hashtagIcon.icon),
|
||||
contentDescription = hashtagIcon.description,
|
||||
tint = hashtagIcon.color,
|
||||
modifier = hashtagIcon.modifier
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Empty Text for Size of Icon
|
||||
Text(
|
||||
text = emptytext,
|
||||
inlineContent = inlineContent
|
||||
)
|
||||
} else {
|
||||
Text(text = remember { "$word " })
|
||||
}
|
||||
tagPair.second?.ifBlank { "" }?.let {
|
||||
Text(text = "$it ")
|
||||
}
|
||||
} ?: Text(text = "$word ")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderHashtag(
|
||||
tagPair: HashWordWithExtra,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val hashtagIcon = remember(tagPair.hashtag) { checkForHashtagWithIcon(tagPair.hashtag) }
|
||||
ClickableText(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
) {
|
||||
append("#${tagPair.hashtag}")
|
||||
}
|
||||
},
|
||||
onClick = { nav("Hashtag/${tagPair.hashtag}") }
|
||||
)
|
||||
|
||||
if (hashtagIcon != null) {
|
||||
val myId = "inlineContent"
|
||||
val emptytext = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
) {
|
||||
append("")
|
||||
appendInlineContent(myId, "[icon]")
|
||||
}
|
||||
}
|
||||
// Empty Text for Size of Icon
|
||||
Text(
|
||||
text = emptytext,
|
||||
inlineContent = mapOf(
|
||||
myId to InlineIcon(hashtagIcon)
|
||||
)
|
||||
)
|
||||
}
|
||||
tagPair.extras?.ifBlank { "" }?.let {
|
||||
Text(text = "$it ")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
width = Font17SP,
|
||||
height = Font17SP,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(hashtagIcon.icon),
|
||||
contentDescription = hashtagIcon.description,
|
||||
tint = hashtagIcon.color,
|
||||
modifier = hashtagIcon.modifier
|
||||
)
|
||||
}
|
||||
|
||||
data class LoadedTag(val user: User?, val note: Note?, val addedChars: String)
|
||||
|
||||
@Composable
|
||||
@@ -932,19 +928,28 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedTag == null) {
|
||||
Text(
|
||||
text = remember {
|
||||
"$word "
|
||||
Crossfade(targetState = loadedTag) {
|
||||
if (it == null) {
|
||||
Text(
|
||||
text = remember {
|
||||
"$word "
|
||||
}
|
||||
)
|
||||
} else if (it.user != null) {
|
||||
Row() {
|
||||
DisplayUserFromTag(it.user, loadedTag?.addedChars ?: "", nav)
|
||||
}
|
||||
} else if (it.note != null) {
|
||||
Row() {
|
||||
DisplayNoteFromTag(
|
||||
it.note,
|
||||
loadedTag?.addedChars ?: "",
|
||||
canPreview,
|
||||
accountViewModel,
|
||||
backgroundColor,
|
||||
nav
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loadedTag?.user?.let {
|
||||
DisplayUserFromTag(it, loadedTag?.addedChars ?: "", nav)
|
||||
}
|
||||
|
||||
loadedTag?.note?.let {
|
||||
DisplayNoteFromTag(it, loadedTag?.addedChars ?: "", canPreview, accountViewModel, backgroundColor, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -991,16 +996,18 @@ private fun DisplayUserFromTag(
|
||||
}.distinctUntilChanged().observeAsState(baseUser.info)
|
||||
|
||||
Crossfade(targetState = meta) {
|
||||
val displayName = remember(it) {
|
||||
it?.bestDisplayName() ?: hex
|
||||
Row() {
|
||||
val displayName = remember(it) {
|
||||
it?.bestDisplayName() ?: hex
|
||||
}
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = displayName,
|
||||
suffix = suffix,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
nav = nav,
|
||||
tags = it?.tags
|
||||
)
|
||||
}
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = displayName,
|
||||
suffix = suffix,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
nav = nav,
|
||||
tags = it?.tags ?: ImmutableListOfLists()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ import com.vitorpamplona.amethyst.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
public var DefaultMutedSetting = mutableStateOf(true)
|
||||
|
||||
@@ -97,12 +99,26 @@ fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thu
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun VideoView(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
VideoView1(videoUri, description, thumb, onDialog)
|
||||
}
|
||||
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoView1(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
var exoPlayerData by remember { mutableStateOf<VideoPlayer?>(null) }
|
||||
val defaultToStart by remember { mutableStateOf(DefaultMutedSetting.value) }
|
||||
@@ -127,6 +143,7 @@ fun VideoView(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun VideoView(
|
||||
videoUri: String,
|
||||
@@ -135,6 +152,21 @@ fun VideoView(
|
||||
defaultToStart: Boolean = false,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog)
|
||||
}
|
||||
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoView1(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
exoPlayerData: VideoPlayer,
|
||||
defaultToStart: Boolean = false,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
@@ -25,6 +24,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
@@ -33,12 +33,10 @@ fun ZapRaiserRequest(
|
||||
newPostViewModel: NewPostViewModel
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -49,7 +47,7 @@ fun ZapRaiserRequest(
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.lightning),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -53,9 +54,14 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatHeadlineBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -110,8 +116,16 @@ private fun ChatroomDirectMessage(
|
||||
}
|
||||
|
||||
userRoomHex?.let {
|
||||
LoadUser(it) { user ->
|
||||
UserRoomCompose(baseNote, user, accountViewModel, nav)
|
||||
LoadUser(it) { baseUser ->
|
||||
Crossfade(baseUser) { user ->
|
||||
if (user != null) {
|
||||
UserRoomCompose(baseNote, user, accountViewModel, nav)
|
||||
} else {
|
||||
Box(Modifier.height(Size75dp).fillMaxWidth()) {
|
||||
// Makes sure just a max amount of objects are loaded.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +260,7 @@ private fun UserRoomCompose(
|
||||
accountViewModel.decrypt(note)
|
||||
}
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages.value != newHasNewMessages) {
|
||||
hasNewMessages.value = newHasNewMessages
|
||||
@@ -288,7 +303,7 @@ private fun WatchNotificationChanges(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
|
||||
fun LoadUser(baseUserHex: String, content: @Composable (User?) -> Unit) {
|
||||
var user by remember(baseUserHex) {
|
||||
mutableStateOf<User?>(LocalCache.getUserIfExists(baseUserHex))
|
||||
}
|
||||
@@ -301,9 +316,7 @@ fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
user?.let {
|
||||
content(it)
|
||||
}
|
||||
content(user)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -348,9 +361,7 @@ fun ChannelName(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(modifier = remember { Modifier.clickable(onClick = onClick) }) {
|
||||
Row(
|
||||
modifier = remember { Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp) }
|
||||
) {
|
||||
Row(modifier = ChatHeadlineBorders) {
|
||||
Column(Size55Modifier) {
|
||||
channelPicture()
|
||||
}
|
||||
@@ -362,11 +373,13 @@ fun ChannelName(
|
||||
verticalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = remember { Modifier.padding(bottom = 4.dp) }
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FirstRow(channelTitle, channelLastTime, remember { Modifier.weight(1f) })
|
||||
}
|
||||
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -377,8 +390,8 @@ fun ChannelName(
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = remember { Modifier.padding(top = 10.dp) },
|
||||
thickness = 0.25.dp
|
||||
modifier = StdTopPadding,
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -39,35 +38,49 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
|
||||
@Composable
|
||||
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val baseNote = remember { multiSetCard.note }
|
||||
@@ -121,32 +134,39 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
}
|
||||
|
||||
Column(modifier = columnModifier) {
|
||||
Galeries(multiSetCard, backgroundColor, nav, accountViewModel)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
Galeries(multiSetCard, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
Log.d("Rendering Metrics", "All Galeries: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed - ")
|
||||
|
||||
Row(remember { Modifier.fillMaxWidth() }) {
|
||||
Spacer(modifier = WidthAuthorPictureModifierWithPadding)
|
||||
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
modifier = remember { Modifier.padding(top = 5.dp) },
|
||||
isBoostedNote = true,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
modifier = remember { Modifier.padding(top = 5.dp) },
|
||||
isBoostedNote = true,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Complete: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
private fun Galeries(
|
||||
multiSetCard: MultiSetCard,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
@@ -157,17 +177,26 @@ private fun Galeries(
|
||||
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
|
||||
|
||||
if (hasZapEvents) {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Zaps: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
|
||||
if (hasBoostEvents) {
|
||||
RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderBoostGallery(boostEvents, nav, accountViewModel)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Repost: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
|
||||
if (hasLikeEvents) {
|
||||
likeEvents.forEach {
|
||||
RenderLikeGallery(it.key, it.value, backgroundColor, nav, accountViewModel)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
likeEvents.forEach {
|
||||
RenderLikeGallery(it.key, it.value, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
Log.d("Rendering Metrics", "Galeries Like: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,39 +204,23 @@ private fun Galeries(
|
||||
fun RenderLikeGallery(
|
||||
reactionType: String,
|
||||
likeEvents: ImmutableList<Note>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val isNotEmpty = remember(likeEvents) {
|
||||
likeEvents.isNotEmpty()
|
||||
}
|
||||
|
||||
val shortReaction by remember {
|
||||
derivedStateOf {
|
||||
reactionType.firstFullChar()
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotEmpty) {
|
||||
Row(remember { Modifier.fillMaxWidth() }) {
|
||||
if (likeEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
}
|
||||
modifier = NotificationIconModifier
|
||||
) {
|
||||
val modifier = remember {
|
||||
Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
Modifier.align(Alignment.TopEnd)
|
||||
}
|
||||
|
||||
when (shortReaction) {
|
||||
when (val shortReaction = reactionType.firstFullChar()) {
|
||||
"+" -> Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember { modifier.size(18.dp) },
|
||||
modifier = modifier.size(Size18dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||
@@ -215,7 +228,7 @@ fun RenderLikeGallery(
|
||||
}
|
||||
}
|
||||
|
||||
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
AuthorGallery(likeEvents, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,21 +250,17 @@ fun RenderZapGallery(
|
||||
@Composable
|
||||
private fun ZapIcon() {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
}
|
||||
modifier = WidthAuthorPictureModifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
tint = BitcoinOrange,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size25dp)
|
||||
.align(Alignment.TopEnd)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -259,21 +268,14 @@ private fun ZapIcon() {
|
||||
@Composable
|
||||
fun RenderBoostGallery(
|
||||
boostEvents: ImmutableList<Note>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(
|
||||
modifier = remember {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 4.dp)
|
||||
}
|
||||
modifier = NotificationIconModifierSmaller
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
@@ -287,7 +289,7 @@ fun RenderBoostGallery(
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(boostEvents, backgroundColor, nav, accountViewModel)
|
||||
AuthorGallery(boostEvents, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,15 +301,27 @@ fun AuthorGalleryZaps(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach {
|
||||
AuthorPictureAndComment(it.request, it.response, backgroundColor, nav, accountViewModel)
|
||||
ParseAuthorCommentAndAmount(it, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ParseAuthorCommentAndAmount(
|
||||
zap: CombinedZap,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
ParseAuthorCommentAndAmount(zap.request, zap.response, accountViewModel) { state ->
|
||||
RenderState(state, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class ZapAmountCommentNotification(
|
||||
val user: User?,
|
||||
@@ -316,12 +330,11 @@ data class ZapAmountCommentNotification(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun AuthorPictureAndComment(
|
||||
private fun ParseAuthorCommentAndAmount(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
accountViewModel: AccountViewModel,
|
||||
onReady: @Composable (MutableState<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
val content = remember {
|
||||
mutableStateOf(
|
||||
@@ -334,7 +347,7 @@ private fun AuthorPictureAndComment(
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
|
||||
launch(Dispatchers.Default) {
|
||||
launch(Dispatchers.IO) {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
@@ -353,111 +366,116 @@ private fun AuthorPictureAndComment(
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = content) {
|
||||
Row(
|
||||
modifier = Modifier.clickable {
|
||||
nav("User/${it.value.user?.pubkeyHex}")
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AuthorPictureAndComment(
|
||||
authorComment = it,
|
||||
backgroundColor = backgroundColor,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.clickable {
|
||||
content.value.user?.let {
|
||||
nav(routeFor(it))
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
DisplayAuthorCommentAndAmount(
|
||||
authorComment = content,
|
||||
backgroundColor = backgroundColor,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val amountBoxModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.size(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val textBoxModifier = Modifier
|
||||
.padding(start = 5.dp)
|
||||
.fillMaxWidth()
|
||||
|
||||
val simpleModifier = Modifier
|
||||
|
||||
val sizedModifier = Modifier.size(Size35dp)
|
||||
|
||||
val bottomPadding1dp = Modifier.padding(bottom = 1.dp)
|
||||
|
||||
val commentTextSize = 12.sp
|
||||
|
||||
@Composable
|
||||
private fun AuthorPictureAndComment(
|
||||
private fun DisplayAuthorCommentAndAmount(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
|
||||
DisplayPicture(authorComment, accountViewModel)
|
||||
DisplayAmount(authorComment)
|
||||
CrossfadeToDisplayPicture(authorComment, accountViewModel)
|
||||
CrossfadeToDisplayAmount(authorComment)
|
||||
}
|
||||
|
||||
DisplayComment(authorComment, backgroundColor, nav, accountViewModel)
|
||||
CrossfadeToDisplayComment(authorComment, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
|
||||
authorComment.value.user?.let {
|
||||
FastNoteAuthorPicture(
|
||||
author = it,
|
||||
size = Size35dp,
|
||||
accountViewModel = accountViewModel,
|
||||
pictureModifier = simpleModifier
|
||||
)
|
||||
fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
|
||||
Crossfade(authorComment.value) {
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(it.user, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
|
||||
authorComment.value.amount?.let {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colors.overPictureBackground
|
||||
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
|
||||
Crossfade(authorComment.value, modifier = amountBoxModifier) {
|
||||
it.amount?.let {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
},
|
||||
modifier = amountBoxModifier,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
val backgroundColor = MaterialTheme.colors.overPictureBackground
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size35dp)
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayComment(
|
||||
fun CrossfadeToDisplayComment(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
authorComment.value.comment?.let {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
Crossfade(authorComment.value) {
|
||||
it.comment?.let {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,14 +483,13 @@ fun DisplayComment(
|
||||
@Composable
|
||||
fun AuthorGallery(
|
||||
authorNotes: ImmutableList<Note>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach { note ->
|
||||
BoxedAuthor(note, backgroundColor, nav, accountViewModel)
|
||||
BoxedAuthor(note, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -481,102 +498,78 @@ fun AuthorGallery(
|
||||
@Composable
|
||||
private fun BoxedAuthor(
|
||||
note: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Box(sizedModifier) {
|
||||
NotePictureAndComment(note, backgroundColor, nav, accountViewModel)
|
||||
Box(modifier = Size35Modifier.clickable(onClick = { nav(authorRouteFor(note)) })) {
|
||||
WatchNoteAuthor(note) { targetAuthor ->
|
||||
Crossfade(targetState = targetAuthor, modifier = Size35Modifier) { author ->
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(author, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotePictureAndComment(
|
||||
baseNote: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
fun WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
|
||||
author: User?,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val author = remember(baseNote) {
|
||||
mutableStateOf(
|
||||
ZapAmountCommentNotification(
|
||||
user = baseNote.author,
|
||||
comment = null,
|
||||
amount = null
|
||||
if (author != null) {
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePicture(author, accountViewModel)
|
||||
} else {
|
||||
DisplayBlankAuthor(Size35dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
author: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
WatchUserMetadata(author) { baseUserPicture ->
|
||||
Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashAsyncImageProxy(
|
||||
robot = author.pubkeyHex,
|
||||
model = userPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colors.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val modifier = remember(baseNote) {
|
||||
Modifier.clickable {
|
||||
nav("User/${baseNote.author?.pubkeyHex}")
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AuthorPictureAndComment(authorComment = author, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FastNoteAuthorPicture(
|
||||
author: User,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val myBoxModifier = remember {
|
||||
Modifier.size(size)
|
||||
}
|
||||
|
||||
Box(myBoxModifier, contentAlignment = Alignment.TopEnd) {
|
||||
WatchMetadataAndRenderPictureWithFollwingMark(author, size, pictureModifier, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchMetadataAndRenderPictureWithFollwingMark(
|
||||
author: User,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var profilePicture by remember {
|
||||
mutableStateOf(author.info?.picture)
|
||||
}
|
||||
|
||||
val authorPubKey = remember {
|
||||
author.pubkeyHex
|
||||
}
|
||||
|
||||
WatchUserMetadata(author) {
|
||||
if (it.picture != profilePicture) {
|
||||
profilePicture = it.picture
|
||||
}
|
||||
}
|
||||
|
||||
PictureAndFollowingMark(
|
||||
userHex = authorPubKey,
|
||||
userPicture = profilePicture,
|
||||
size = size,
|
||||
modifier = pictureModifier,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchUserMetadata(userBase: User, onMetadataChanges: (UserMetadata) -> Unit) {
|
||||
val userState by userBase.live().metadata.observeAsState()
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.Default) {
|
||||
userState?.user?.info?.let {
|
||||
launch(Dispatchers.Main) {
|
||||
onMetadataChanges(it)
|
||||
WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
Crossfade(targetState = isFollowing) {
|
||||
if (it) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
FollowingIcon(Size10dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchNoteAuthor(
|
||||
baseNote: Note,
|
||||
onContent: @Composable (User?) -> Unit
|
||||
) {
|
||||
val author by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.observeAsState(baseNote.author)
|
||||
|
||||
onContent(author)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchUserMetadata(
|
||||
author: User,
|
||||
onNewMetadata: @Composable (String?) -> Unit
|
||||
) {
|
||||
val userProfile by author.live().metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged().observeAsState(author.profilePicture())
|
||||
|
||||
onNewMetadata(userProfile)
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ fun InnerNoteWithReactions(
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
|
||||
}
|
||||
println("AAA Rendering Auth $elapsed - ${baseNote.event?.content()?.take(10)}")
|
||||
Log.d("Rendering Metrics", "Author: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
@@ -625,7 +625,7 @@ fun InnerNoteWithReactions(
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
println("AAA Rendering Body $elapsed - ${baseNote.event?.content()?.take(10)}")
|
||||
Log.d("Rendering Metrics", "TextBody: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@ fun InnerNoteWithReactions(
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
println("AAA Rendering Reac $elapsed - ${baseNote.event?.content()?.take(10)}")
|
||||
Log.d("Rendering Metrics", "Reaction: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,6 +830,14 @@ fun routeFor(note: Note, loggedIn: User): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun routeFor(user: User): String {
|
||||
return "User/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
fun authorRouteFor(note: Note): String {
|
||||
return "User/${note.author?.pubkeyHex}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderTextEvent(
|
||||
note: Note,
|
||||
@@ -3036,7 +3044,7 @@ fun NoteAuthorPicture(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayBlankAuthor(size: Dp, modifier: Modifier) {
|
||||
fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val nullModifier = remember {
|
||||
@@ -3161,16 +3169,14 @@ fun BaseUserPicture(
|
||||
Modifier.size(size)
|
||||
}
|
||||
|
||||
Crossfade(targetState = userProfile) {
|
||||
Box(myBoxModifier, contentAlignment = TopEnd) {
|
||||
PictureAndFollowingMark(
|
||||
userHex = userPubkey,
|
||||
userPicture = it,
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
Box(myBoxModifier, contentAlignment = TopEnd) {
|
||||
PictureAndFollowingMark(
|
||||
userHex = userPubkey,
|
||||
userPicture = userProfile,
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3207,50 +3213,56 @@ fun PictureAndFollowingMark(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
|
||||
val showFollowingMark by accountViewModel.userFollows.map {
|
||||
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
)
|
||||
|
||||
Crossfade(targetState = showFollowingMark) {
|
||||
if (it) {
|
||||
Box(contentAlignment = TopEnd) {
|
||||
FollowingIcon(iconSize)
|
||||
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
|
||||
WatchFollows(userHex, accountViewModel) {
|
||||
Crossfade(targetState = it) {
|
||||
if (it) {
|
||||
Box(contentAlignment = TopEnd) {
|
||||
FollowingIcon(iconSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FollowingIcon(iconSize: Dp) {
|
||||
val myIconBoxModifier = remember {
|
||||
fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
||||
val showFollowingMark by accountViewModel.userFollows.map {
|
||||
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
)
|
||||
|
||||
onFollowChanges(showFollowingMark)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowingIcon(iconSize: Dp) {
|
||||
val modifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val myIconBackgroundModifier = remember {
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.size(iconSize.times(0.6f))
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
}
|
||||
val myIconBackgroundModifier = remember {
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.size(iconSize.times(0.6f))
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
}
|
||||
|
||||
Box(
|
||||
myIconBackgroundModifier
|
||||
)
|
||||
FollowingIcon(modifier, myIconBackgroundModifier)
|
||||
}
|
||||
|
||||
val myIconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
@Composable
|
||||
fun FollowingIcon(modifier: Modifier, myIconBackgroundModifier: Modifier) {
|
||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||
Box(myIconBackgroundModifier)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
stringResource(id = R.string.following),
|
||||
modifier = myIconModifier,
|
||||
modifier = modifier,
|
||||
tint = Following
|
||||
)
|
||||
}
|
||||
|
||||
@@ -419,7 +419,6 @@ private fun ReactionDetailGallery(
|
||||
if (hasBoostEvents) {
|
||||
RenderBoostGallery(
|
||||
boostEvents,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
@@ -431,7 +430,6 @@ private fun ReactionDetailGallery(
|
||||
RenderLikeGallery(
|
||||
it.key,
|
||||
reactions,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -18,6 +19,8 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Composable
|
||||
fun ChatroomListFeedView(
|
||||
@@ -68,6 +71,7 @@ private fun CrossFadeState(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
private fun FeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
@@ -108,11 +112,14 @@ private fun FeedLoaded(
|
||||
key = { index, item -> if (index == 0) index else item.idHex }
|
||||
) { _, item ->
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
ChatroomCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
ChatroomCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Chat Header Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -215,7 +216,7 @@ private fun FeedLoaded(
|
||||
}
|
||||
}
|
||||
|
||||
println("AAA Rendering $elapsed - ${item.event?.content()?.take(10)}")
|
||||
Log.d("Rendering Metrics", "Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -34,8 +34,8 @@ import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.components.richTextDefaults
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -86,8 +86,8 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
|
||||
Column(
|
||||
modifier = Modifier.padding(30.dp)
|
||||
) {
|
||||
val myMarkDownStyle = richTextDefaults.copy(
|
||||
stringStyle = richTextDefaults.stringStyle?.copy(
|
||||
val myMarkDownStyle = RichTextDefaults.copy(
|
||||
stringStyle = RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colors.primary
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
@@ -20,6 +21,8 @@ val Shapes = Shapes(
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
|
||||
val RippleRadius45dp = 45.dp // Ripple should be +10.dp over the component size
|
||||
|
||||
val SmallBorder = RoundedCornerShape(7.dp)
|
||||
val QuoteBorder = RoundedCornerShape(15.dp)
|
||||
val ButtonBorder = RoundedCornerShape(20.dp)
|
||||
@@ -40,6 +43,7 @@ val DoubleVertSpacer = Modifier.height(10.dp)
|
||||
val HalfDoubleVertSpacer = Modifier.height(7.dp)
|
||||
|
||||
val Size0dp = 0.dp
|
||||
val Size10dp = 10.dp
|
||||
val Size13dp = 13.dp
|
||||
val Size15dp = 15.dp
|
||||
val Size16dp = 16.dp
|
||||
@@ -56,15 +60,20 @@ val Size75dp = 75.dp
|
||||
|
||||
val HalfStartPadding = Modifier.padding(start = 5.dp)
|
||||
val StdStartPadding = Modifier.padding(start = 10.dp)
|
||||
val StdTopPadding = Modifier.padding(top = 10.dp)
|
||||
|
||||
val HalfPadding = Modifier.padding(5.dp)
|
||||
val StdPadding = Modifier.padding(10.dp)
|
||||
|
||||
val Size6Modifier = Modifier.size(6.dp)
|
||||
val Size10Modifier = Modifier.size(10.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
val Size18Modifier = Modifier.size(18.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size35Modifier = Modifier.size(35.dp)
|
||||
val Size55Modifier = Modifier.size(55.dp)
|
||||
|
||||
val TinyBorders = Modifier.padding(2.dp)
|
||||
@@ -93,3 +102,9 @@ val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp)
|
||||
val ChatBubbleMaxSizeModifier = Modifier.fillMaxWidth(0.85f)
|
||||
|
||||
val ModifierWidth3dp = Modifier.width(3.dp)
|
||||
|
||||
val NotificationIconModifier = Modifier.width(55.dp).padding(end = 5.dp)
|
||||
val NotificationIconModifierSmaller = Modifier.width(55.dp).padding(end = 4.dp)
|
||||
|
||||
val ZapPictureCommentModifier = Modifier.height(35.dp).widthIn(min = 35.dp)
|
||||
val ChatHeadlineBorders = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -25,9 +26,7 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultHeadingStyle
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultParagraphSpacing
|
||||
import com.vitorpamplona.amethyst.ui.components.richTextDefaults
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
@@ -121,6 +120,16 @@ val LightImageModifier = Modifier
|
||||
QuoteBorder
|
||||
)
|
||||
|
||||
val DarkProfile35dpModifier = Modifier
|
||||
.size(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
.drawBehind { drawRect(DarkColorPalette.background) }
|
||||
|
||||
val LightProfile35dpModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = CircleShape)
|
||||
.drawBehind { drawRect(LightColorPalette.background) }
|
||||
|
||||
val DarkReplyBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
@@ -161,10 +170,12 @@ val LightInnerPostBorderModifier = Modifier
|
||||
QuoteBorder
|
||||
)
|
||||
|
||||
val MarkDownStyleOnDark = richTextDefaults.copy(
|
||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
val MarkDownStyleOnDark = RichTextDefaults.copy(
|
||||
paragraphSpacing = DefaultParagraphSpacing,
|
||||
headingStyle = DefaultHeadingStyle,
|
||||
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
||||
codeBlockStyle = RichTextDefaults.codeBlockStyle?.copy(
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = Font14SP
|
||||
@@ -182,7 +193,7 @@ val MarkDownStyleOnDark = richTextDefaults.copy(
|
||||
drawRect(DarkColorPalette.onSurface.copy(alpha = 0.05f))
|
||||
}
|
||||
),
|
||||
stringStyle = richTextDefaults.stringStyle?.copy(
|
||||
stringStyle = RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle = SpanStyle(
|
||||
color = DarkColorPalette.primary
|
||||
),
|
||||
@@ -194,10 +205,10 @@ val MarkDownStyleOnDark = richTextDefaults.copy(
|
||||
)
|
||||
)
|
||||
|
||||
val MarkDownStyleOnLight = richTextDefaults.copy(
|
||||
val MarkDownStyleOnLight = RichTextDefaults.copy(
|
||||
paragraphSpacing = DefaultParagraphSpacing,
|
||||
headingStyle = DefaultHeadingStyle,
|
||||
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
||||
codeBlockStyle = RichTextDefaults.codeBlockStyle?.copy(
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = Font14SP
|
||||
@@ -215,7 +226,7 @@ val MarkDownStyleOnLight = richTextDefaults.copy(
|
||||
drawRect(LightColorPalette.onSurface.copy(alpha = 0.05f))
|
||||
}
|
||||
),
|
||||
stringStyle = richTextDefaults.stringStyle?.copy(
|
||||
stringStyle = RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle = SpanStyle(
|
||||
color = LightColorPalette.primary
|
||||
),
|
||||
@@ -283,6 +294,9 @@ val Colors.repostProfileBorder: Modifier
|
||||
val Colors.imageModifier: Modifier
|
||||
get() = if (isLight) LightImageModifier else DarkImageModifier
|
||||
|
||||
val Colors.profile35dpModifier: Modifier
|
||||
get() = if (isLight) LightProfile35dpModifier else DarkProfile35dpModifier
|
||||
|
||||
val Colors.replyModifier: Modifier
|
||||
get() = if (isLight) LightReplyBorderModifier else DarkReplyBorderModifier
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.halilibo.richtext.ui.HeadingStyle
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
@@ -29,5 +31,37 @@ val Typography = Typography(
|
||||
)
|
||||
|
||||
val Font14SP = 14.sp
|
||||
val Font17SP = 17.sp
|
||||
|
||||
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
|
||||
|
||||
val DefaultParagraphSpacing: TextUnit = 12.sp
|
||||
|
||||
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
|
||||
when (level) {
|
||||
0 -> textStyle.copy(
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
1 -> textStyle.copy(
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
2 -> textStyle.copy(
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Light
|
||||
)
|
||||
3 -> textStyle.copy(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
4 -> textStyle.copy(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
5 -> textStyle.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
else -> textStyle
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -12,6 +12,8 @@ import com.google.mlkit.nl.translate.Translator
|
||||
import com.google.mlkit.nl.translate.TranslatorOptions
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Immutable
|
||||
@@ -22,7 +24,9 @@ data class ResultOrError(
|
||||
)
|
||||
|
||||
object LanguageTranslatorService {
|
||||
private val options = LanguageIdentificationOptions.Builder().setConfidenceThreshold(0.6f).build()
|
||||
var executorService = Executors.newScheduledThreadPool(5)
|
||||
|
||||
private val options = LanguageIdentificationOptions.Builder().setExecutor(executorService).setConfidenceThreshold(0.6f).build()
|
||||
private val languageIdentification = LanguageIdentification.getClient(options)
|
||||
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
|
||||
val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE)
|
||||
@@ -48,6 +52,7 @@ object LanguageTranslatorService {
|
||||
}
|
||||
|
||||
fun translate(text: String, source: String, target: String): Task<ResultOrError> {
|
||||
checkNotInMainThread()
|
||||
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
|
||||
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
|
||||
|
||||
@@ -62,19 +67,22 @@ object LanguageTranslatorService {
|
||||
|
||||
val translator = translators[options]
|
||||
|
||||
return translator.downloadModelIfNeeded().onSuccessTask {
|
||||
val tasks = mutableListOf<Task<String>>()
|
||||
return translator.downloadModelIfNeeded().onSuccessTask(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val tasks = mutableListOf<Task<String>>()
|
||||
val dict = lnDictionary(text) + urlDictionary(text) + tagDictionary(text)
|
||||
|
||||
for (paragraph in encodeDictionary(text, dict).split("\n")) {
|
||||
tasks.add(translator.translate(paragraph))
|
||||
}
|
||||
|
||||
Tasks.whenAll(tasks).continueWith {
|
||||
Tasks.whenAll(tasks).continueWith(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val results: MutableList<String> = ArrayList()
|
||||
for (task in tasks) {
|
||||
var fixedText = task.result.replace("# [", "#[") // fixes tags that always return with a space
|
||||
val fixedText = task.result.replace("# [", "#[") // fixes tags that always return with a space
|
||||
results.add(decodeDictionary(fixedText, dict))
|
||||
}
|
||||
ResultOrError(results.joinToString("\n"), source, target)
|
||||
@@ -140,7 +148,7 @@ object LanguageTranslatorService {
|
||||
}
|
||||
|
||||
fun autoTranslate(text: String, dontTranslateFrom: Set<String>, translateTo: String): Task<ResultOrError> {
|
||||
return identifyLanguage(text).onSuccessTask {
|
||||
return identifyLanguage(text).onSuccessTask(executorService) {
|
||||
if (it == translateTo) {
|
||||
Tasks.forCanceled()
|
||||
} else if (it != "und" && !dontTranslateFrom.contains(it)) {
|
||||
|
||||
+71
-36
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Divider
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -33,7 +35,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
@@ -41,6 +42,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Immutable
|
||||
data class TranslationConfig(
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
val showOriginal: Boolean
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
@@ -51,32 +60,53 @@ fun TranslatableRichTextViewer(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var translatedTextState by remember {
|
||||
mutableStateOf(TranslationConfig(content, null, null, false))
|
||||
}
|
||||
|
||||
TranslateAndWatchLanguageChanges(content, accountViewModel) { result ->
|
||||
if (!translatedTextState.result.equals(result.result, true) ||
|
||||
translatedTextState.sourceLang != result.sourceLang ||
|
||||
translatedTextState.targetLang != result.targetLang
|
||||
) {
|
||||
translatedTextState = result
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = translatedTextState) {
|
||||
RenderText(
|
||||
it,
|
||||
content,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderText(
|
||||
translatedTextState: TranslationConfig,
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var showOriginal by remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
|
||||
|
||||
val toBeViewed by remember(translatedTextState) {
|
||||
derivedStateOf {
|
||||
if (showOriginal) content else translatedTextState.result ?: content
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
var translatedTextState by remember {
|
||||
mutableStateOf(ResultOrError(content, null, null))
|
||||
}
|
||||
|
||||
var showOriginal by remember { mutableStateOf(false) }
|
||||
|
||||
TranslateAndWatchLanguageChanges(content, accountViewModel) { result, newShowOriginal ->
|
||||
if (!translatedTextState.result.equals(result.result, true) ||
|
||||
translatedTextState.sourceLang != result.sourceLang ||
|
||||
translatedTextState.targetLang != result.targetLang
|
||||
) {
|
||||
translatedTextState = result
|
||||
}
|
||||
|
||||
if (showOriginal != newShowOriginal) {
|
||||
showOriginal = newShowOriginal
|
||||
}
|
||||
}
|
||||
|
||||
val toBeViewed by remember(translatedTextState) {
|
||||
derivedStateOf {
|
||||
if (showOriginal) content else translatedTextState.result ?: content
|
||||
}
|
||||
}
|
||||
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
canPreview,
|
||||
@@ -92,8 +122,8 @@ fun TranslatableRichTextViewer(
|
||||
translatedTextState.sourceLang != translatedTextState.targetLang
|
||||
) {
|
||||
TranslationMessage(
|
||||
translatedTextState.sourceLang!!,
|
||||
translatedTextState.targetLang!!,
|
||||
translatedTextState.sourceLang,
|
||||
translatedTextState.targetLang,
|
||||
accountViewModel
|
||||
) {
|
||||
showOriginal = it
|
||||
@@ -280,25 +310,30 @@ private fun TranslationMessage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (ResultOrError, Boolean) -> Unit) {
|
||||
fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (TranslationConfig) -> Unit) {
|
||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(accountState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
LanguageTranslatorService.autoTranslate(
|
||||
content,
|
||||
account.dontTranslateFrom,
|
||||
account.translateTo
|
||||
).addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && content != task.result.result) {
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
|
||||
launch(Dispatchers.Main) {
|
||||
onTranslated(task.result, preference == task.result.sourceLang)
|
||||
}
|
||||
val newConfig = TranslationConfig(
|
||||
result = task.result.result,
|
||||
sourceLang = task.result.sourceLang,
|
||||
targetLang = task.result.targetLang,
|
||||
showOriginal = preference == task.result.sourceLang
|
||||
)
|
||||
|
||||
// withContext(Dispatchers.Main) {
|
||||
onTranslated(newConfig)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user