From 585b28163a8ab865e78439cfb6efe0b930f0bcb4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 May 2026 18:25:07 -0400 Subject: [PATCH] Better rendering of Public Chats --- .../ui/components/SensitivityWarning.kt | 8 +- .../amethyst/ui/note/DisplayAuthorBanner.kt | 27 +++++- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 9 +- .../ui/note/elements/DefaultImageHeader.kt | 40 ++++----- .../RenderPublicChatChannelThumb.kt | 5 +- .../publicChats/PublicChatsFeedLoaded.kt | 84 +++++++++++++++---- .../nip28PublicChats/PublicChatListState.kt | 14 ++++ 7 files changed, 133 insertions(+), 54 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index ddbb336cbc..fd8d7b7e29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -82,7 +82,13 @@ fun SensitivityWarning( accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - note.event?.let { SensitivityWarning(it, accountViewModel, content) } + val noteEvent = note.event + + if (noteEvent == null) { + content() + } else { + SensitivityWarning(noteEvent, accountViewModel, content) + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt index 337c19e403..67eb8ac256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt @@ -21,23 +21,44 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote +import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder @Composable fun DisplayAuthorBanner( - note: Note, + baseNote: Note, accountViewModel: AccountViewModel, modifier: Modifier = SimpleImageBorder, ) { - WatchAuthor(note, accountViewModel) { + val noteAuthor = baseNote.author + if (noteAuthor != null) { BannerImage( - it, + noteAuthor, modifier, accountViewModel, ) + } else { + val authorState by observeNote(baseNote, accountViewModel) + CrossfadeIfEnabled(authorState.note.author, accountViewModel = accountViewModel) { author -> + if (author != null) { + BannerImage( + author, + modifier, + accountViewModel, + ) + } else { + BannerImage( + null as String?, + modifier, + accountViewModel, + ) + } + } } } 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 28e04659bb..a7e17d4536 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 @@ -154,12 +154,11 @@ fun LoadPublicChatChannel( accountViewModel: AccountViewModel, content: @Composable (PublicChatChannel) -> Unit, ) { - val channel = - produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreatePublicChatChannel(id) - } + val channel by produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreatePublicChatChannel(id) + } - channel.value?.let { content(it) } + channel?.let { content(it) } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index dabcf9a69f..943c105290 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -119,38 +119,32 @@ fun BannerImage( if (!banner.isNullOrBlank()) { MyAsyncImage( imageUrl = banner, - contentDescription = - stringRes( - R.string.preview_card_image_for, - banner, - ), + contentDescription = stringRes(R.string.preview_card_image_for, banner), contentScale = ContentScale.Crop, mainImageModifier = Modifier, loadedImageModifier = modifier, accountViewModel = accountViewModel, onLoadingBackground = { - Image( - painter = painterRes(R.drawable.profile_banner, 4), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 4) }, onError = { - Image( - painter = painterRes(R.drawable.profile_banner, 4), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 4) }, ) } else { - Image( - painter = painterRes(R.drawable.profile_banner, 5), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 5) } } + +@Composable +fun DefaultProfileBanner( + modifier: Modifier, + sizeReference: Int, +) { + Image( + painter = painterRes(R.drawable.profile_banner, sizeReference), + contentDescription = stringRes(R.string.profile_banner), + contentScale = ContentScale.Crop, + modifier = modifier, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 2d2cc7f720..e5bab74702 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -68,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -81,8 +80,6 @@ fun RenderPublicChatChannelThumb( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? ChannelCreateEvent ?: return - LoadPublicChatChannel(baseNote.idHex, accountViewModel) { RenderPublicChatChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav) } @@ -96,7 +93,7 @@ fun RenderPublicChatChannelThumb( nav: INav, ) { val channelUpdates by observeChannel(channel, accountViewModel) - val publicChat = channelUpdates?.channel as PublicChatChannel + val publicChat = (channelUpdates?.channel as? PublicChatChannel) ?: channel val name = remember(channelUpdates) { publicChat.toBestDisplayName() } val description = remember(channelUpdates) { publicChat.summary()?.ifBlank { null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index b15269c53b..02e1a22882 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -45,15 +46,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport +import com.vitorpamplona.amethyst.ui.note.ClickableNote +import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction +import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent +import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.amethyst.ui.theme.StdPadding @Composable fun PublicChatsFeedLoaded( @@ -66,12 +72,8 @@ fun PublicChatsFeedLoaded( val followedSet by accountViewModel.account.publicChatList.flowSet .collectAsStateWithLifecycle() - val pinned = - remember(followedSet) { - followedSet.mapNotNull { idHex -> - LocalCache.getNoteIfExists(idHex)?.takeIf { it.event is ChannelCreateEvent } - } - } + val pinned by accountViewModel.account.publicChatList.flowSetNote + .collectAsStateWithLifecycle() val unpinned = remember(items.list, followedSet) { @@ -91,7 +93,6 @@ fun PublicChatsFeedLoaded( items( pinned, key = { item -> "pinned-" + item.idHex }, - contentType = { item -> item.event?.kind ?: -1 }, ) { item -> PublicChatRow(item, pinned = true, accountViewModel, nav) } @@ -105,7 +106,6 @@ fun PublicChatsFeedLoaded( itemsIndexed( unpinned, key = { _, item -> item.idHex }, - contentType = { _, item -> item.event?.kind ?: -1 }, ) { _, item -> PublicChatRow(item, pinned = false, accountViewModel, nav) } @@ -114,20 +114,34 @@ fun PublicChatsFeedLoaded( @Composable private fun LazyItemScope.PublicChatRow( - item: Note, + baseNote: Note, pinned: Boolean, accountViewModel: AccountViewModel, nav: INav, ) { + val modifier = Modifier.fillMaxWidth() + Box(Modifier.fillMaxWidth().animateItem()) { - ChannelCardCompose( - baseNote = item, - routeForLastRead = "PublicChatsFeed", - modifier = Modifier.fillMaxWidth(), - forceEventKind = ChannelCreateEvent.KIND, + WatchNoteEvent( + baseNote = baseNote, accountViewModel = accountViewModel, - nav = nav, + onBlank = { + RenderChannel(baseNote, modifier, accountViewModel, nav) + }, + onNoteEventFound = { + CheckHiddenFeedWatchBlockAndReport( + note = baseNote, + modifier = modifier, + ignoreAllBlocksAndReports = false, + showHiddenWarning = false, + accountViewModel = accountViewModel, + nav = nav, + ) { _ -> + RenderChannel(baseNote, modifier, accountViewModel, nav) + } + }, ) + if (pinned) { PinBadge( modifier = @@ -143,6 +157,40 @@ private fun LazyItemScope.PublicChatRow( ) } +@Composable +private fun RenderChannel( + baseNote: Note, + modifier: Modifier, + accountViewModel: AccountViewModel, + nav: INav, +) { + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> + ClickableNote( + baseNote = baseNote, + backgroundColor = + calculateBackgroundColor( + createdAt = baseNote.createdAt(), + routeForLastRead = "PublicChatsFeed", + parentBackgroundColor = null, + accountViewModel = accountViewModel, + ), + modifier = modifier, + accountViewModel = accountViewModel, + showPopup = showPopup, + nav = nav, + ) { + Column(StdPadding) { + SensitivityWarning( + note = baseNote, + accountViewModel = accountViewModel, + ) { + RenderPublicChatChannelThumb(baseNote, accountViewModel, nav) + } + } + } + } +} + @Composable private fun PinBadge(modifier: Modifier = Modifier) { Box( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index 2a676de5f9..3103bc76a7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest @@ -96,6 +97,19 @@ class PublicChatListState( emptySet(), ) + val flowSetNote = + flowSet + .map { + it.mapNotNull { + cache.checkGetOrCreateNote(it) + } + }.flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + suspend fun follow(channel: PublicChatChannel): ChannelListEvent { val publicChatList = getChannelList()