Simplifying badge composables

This commit is contained in:
Vitor Pamplona
2025-08-01 16:10:19 -04:00
parent 6b946a57c2
commit eed4350bca
4 changed files with 78 additions and 95 deletions

View File

@@ -27,6 +27,7 @@ import androidx.compose.runtime.ProduceStateScope
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -43,9 +44,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
@Composable
@@ -94,20 +93,13 @@ fun LoadAddressableNote(
accountViewModel: AccountViewModel,
content: @Composable (AddressableNote?) -> Unit,
) {
var note by
remember(address) {
mutableStateOf(accountViewModel.getAddressableNoteIfExists(address))
}
if (note == null) {
LaunchedEffect(key1 = address) {
val newNote =
withContext(Dispatchers.IO) {
accountViewModel.getOrCreateAddressableNote(address)
}
if (note != newNote) {
note = newNote
}
val note by produceState(
accountViewModel.getAddressableNoteIfExists(address),
address,
) {
val newNote = accountViewModel.getOrCreateAddressableNote(address)
if (newNote != value) {
value = newNote
}
}

View File

@@ -20,25 +20,18 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges
import android.R.attr.onClick
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
@@ -46,23 +39,24 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.BadgePictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun DisplayBadges(
@@ -114,17 +108,20 @@ private fun LoadAndRenderBadge(
accountViewModel: AccountViewModel,
nav: INav,
) {
var baseNote by remember(badgeAwardEvent) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEvent)) }
LaunchedEffect(key1 = badgeAwardEvent) {
if (baseNote == null) {
withContext(Dispatchers.IO) {
baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEvent)
val baseNote =
produceState(
LocalCache.getNoteIfExists(badgeAwardEvent),
badgeAwardEvent,
) {
val newValue = LocalCache.checkGetOrCreateNote(badgeAwardEvent)
if (newValue != value) {
value = newValue
}
}
}
baseNote?.let { ObserveAndRenderBadge(it, accountViewModel, nav) }
baseNote.value?.let {
ObserveAndRenderBadge(it, accountViewModel, nav)
}
}
@Composable
@@ -133,90 +130,80 @@ private fun ObserveAndRenderBadge(
accountViewModel: AccountViewModel,
nav: INav,
) {
val badgeAwardState by observeNote(it, accountViewModel)
val baseBadgeDefinition = badgeAwardState.note.replyTo?.firstOrNull()
baseBadgeDefinition?.let { BadgeThumb(it, accountViewModel, nav, Size35dp) }
}
@Composable
fun BadgeThumb(
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
size: Dp,
pictureModifier: Modifier = Modifier,
) {
BadgeThumb(note, accountViewModel, size, pictureModifier) { nav.nav(Route.Note(note.idHex)) }
val badgeAwardState by observeNoteEvent<BadgeAwardEvent>(it, accountViewModel)
val badgeDefinitionId = badgeAwardState?.awardDefinition()?.firstOrNull()
if (badgeDefinitionId != null) {
LoadAddressableNote(badgeDefinitionId, accountViewModel) { badgeDefNote ->
badgeDefNote?.let {
BadgeThumb(it, accountViewModel, nav)
}
}
}
}
@Composable
fun BadgeThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier,
onClick: ((String) -> Unit)? = null,
nav: INav,
) {
Box(
remember {
Modifier
.width(size)
.height(size)
},
modifier =
Size35Modifier.clickable(
onClick = {
nav.nav {
routeFor(baseNote, accountViewModel.account)
}
},
),
) {
WatchAndRenderBadgeImage(baseNote, size, pictureModifier, accountViewModel, onClick)
WatchAndRenderBadgeImage(baseNote, accountViewModel)
}
}
@Composable
private fun WatchAndRenderBadgeImage(
baseNote: Note,
size: Dp,
pictureModifier: Modifier,
accountViewModel: AccountViewModel,
onClick: ((String) -> Unit)?,
) {
val noteState by observeNote(baseNote, accountViewModel)
val eventId = remember(noteState) { noteState?.note?.idHex } ?: return
val image by
remember(noteState) {
derivedStateOf {
val event = noteState.note.event as? BadgeDefinitionEvent
val event by observeNoteEvent<BadgeDefinitionEvent>(baseNote, accountViewModel)
event?.let {
val image =
remember(event) {
event?.thumb()?.ifBlank { null } ?: event?.image()?.ifBlank { null }
}
RenderBadgeImage(it.id, it.name(), image, accountViewModel)
}
}
@Composable
private fun RenderBadgeImage(
id: String,
name: String?,
image: String?,
accountViewModel: AccountViewModel,
) {
val description =
if (name != null) {
stringRes(id = R.string.badge_award_image_for, name)
} else {
stringRes(id = R.string.badge_award_image)
}
if (image == null) {
RobohashAsyncImage(
robot = "authornotfound",
contentDescription = stringRes(R.string.unknown_author),
modifier =
remember {
pictureModifier
.width(size)
.height(size)
},
robot = "badgenotfound",
contentDescription = description,
modifier = BadgePictureModifier,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
} else {
RobohashFallbackAsyncImage(
robot = eventId,
model = image!!,
contentDescription = stringRes(id = R.string.profile_image),
modifier =
remember {
pictureModifier
.width(size)
.height(size)
.clip(shape = CutCornerShape(20))
.run {
if (onClick != null) {
this.clickable(onClick = { onClick(eventId) })
} else {
this
}
}
},
robot = id,
model = image,
contentDescription = description,
modifier = BadgePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)

View File

@@ -35,6 +35,7 @@ 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.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Shapes
@@ -344,3 +345,5 @@ val SimpleImage35Modifier = Modifier.size(Size34dp).clip(shape = CircleShape)
val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder)
val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp)
val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20))

View File

@@ -311,6 +311,7 @@
<string name="biometric_error">Error</string>
<string name="badge_created_by">"Created by %1$s"</string>
<string name="badge_award_image_for">"Badge award image for %1$s"</string>
<string name="badge_award_image">"Badge award image</string>
<string name="new_badge_award_notif">You Received a new Badge Award</string>
<string name="award_granted_to">Badge award granted to</string>
<string name="copied_note_text_to_clipboard">Copied note text to clipboard</string>