diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index a834331c41..570c04cb04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -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 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index 47d054a68c..0311015e86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -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(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(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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index d5bbcb764d..272519c0c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -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)) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index b6517ec190..fb6d27b6fb 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -311,6 +311,7 @@ Error "Created by %1$s" "Badge award image for %1$s" + "Badge award image You Received a new Badge Award Badge award granted to Copied note text to clipboard