Improving performance of the RichText Viewer

This commit is contained in:
Vitor Pamplona
2023-05-07 16:12:28 -04:00
parent 936f750826
commit 13de5973ca
7 changed files with 330 additions and 193 deletions
@@ -42,11 +42,13 @@ fun ExpandableRichTextViewer(
) { ) {
var showFullText by remember { mutableStateOf(false) } var showFullText by remember { mutableStateOf(false) }
// Cuts the text in the first space after 350 val whereToCut = remember {
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } // Cuts the text in the first space after 350
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
val whereToCut = minOf(firstSpaceAfterCut, firstNewLineAfterCut) minOf(firstSpaceAfterCut, firstNewLineAfterCut)
}
val text = if (showFullText) { val text = if (showFullText) {
content content
@@ -75,6 +75,14 @@ fun isValidURL(url: String?): Boolean {
val richTextDefaults = RichTextStyle().resolveDefaults() val richTextDefaults = RichTextStyle().resolveDefaults()
fun isMarkdown(content: String): Boolean {
return content.startsWith("> ") ||
content.startsWith("# ") ||
content.contains("##") ||
content.contains("__") ||
content.contains("```")
}
@Composable @Composable
fun RichTextViewer( fun RichTextViewer(
content: String, content: String,
@@ -85,58 +93,39 @@ fun RichTextViewer(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val isMarkdown = remember { isMarkdown(content) }
Column(modifier = modifier) { Column(modifier = modifier) {
if (content.startsWith("> ") || if (isMarkdown) {
content.startsWith("# ") || RenderContentAsMarkdown(content, backgroundColor)
content.contains("##") ||
content.contains("__") ||
content.contains("```")
) {
val myMarkDownStyle = richTextDefaults.copy(
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
),
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
.background(
MaterialTheme.colors.onSurface
.copy(alpha = 0.05f)
.compositeOver(backgroundColor)
)
),
stringStyle = richTextDefaults.stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
)
)
)
val markdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
MaterialRichText(
style = myMarkDownStyle
) {
Markdown(
content = markdownWithSpecialContent,
markdownParseOptions = MarkdownParseOptions.Default
)
}
} else { } else {
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, navController)
}
}
}
class RichTextViewerState(
val content: String,
val urlSet: LinkedHashSet<String>,
val imagesForPager: Map<String, ZoomableUrlContent>,
val imageList: List<ZoomableUrlContent>
)
@Composable
private fun RenderRegular(
content: String,
tags: List<List<String>>?,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
var processedState by remember {
mutableStateOf<RichTextViewerState?>(null)
}
LaunchedEffect(key1 = content) {
withContext(Dispatchers.IO) {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect() val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl } val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
val imagesForPager = urlSet.mapNotNull { fullUrl -> val imagesForPager = urlSet.mapNotNull { fullUrl ->
@@ -151,67 +140,71 @@ fun RichTextViewer(
}.associateBy { it.url } }.associateBy { it.url }
val imageList = imagesForPager.values.toList() val imageList = imagesForPager.values.toList()
// FlowRow doesn't work well with paragraphs. So we need to split them processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList)
content.split('\n').forEach { paragraph -> }
FlowRow() { }
val s = if (isArabic(paragraph)) paragraph.trim().split(' ').reversed() else paragraph.trim().split(' ')
s.forEach { word: String -> // FlowRow doesn't work well with paragraphs. So we need to split them
if (canPreview) { processedState?.let { state ->
// Explicit URL content.split('\n').forEach { paragraph ->
val img = imagesForPager[word] FlowRow() {
if (img != null) { val s = if (isArabic(paragraph)) {
ZoomableContentView(img, imageList) paragraph.trim().split(' ')
} else if (urlSet.contains(word)) { .reversed()
UrlPreview(word, "$word ") } else {
} else if (word.startsWith("lnbc", true)) { paragraph.trim().split(' ')
MayBeInvoicePreview(word) }
} else if (word.startsWith("lnurl", true)) { s.forEach { word: String ->
MayBeWithdrawal(word) if (canPreview) {
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { // Explicit URL
ClickableEmail(word) val img = state.imagesForPager[word]
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { if (img != null) {
ClickablePhone(word) ZoomableContentView(img, state.imageList)
} else if (isBechLink(word)) { } else if (state.urlSet.contains(word)) {
BechLink( UrlPreview(word, "$word ")
} else if (word.startsWith("lnbc", true)) {
MayBeInvoicePreview(word)
} else if (word.startsWith("lnurl", true)) {
MayBeWithdrawal(word)
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word)
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
ClickablePhone(word)
} else if (isBechLink(word)) {
BechLink(
word,
canPreview,
backgroundColor,
accountViewModel,
navController
)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word, word,
tags,
canPreview, canPreview,
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
navController navController
) )
} else if (word.startsWith("#")) { } else if (hashTagsPattern.matcher(word).matches()) {
if (tagIndex.matcher(word).matches() && tags != null) { HashTag(word, navController)
TagLink( } else {
word, Text(
tags, text = "$word ",
canPreview, style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
backgroundColor, )
accountViewModel, }
navController } else if (noProtocolUrlValidator.matcher(word).matches()) {
) val matcher = noProtocolUrlValidator.matcher(word)
} else if (hashTagsPattern.matcher(word).matches()) { matcher.find()
HashTag(word, navController) val url = matcher.group(1) // url
} else { val additionalChars = matcher.group(4) ?: "" // additional chars
Text(
text = "$word ",
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) { if (url != null) {
ClickableUrl(url, "https://$url") ClickableUrl(url, "https://$url")
Text("$additionalChars ") Text("$additionalChars ")
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
@@ -219,69 +212,74 @@ fun RichTextViewer(
) )
} }
} else { } else {
if (urlSet.contains(word)) { Text(
ClickableUrl("$word ", word) text = "$word ",
} else if (word.startsWith("lnurl", true)) { style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word) )
if (lnWithdrawal != null) { }
ClickableWithdrawal(withdrawalString = lnWithdrawal) } else {
} else { if (state.urlSet.contains(word)) {
Text( ClickableUrl("$word ", word)
text = "$word ", } else if (word.startsWith("lnurl", true)) {
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
) if (lnWithdrawal != null) {
} ClickableWithdrawal(withdrawalString = lnWithdrawal)
} 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,
navController
)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
navController
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, navController)
} else {
Text(
text = "$word ",
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 = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
) )
} }
} 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,
navController
)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
navController
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, navController)
} else {
Text(
text = "$word ",
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 = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
} }
} }
} }
@@ -291,21 +289,137 @@ fun RichTextViewer(
} }
@Composable @Composable
private fun getDisplayNameFromUserNip19(parsedNip19: Nip19.Return): String? { private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) {
if (parsedNip19.type == Nip19.Type.USER) { val myMarkDownStyle = richTextDefaults.copy(
val userHex = parsedNip19.hex codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
val userBase = LocalCache.getOrCreateUser(userHex) textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
),
modifier = Modifier
.padding(0.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
.background(
MaterialTheme.colors.onSurface
.copy(alpha = 0.05f)
.compositeOver(backgroundColor)
)
),
stringStyle = richTextDefaults.stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary
),
codeStyle = SpanStyle(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f)
.compositeOver(backgroundColor)
)
)
)
val userState by userBase.live().metadata.observeAsState() var markdownWithSpecialContent by remember { mutableStateOf<String?>(null) }
val displayName = userState?.user?.bestDisplayName() var nip19References by remember { mutableStateOf<List<Nip19.Return>>(emptyList()) }
if (displayName !== null) { var refresh by remember { mutableStateOf(0) }
return displayName
LaunchedEffect(key1 = content) {
withContext(Dispatchers.IO) {
nip19References = returnNIP19References(content)
markdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
}
}
LaunchedEffect(key1 = refresh) {
withContext(Dispatchers.IO) {
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
}
}
}
nip19References.forEach {
var baseUser by remember { mutableStateOf<User?>(null) }
var baseNote by remember { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = it.hex) {
withContext(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
}
}
if (it.type == Nip19.Type.USER) {
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
baseUser = user
}
}
}
}
baseNote?.let {
val noteState by it.live().metadata.observeAsState()
if (noteState?.note?.event != null) {
refresh++
}
}
baseUser?.let {
val userState by it.live().metadata.observeAsState()
if (userState?.user?.info != null) {
refresh++
}
}
}
markdownWithSpecialContent?.let {
MaterialRichText(
style = myMarkDownStyle
) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default
)
} }
} }
return null
} }
@Composable private fun getDisplayNameFromNip19(nip19: Nip19.Return): String? {
if (nip19.type == Nip19.Type.USER) {
return LocalCache.users[nip19.hex]?.bestDisplayName()
} else if (nip19.type == Nip19.Type.NOTE) {
return LocalCache.notes[nip19.hex]?.idDisplayNote()
} else if (nip19.type == Nip19.Type.ADDRESS) {
return LocalCache.addressables[nip19.hex]?.idDisplayNote()
} else if (nip19.type == Nip19.Type.EVENT) {
return LocalCache.notes[nip19.hex]?.idDisplayNote() ?: LocalCache.addressables[nip19.hex]?.idDisplayNote()
} else {
return null
}
}
private fun returnNIP19References(content: String): List<Nip19.Return> {
val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (isBechLink(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
parsedNip19?.let {
listOfReferences.add(it)
}
}
}
}
return listOfReferences
}
private fun returnMarkdownWithSpecialContent(content: String): String { private fun returnMarkdownWithSpecialContent(content: String): String {
var returnContent = "" var returnContent = ""
content.split('\n').forEach { paragraph -> content.split('\n').forEach { paragraph ->
@@ -319,7 +433,7 @@ private fun returnMarkdownWithSpecialContent(content: String): String {
} else if (isBechLink(word)) { } else if (isBechLink(word)) {
val parsedNip19 = Nip19.uriToRoute(word) val parsedNip19 = Nip19.uriToRoute(word)
returnContent += if (parsedNip19 !== null) { returnContent += if (parsedNip19 !== null) {
val displayName = getDisplayNameFromUserNip19(parsedNip19) val displayName = getDisplayNameFromNip19(parsedNip19)
if (displayName != null) { if (displayName != null) {
"[@$displayName](nostr://$word) " "[@$displayName](nostr://$word) "
} else { } else {
@@ -209,7 +209,9 @@ private fun LocalImageView(
mutableStateOf<AsyncImagePainter.State?>(null) mutableStateOf<AsyncImagePainter.State?>(null)
} }
val ratio = aspectRatio(content.dim) val ratio = remember {
aspectRatio(content.dim)
}
BoxWithConstraints(contentAlignment = Alignment.Center) { BoxWithConstraints(contentAlignment = Alignment.Center) {
val myModifier = mainImageModifier.also { val myModifier = mainImageModifier.also {
@@ -257,6 +259,7 @@ private fun UrlImageView(
content: ZoomableUrlImage, content: ZoomableUrlImage,
mainImageModifier: Modifier mainImageModifier: Modifier
) { ) {
println("UrlImageView")
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
@@ -269,7 +272,9 @@ private fun UrlImageView(
mutableStateOf<Boolean?>(null) mutableStateOf<Boolean?>(null)
} }
val ratio = aspectRatio(content.dim) val ratio = remember {
aspectRatio(content.dim)
}
LaunchedEffect(key1 = content.url, key2 = imageState) { LaunchedEffect(key1 = content.url, key2 = imageState) {
if (imageState is AsyncImagePainter.State.Success) { if (imageState is AsyncImagePainter.State.Success) {
@@ -38,7 +38,6 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
val noteState by messageSetCard.note.live().metadata.observeAsState() val noteState by messageSetCard.note.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
val noteEvent = note?.event
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -50,10 +49,14 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
LaunchedEffect(key1 = messageSetCard.createdAt()) { LaunchedEffect(key1 = messageSetCard.createdAt()) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
isNew = val newIsNew =
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead) messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt()) NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
if (newIsNew != isNew) {
isNew = newIsNew
}
} }
} }
@@ -68,9 +68,13 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
LaunchedEffect(key1 = multiSetCard.createdAt()) { LaunchedEffect(key1 = multiSetCard.createdAt()) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead) val newIsNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt) NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt)
if (newIsNew != isNew) {
isNew = newIsNew
}
} }
} }
@@ -249,7 +249,11 @@ fun NoteComposeInner(
val createdAt = note.createdAt() val createdAt = note.createdAt()
if (createdAt != null) { if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt) NotificationCache.markAsRead(it, createdAt)
isNew = createdAt > lastTime
val newIsNew = createdAt > lastTime
if (newIsNew != isNew) {
isNew = newIsNew
}
} }
} }
} }
@@ -420,7 +424,8 @@ private fun RenderTextEvent(
) { ) {
val noteEvent = note.event ?: return val noteEvent = note.event ?: return
val eventContent = accountViewModel.decrypt(note) val eventContent = remember { accountViewModel.decrypt(note) }
val modifier = remember { Modifier.fillMaxWidth() }
if (eventContent != null) { if (eventContent != null) {
if (makeItShort && accountViewModel.isLoggedUser(note.author)) { if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
@@ -434,7 +439,7 @@ private fun RenderTextEvent(
TranslatableRichTextViewer( TranslatableRichTextViewer(
content = eventContent, content = eventContent,
canPreview = canPreview && !makeItShort, canPreview = canPreview && !makeItShort,
modifier = Modifier.fillMaxWidth(), modifier = modifier,
tags = noteEvent.tags(), tags = noteEvent.tags(),
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -548,10 +553,10 @@ private fun RenderPrivateMessage(
navController: NavController navController: NavController
) { ) {
val noteEvent = note.event as? PrivateDmEvent ?: return val noteEvent = note.event as? PrivateDmEvent ?: return
val withMe = noteEvent.with(accountViewModel.userProfile().pubkeyHex) val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
if (withMe) { if (withMe) {
val eventContent = accountViewModel.decrypt(note) val eventContent = remember { accountViewModel.decrypt(note) }
if (eventContent != null) { if (eventContent != null) {
if (makeItShort && accountViewModel.isLoggedUser(note.author)) { if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
@@ -908,7 +913,7 @@ fun TimeAgo(time: Long) {
@Composable @Composable
private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) { private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) {
val baseChannel = baseNote.channel() val baseChannel = remember { baseNote.channel() }
Column(Modifier.width(55.dp)) { Column(Modifier.width(55.dp)) {
// Draws the boosted picture outside the boosted card. // Draws the boosted picture outside the boosted card.
@@ -1002,7 +1007,13 @@ fun DisplayHighlight(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val quote = highlight.split("\n").map { "> *${it.removeSuffix(" ")}*" }.joinToString("\n") val quote =
remember {
highlight
.split("\n")
.map { "> *${it.removeSuffix(" ")}*" }
.joinToString("\n")
}
TranslatableRichTextViewer( TranslatableRichTextViewer(
quote, quote,
@@ -1205,9 +1216,9 @@ fun BadgeDisplay(baseNote: Note) {
val background = MaterialTheme.colors.background val background = MaterialTheme.colors.background
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
val image = badgeData.image() val image = remember { badgeData.image() }
val name = badgeData.name() val name = remember { badgeData.name() }
val description = badgeData.description() val description = remember { badgeData.description() }
var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) } var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) }
var imageResult by remember { mutableStateOf<AsyncImagePainter.State.Success?>(null) } var imageResult by remember { mutableStateOf<AsyncImagePainter.State.Success?>(null) }
@@ -73,8 +73,6 @@ fun TranslatableRichTextViewer(
showOriginal = preference == task.result.sourceLang showOriginal = preference == task.result.sourceLang
} }
translatedTextState.value = task.result translatedTextState.value = task.result
} else {
translatedTextState.value = ResultOrError(content, null, null, null)
} }
} }
} }