From 2503cbaeac1dd65a13ee80622d8e22ccbe1c68c0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 13 Jun 2024 16:50:19 -0400 Subject: [PATCH] - Unifies All Badge rendering - Constraints badge layouts on Notes - Creates a flow cache to speed up Note relay layout - Speeds up NoteQuickActionMenus - Optimizes NIP-11 pings - Better error messages when NIP-11 fails --- .../com/vitorpamplona/amethyst/model/Note.kt | 25 ++-- .../ui/components/RobohashAsyncImage.kt | 21 ++- .../ui/note/ChatroomMessageCompose.kt | 19 +-- .../amethyst/ui/note/NoteCompose.kt | 37 +++-- .../amethyst/ui/note/NoteQuickActionMenu.kt | 53 ++++--- .../amethyst/ui/note/RelayListBox.kt | 134 ++++++++++++++---- .../amethyst/ui/note/RelayListRow.kt | 90 +++--------- .../ui/screen/loggedIn/AccountViewModel.kt | 18 +++ .../ui/screen/loggedIn/VideoScreen.kt | 23 +-- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 14 +- app/src/main/res/values/strings.xml | 6 + 11 files changed, 240 insertions(+), 200 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index fe54cb03f..a11f288bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -418,7 +418,7 @@ open class Note( fun addRelay(relay: Relay) { if (relay.brief !in relays) { addRelaySync(relay.brief) - liveSet?.innerRelays?.invalidateData() + flowSet?.relays?.invalidateData() } } @@ -847,12 +847,17 @@ class NoteFlowSet( // Observers line up here. val metadata = NoteBundledRefresherFlow(u) val reports = NoteBundledRefresherFlow(u) + val relays = NoteBundledRefresherFlow(u) - fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0 || reports.stateFlow.subscriptionCount.value > 0 + fun isInUse(): Boolean = + metadata.stateFlow.subscriptionCount.value > 0 || + reports.stateFlow.subscriptionCount.value > 0 || + relays.stateFlow.subscriptionCount.value > 0 fun destroy() { metadata.destroy() reports.destroy() + relays.destroy() } } @@ -865,7 +870,6 @@ class NoteLiveSet( val innerReactions = NoteBundledRefresherLiveData(u) val innerBoosts = NoteBundledRefresherLiveData(u) val innerReplies = NoteBundledRefresherLiveData(u) - val innerRelays = NoteBundledRefresherLiveData(u) val innerZaps = NoteBundledRefresherLiveData(u) val innerOts = NoteBundledRefresherLiveData(u) val innerModifications = NoteBundledRefresherLiveData(u) @@ -874,7 +878,6 @@ class NoteLiveSet( val reactions = innerReactions.map { it } val boosts = innerBoosts.map { it } val replies = innerReplies.map { it } - val relays = innerRelays.map { it } val zaps = innerZaps.map { it } val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged() @@ -900,8 +903,6 @@ class NoteLiveSet( val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged() - val relayInfo = innerRelays.map { it.note.relays } - val content = innerMetadata.map { it.note.event?.content() ?: "" } fun isInUse(): Boolean = @@ -909,7 +910,6 @@ class NoteLiveSet( reactions.hasObservers() || boosts.hasObservers() || replies.hasObservers() || - relays.hasObservers() || zaps.hasObservers() || hasEvent.hasObservers() || hasReactions.hasObservers() || @@ -924,7 +924,6 @@ class NoteLiveSet( innerReactions.destroy() innerBoosts.destroy() innerReplies.destroy() - innerRelays.destroy() innerZaps.destroy() innerOts.destroy() innerModifications.destroy() @@ -936,6 +935,7 @@ class NoteBundledRefresherFlow( val note: Note, ) { // Refreshes observers in batches. + // TODO: Replace the bundler for .sample private val bundler = BundledUpdate(500, Dispatchers.IO) val stateFlow = MutableStateFlow(NoteState(note)) @@ -1015,11 +1015,12 @@ object RelayBriefInfoCache { val cache = LruCache(50) @Immutable - data class RelayBriefInfo( + class RelayBriefInfo( val url: String, - val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern(), - val favIcon: String = "https://$displayUrl/favicon.ico".intern(), - ) + ) { + val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern() + val favIcon: String = "https://$displayUrl/favicon.ico".intern() + } fun get(url: String): RelayBriefInfo { val info = cache[url] diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index 9a947f999..a730b2762 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -87,13 +87,11 @@ fun RobohashFallbackAsyncImage( filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, loadProfilePicture: Boolean, ) { - val context = LocalContext.current - val isLightTheme = MaterialTheme.colorScheme.isLight - if (model != null && loadProfilePicture) { val isBase64 by remember { derivedStateOf { model.startsWith("data:image/jpeg;base64,") } } if (isBase64) { + val context = LocalContext.current val base64Painter = rememberAsyncImagePainter( model = Base64Requester.imageRequest(context, model), @@ -110,7 +108,7 @@ fun RobohashFallbackAsyncImage( } else { val painter = rememberVectorPainter( - image = CachedRobohash.get(robot, isLightTheme), + image = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight), ) AsyncImage( @@ -128,7 +126,7 @@ fun RobohashFallbackAsyncImage( ) } } else { - val robotPainter = CachedRobohash.get(robot, isLightTheme) + val robotPainter = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight) Image( imageVector = robotPainter, @@ -145,9 +143,12 @@ object Base64Requester { fun imageRequest( context: Context, message: String, - ): ImageRequest { - return ImageRequest.Builder(context).data(message).fetcherFactory(Base64Fetcher.Factory).build() - } + ): ImageRequest = + ImageRequest + .Builder(context) + .data(message) + .fetcherFactory(Base64Fetcher.Factory) + .build() } @Stable @@ -179,8 +180,6 @@ class Base64Fetcher( data: Uri, options: Options, imageLoader: ImageLoader, - ): Fetcher { - return Base64Fetcher(options, data) - } + ): Fetcher = Base64Fetcher(options, data) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 8d64ae0bf..9f0ffe7e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -190,10 +190,9 @@ fun NormalChatNote( nav("User/${it.pubkeyHex}") } }, - actionMenu = { popupExpanded, onDismiss -> + actionMenu = { onDismiss -> NoteQuickActionMenu( note = note, - popupExpanded = popupExpanded, onDismiss = onDismiss, onWantsToEditDraft = { onWantsToEditDraft(note) }, accountViewModel = accountViewModel, @@ -256,7 +255,7 @@ fun ChatBubbleLayout( parentBackgroundColor: MutableState? = null, onClick: () -> Boolean, onAuthorClick: () -> Unit, - actionMenu: @Composable (popupExpanded: Boolean, onDismiss: () -> Unit) -> Unit, + actionMenu: @Composable (onDismiss: () -> Unit) -> Unit, detailRow: @Composable () -> Unit, drawAuthorLine: @Composable () -> Unit, inner: @Composable (MutableState) -> Unit, @@ -294,7 +293,7 @@ fun ChatBubbleLayout( modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, horizontalArrangement = alignment, ) { - var popupExpanded by remember { mutableStateOf(false) } + var popupExpanded = remember { mutableStateOf(false) } val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier @@ -319,7 +318,7 @@ fun ChatBubbleLayout( } } }, - onLongClick = { popupExpanded = true }, + onLongClick = { popupExpanded.value = true }, ) } @@ -357,8 +356,10 @@ fun ChatBubbleLayout( } } - actionMenu(popupExpanded) { - popupExpanded = false + if (popupExpanded.value) { + actionMenu { + popupExpanded.value = false + } } } } @@ -381,7 +382,7 @@ private fun BubblePreview() { parentBackgroundColor = backgroundBubbleColor, onClick = { false }, onAuthorClick = {}, - actionMenu = { popupExpanded, onDismiss -> + actionMenu = { onDismiss -> }, drawAuthorLine = { UserDisplayNameLayout( @@ -411,7 +412,7 @@ private fun BubblePreview() { parentBackgroundColor = backgroundBubbleColor, onClick = { false }, onAuthorClick = {}, - actionMenu = { popupExpanded, onDismiss -> + actionMenu = { onDismiss -> }, drawAuthorLine = { UserDisplayNameLayout( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index fee6e852a..dbb6e76a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -121,6 +120,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp @@ -179,7 +179,6 @@ import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent -import kotlinx.coroutines.launch @Composable fun NoteCompose( @@ -416,22 +415,18 @@ fun ClickableNote( nav: (String) -> Unit, content: @Composable () -> Unit, ) { - val scope = rememberCoroutineScope() - val updatedModifier = remember(baseNote, backgroundColor.value) { modifier .combinedClickable( onClick = { - scope.launch { - val redirectToNote = - if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { - baseNote.replyTo?.lastOrNull() ?: baseNote - } else { - baseNote - } - routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) } - } + val redirectToNote = + if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { + baseNote.replyTo?.lastOrNull() ?: baseNote + } else { + baseNote + } + routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) } }, onLongClick = showPopup, ).background(backgroundColor.value) @@ -465,9 +460,7 @@ fun InnerNoteWithReactions( }, ) { if (notBoostedNorQuote) { - Column(WidthAuthorPictureModifier) { - AuthorAndRelayInformation(baseNote, accountViewModel, nav) - } + AuthorAndRelayInformation(baseNote, accountViewModel, nav) Spacer(modifier = DoubleHorzSpacer) } @@ -1064,12 +1057,14 @@ private fun AuthorAndRelayInformation( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - // Draws the boosted picture outside the boosted card. - Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) { - RenderAuthorImages(baseNote, nav, accountViewModel) - } + Column(WidthAuthorPictureModifier, verticalArrangement = RowColSpacing5dp) { + // Draws the boosted picture outside the boosted card. + Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) { + RenderAuthorImages(baseNote, nav, accountViewModel) + } - BadgeBox(baseNote, accountViewModel, nav) + BadgeBox(baseNote, accountViewModel, nav) + } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 8ad842556..94b058d57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -137,23 +137,22 @@ fun LongPressToQuickAction( content: @Composable (() -> Unit) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } - val showPopup = remember { { popupExpanded.value = true } } - content(showPopup) + content { popupExpanded.value = true } - NoteQuickActionMenu( - note = baseNote, - popupExpanded = popupExpanded.value, - onDismiss = { popupExpanded.value = false }, - accountViewModel = accountViewModel, - nav = {}, - ) + if (popupExpanded.value) { + NoteQuickActionMenu( + note = baseNote, + onDismiss = { popupExpanded.value = false }, + accountViewModel = accountViewModel, + nav = {}, + ) + } } @Composable fun NoteQuickActionMenu( note: Note, - popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -173,7 +172,6 @@ fun NoteQuickActionMenu( NoteQuickActionMenu( note = note, - popupExpanded = popupExpanded, onDismiss = onDismiss, onWantsToEditDraft = { editDraftDialog.value = true }, accountViewModel = accountViewModel, @@ -184,7 +182,6 @@ fun NoteQuickActionMenu( @Composable fun NoteQuickActionMenu( note: Note, - popupExpanded: Boolean, onDismiss: () -> Unit, onWantsToEditDraft: () -> Unit, accountViewModel: AccountViewModel, @@ -195,17 +192,15 @@ fun NoteQuickActionMenu( val showBlockAlertDialog = remember { mutableStateOf(false) } val showReportDialog = remember { mutableStateOf(false) } - if (popupExpanded) { - RenderMainPopup( - accountViewModel, - note, - onDismiss, - showBlockAlertDialog, - showDeleteAlertDialog, - showReportDialog, - onWantsToEditDraft, - ) - } + RenderMainPopup( + accountViewModel, + note, + onDismiss, + showBlockAlertDialog, + showDeleteAlertDialog, + showReportDialog, + onWantsToEditDraft, + ) if (showSelectTextDialog.value) { val decryptedNote = remember { mutableStateOf(null) } @@ -267,12 +262,12 @@ private fun RenderMainPopup( val showToast = { stringResource: Int -> scope.launch { - Toast.makeText( - context, - context.getString(stringResource), - Toast.LENGTH_SHORT, - ) - .show() + Toast + .makeText( + context, + context.getString(stringResource), + Toast.LENGTH_SHORT, + ).show() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 2d946e5a1..893c142c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,10 +41,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,41 +50,91 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer +import com.vitorpamplona.amethyst.ui.theme.Size17Modifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.noteComposeRelayBox import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.flow.map -@OptIn(ExperimentalLayoutApi::class) @Composable fun RelayBadges( baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - var expanded by remember { mutableStateOf(false) } + val expanded = remember { mutableStateOf(false) } - val relayList by baseNote.live().relayInfo.observeAsState(baseNote.relays) - - Spacer(StdVertSpacer) - - // FlowRow Seems to be a lot faster than LazyVerticalGrid - Box(modifier = Modifier.fillMaxWidth().padding(start = 2.dp, end = 1.dp)) { - FlowRow(modifier = Modifier.fillMaxWidth()) { - if (expanded) { - relayList?.forEach { RenderRelay(it, accountViewModel, nav) } - } else { - relayList?.getOrNull(0)?.let { RenderRelay(it, accountViewModel, nav) } - relayList?.getOrNull(1)?.let { RenderRelay(it, accountViewModel, nav) } - relayList?.getOrNull(2)?.let { RenderRelay(it, accountViewModel, nav) } + Crossfade(expanded.value, modifier = noteComposeRelayBox, label = "RelayBadges") { + if (it) { + RenderAllRelayList(baseNote, Modifier.fillMaxWidth(), accountViewModel = accountViewModel, nav = nav) + } else { + Column { + RenderClosedRelayList(baseNote, Modifier.fillMaxWidth(), accountViewModel = accountViewModel, nav = nav) + ShouldShowExpandButton(baseNote, accountViewModel) { ShowMoreRelaysButton { expanded.value = true } } } } } +} - if (relayList.size > 3 && !expanded) { - ShowMoreRelaysButton { expanded = true } +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RenderAllRelayList( + baseNote: Note, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteRelays by baseNote + .flow() + .relays.stateFlow + .collectAsStateWithLifecycle() + + FlowRow(modifier, verticalArrangement = verticalArrangement) { + noteRelays.note.relays.forEach { RenderRelay(it, accountViewModel, nav) } + } +} + +@Composable +fun RenderClosedRelayList( + baseNote: Note, + modifier: Modifier = Modifier, + verticalAlignment: Alignment.Vertical = Alignment.Top, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Row(modifier, verticalAlignment = verticalAlignment) { + WatchAndRenderRelay(baseNote, 0, accountViewModel, nav) + WatchAndRenderRelay(baseNote, 1, accountViewModel, nav) + WatchAndRenderRelay(baseNote, 2, accountViewModel, nav) + } +} + +@Composable +fun WatchAndRenderRelay( + baseNote: Note, + relayIndex: Int, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteRelays by baseNote + .flow() + .relays.stateFlow + .map { + it.note.relays.getOrNull(relayIndex) + }.collectAsStateWithLifecycle( + baseNote.relays.getOrNull(relayIndex), + ) + + Crossfade(targetState = noteRelays, label = "RenderRelay", modifier = Size17Modifier) { + if (it != null) { + RenderRelay(it, accountViewModel, nav) + } } } @@ -97,46 +146,79 @@ fun RelayIconLayoutPreview() { Spacer(StdVertSpacer) // FlowRow Seems to be a lot faster than LazyVerticalGrid - Box(modifier = Modifier.fillMaxWidth().padding(start = 2.dp, end = 1.dp)) { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 2.dp, end = 1.dp), + ) { FlowRow { Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { Box( - Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black), + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), ) } Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { - Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + Box( + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), + ) } Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { - Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + Box( + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), + ) } Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { - Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + Box( + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), + ) } Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { - Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + Box( + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), + ) } Box( modifier = Modifier.size(17.dp), contentAlignment = Alignment.Center, ) { - Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + Box( + Modifier + .size(13.dp) + .clip(shape = CircleShape) + .background(Color.Black), + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 7e8b9671f..7486e908e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -21,32 +21,24 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource 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.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.lifecycle.map +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfoCache @@ -70,42 +62,25 @@ public fun RelayBadgesHorizontal( ) { val expanded = remember { mutableStateOf(false) } - RenderRelayList(baseNote, expanded, accountViewModel, nav) - - RenderExpandButton(baseNote, expanded) { ChatRelayExpandButton { expanded.value = true } } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun RenderRelayList( - baseNote: Note, - expanded: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteRelays by baseNote.live().relayInfo.observeAsState(baseNote.relays) - - FlowRow(StdStartPadding, verticalArrangement = Arrangement.Center) { - if (expanded.value) { - noteRelays?.forEach { RenderRelay(it, accountViewModel, nav) } - } else { - noteRelays?.getOrNull(0)?.let { RenderRelay(it, accountViewModel, nav) } - noteRelays?.getOrNull(1)?.let { RenderRelay(it, accountViewModel, nav) } - noteRelays?.getOrNull(2)?.let { RenderRelay(it, accountViewModel, nav) } + if (expanded.value) { + RenderAllRelayList(baseNote, StdStartPadding, verticalArrangement = Arrangement.Center, accountViewModel, nav) + } else { + RenderClosedRelayList(baseNote, StdStartPadding, verticalAlignment = Alignment.CenterVertically, accountViewModel = accountViewModel, nav = nav) + ShouldShowExpandButton(baseNote, accountViewModel) { + ChatRelayExpandButton { expanded.value = true } } } } @Composable -fun RenderExpandButton( +fun ShouldShowExpandButton( baseNote: Note, - expanded: MutableState, + accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - val showExpandButton by - baseNote.live().relays.map { it.note.relays.size > 3 }.observeAsState(baseNote.relays.size > 3) + val showExpandButton by accountViewModel.createMustShowExpandButtonFlows(baseNote).collectAsStateWithLifecycle() - if (showExpandButton && !expanded.value) { + if (showExpandButton) { content() } } @@ -159,19 +134,11 @@ fun RenderRelay( ) } - val context = LocalContext.current - - val interactionSource = remember { MutableInteractionSource() } - val ripple = rememberRipple(bounded = false, radius = Size17dp) - val clickableModifier = remember(relay) { Modifier .size(Size17dp) .clickable( - role = Role.Button, - interactionSource = interactionSource, - indication = ripple, onClick = { accountViewModel.retrieveRelayDocument( relay.url, @@ -179,40 +146,23 @@ fun RenderRelay( openRelayDialog = true }, onError = { url, errorCode, exceptionMessage -> - val msg = + accountViewModel.toast( + R.string.unable_to_download_relay_document, when (errorCode) { Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - context.getString( - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) + R.string.relay_information_document_error_failed_to_assemble_url Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - context.getString( - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) + R.string.relay_information_document_error_failed_to_reach_server Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - context.getString( - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) + R.string.relay_information_document_error_failed_to_parse_response Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - context.getString( - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - } - - accountViewModel.toast( - context.getString(R.string.unable_to_download_relay_document), - msg, + R.string.relay_information_document_error_failed_with_http + }, + url, + exceptionMessage ?: errorCode.toString(), ) }, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index adbbd46ee..f06311cbb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -322,6 +323,23 @@ class AccountViewModel( noteIsHiddenFlows.put(note, it) } + val noteMustShowExpandButtonFlows = LruCache>(300) + + fun createMustShowExpandButtonFlows(note: Note): StateFlow = + noteMustShowExpandButtonFlows.get(note) + ?: note + .flow() + .relays + .stateFlow + .map { it.note.relays.size > 3 } + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + note.relays.size > 3, + ).also { + noteMustShowExpandButtonFlows.put(note, it) + } + fun hasReactedTo( baseNote: Note, reaction: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 7a4f86202..0c230a69e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -26,8 +26,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -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.fillMaxHeight @@ -49,7 +47,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -74,7 +71,7 @@ import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay -import com.vitorpamplona.amethyst.ui.note.RenderRelay +import com.vitorpamplona.amethyst.ui.note.RenderAllRelayList import com.vitorpamplona.amethyst.ui.note.ReplyReaction import com.vitorpamplona.amethyst.ui.note.ViewCountReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction @@ -312,7 +309,9 @@ private fun RenderAuthorInformation( Spacer(modifier = DoubleHorzSpacer) Column( - Modifier.height(65.dp).weight(1f), + Modifier + .height(65.dp) + .weight(1f), verticalArrangement = Arrangement.Center, ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -332,7 +331,7 @@ private fun RenderAuthorInformation( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 2.dp), ) { - RelayBadges(baseNote = note, accountViewModel, nav) + RenderAllRelayList(baseNote = note, accountViewModel = accountViewModel, nav = nav) } } } @@ -369,18 +368,6 @@ private fun VideoUserOptionAction( } } -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun RelayBadges( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteRelays by baseNote.live().relayInfo.observeAsState(baseNote.relays) - - FlowRow { noteRelays?.forEach { relayInfo -> RenderRelay(relayInfo, accountViewModel, nav) } } -} - @Composable fun ReactionsColumn( baseNote: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 4c9273384..83939b393 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -167,7 +168,7 @@ val HeaderPictureModifier = Modifier.size(34.dp).clip(shape = CircleShape) val ShowMoreRelaysButtonIconButtonModifier = Modifier.size(15.dp) val ShowMoreRelaysButtonIconModifier = Modifier.size(20.dp) -val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(17.dp) +val ShowMoreRelaysButtonBoxModifer = Modifier.width(55.dp).height(17.dp) val ChatBubbleMaxSizeModifier = Modifier.fillMaxWidth(0.85f) @@ -194,7 +195,8 @@ val ButtonPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) val ChatPaddingInnerQuoteModifier = Modifier.padding(top = 10.dp) val ChatPaddingModifier = - Modifier.fillMaxWidth(1f) + Modifier + .fillMaxWidth(1f) .padding( start = 12.dp, end = 12.dp, @@ -217,7 +219,8 @@ val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp) val authorNotePictureForImageHeader = Modifier.size(75.dp).padding(10.dp) val normalWithTopMarginNoteModifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding( start = 12.dp, end = 12.dp, @@ -225,7 +228,8 @@ val normalWithTopMarginNoteModifier = ) val boostedNoteModifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding( start = 0.dp, end = 0.dp, @@ -258,3 +262,5 @@ val incognitoIconModifier = .size(14.dp) val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp) + +val noteComposeRelayBox = Modifier.width(55.dp).heightIn(min = 17.dp).padding(start = 2.dp, end = 1.dp) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af3df202b..ebc060255 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -600,6 +600,12 @@ OK Failed to reach %1$s: %2$s + + Failed to assemble NIP-11 url for %1$s: %2$s + Failed to reach %1$s: %2$s + Failed to parse response from %1$s: %2$s + Relay declined requrest %1$s: %2$s + Failed to reach %1$s: %2$s Failed to parse result from %1$s: %2$s %1$s failed with code %2$s