- 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
This commit is contained in:
Vitor Pamplona
2024-06-13 16:50:19 -04:00
parent 6bc305dbe6
commit 2503cbaeac
11 changed files with 240 additions and 200 deletions
@@ -418,7 +418,7 @@ open class Note(
fun addRelay(relay: Relay) { fun addRelay(relay: Relay) {
if (relay.brief !in relays) { if (relay.brief !in relays) {
addRelaySync(relay.brief) addRelaySync(relay.brief)
liveSet?.innerRelays?.invalidateData() flowSet?.relays?.invalidateData()
} }
} }
@@ -847,12 +847,17 @@ class NoteFlowSet(
// Observers line up here. // Observers line up here.
val metadata = NoteBundledRefresherFlow(u) val metadata = NoteBundledRefresherFlow(u)
val reports = 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() { fun destroy() {
metadata.destroy() metadata.destroy()
reports.destroy() reports.destroy()
relays.destroy()
} }
} }
@@ -865,7 +870,6 @@ class NoteLiveSet(
val innerReactions = NoteBundledRefresherLiveData(u) val innerReactions = NoteBundledRefresherLiveData(u)
val innerBoosts = NoteBundledRefresherLiveData(u) val innerBoosts = NoteBundledRefresherLiveData(u)
val innerReplies = NoteBundledRefresherLiveData(u) val innerReplies = NoteBundledRefresherLiveData(u)
val innerRelays = NoteBundledRefresherLiveData(u)
val innerZaps = NoteBundledRefresherLiveData(u) val innerZaps = NoteBundledRefresherLiveData(u)
val innerOts = NoteBundledRefresherLiveData(u) val innerOts = NoteBundledRefresherLiveData(u)
val innerModifications = NoteBundledRefresherLiveData(u) val innerModifications = NoteBundledRefresherLiveData(u)
@@ -874,7 +878,6 @@ class NoteLiveSet(
val reactions = innerReactions.map { it } val reactions = innerReactions.map { it }
val boosts = innerBoosts.map { it } val boosts = innerBoosts.map { it }
val replies = innerReplies.map { it } val replies = innerReplies.map { it }
val relays = innerRelays.map { it }
val zaps = innerZaps.map { it } val zaps = innerZaps.map { it }
val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged() 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 boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
val relayInfo = innerRelays.map { it.note.relays }
val content = innerMetadata.map { it.note.event?.content() ?: "" } val content = innerMetadata.map { it.note.event?.content() ?: "" }
fun isInUse(): Boolean = fun isInUse(): Boolean =
@@ -909,7 +910,6 @@ class NoteLiveSet(
reactions.hasObservers() || reactions.hasObservers() ||
boosts.hasObservers() || boosts.hasObservers() ||
replies.hasObservers() || replies.hasObservers() ||
relays.hasObservers() ||
zaps.hasObservers() || zaps.hasObservers() ||
hasEvent.hasObservers() || hasEvent.hasObservers() ||
hasReactions.hasObservers() || hasReactions.hasObservers() ||
@@ -924,7 +924,6 @@ class NoteLiveSet(
innerReactions.destroy() innerReactions.destroy()
innerBoosts.destroy() innerBoosts.destroy()
innerReplies.destroy() innerReplies.destroy()
innerRelays.destroy()
innerZaps.destroy() innerZaps.destroy()
innerOts.destroy() innerOts.destroy()
innerModifications.destroy() innerModifications.destroy()
@@ -936,6 +935,7 @@ class NoteBundledRefresherFlow(
val note: Note, val note: Note,
) { ) {
// Refreshes observers in batches. // Refreshes observers in batches.
// TODO: Replace the bundler for .sample
private val bundler = BundledUpdate(500, Dispatchers.IO) private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(NoteState(note)) val stateFlow = MutableStateFlow(NoteState(note))
@@ -1015,11 +1015,12 @@ object RelayBriefInfoCache {
val cache = LruCache<String, RelayBriefInfo?>(50) val cache = LruCache<String, RelayBriefInfo?>(50)
@Immutable @Immutable
data class RelayBriefInfo( class RelayBriefInfo(
val url: String, 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 { fun get(url: String): RelayBriefInfo {
val info = cache[url] val info = cache[url]
@@ -87,13 +87,11 @@ fun RobohashFallbackAsyncImage(
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
loadProfilePicture: Boolean, loadProfilePicture: Boolean,
) { ) {
val context = LocalContext.current
val isLightTheme = MaterialTheme.colorScheme.isLight
if (model != null && loadProfilePicture) { if (model != null && loadProfilePicture) {
val isBase64 by remember { derivedStateOf { model.startsWith("data:image/jpeg;base64,") } } val isBase64 by remember { derivedStateOf { model.startsWith("data:image/jpeg;base64,") } }
if (isBase64) { if (isBase64) {
val context = LocalContext.current
val base64Painter = val base64Painter =
rememberAsyncImagePainter( rememberAsyncImagePainter(
model = Base64Requester.imageRequest(context, model), model = Base64Requester.imageRequest(context, model),
@@ -110,7 +108,7 @@ fun RobohashFallbackAsyncImage(
} else { } else {
val painter = val painter =
rememberVectorPainter( rememberVectorPainter(
image = CachedRobohash.get(robot, isLightTheme), image = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight),
) )
AsyncImage( AsyncImage(
@@ -128,7 +126,7 @@ fun RobohashFallbackAsyncImage(
) )
} }
} else { } else {
val robotPainter = CachedRobohash.get(robot, isLightTheme) val robotPainter = CachedRobohash.get(robot, MaterialTheme.colorScheme.isLight)
Image( Image(
imageVector = robotPainter, imageVector = robotPainter,
@@ -145,9 +143,12 @@ object Base64Requester {
fun imageRequest( fun imageRequest(
context: Context, context: Context,
message: String, message: String,
): ImageRequest { ): ImageRequest =
return ImageRequest.Builder(context).data(message).fetcherFactory(Base64Fetcher.Factory).build() ImageRequest
} .Builder(context)
.data(message)
.fetcherFactory(Base64Fetcher.Factory)
.build()
} }
@Stable @Stable
@@ -179,8 +180,6 @@ class Base64Fetcher(
data: Uri, data: Uri,
options: Options, options: Options,
imageLoader: ImageLoader, imageLoader: ImageLoader,
): Fetcher { ): Fetcher = Base64Fetcher(options, data)
return Base64Fetcher(options, data)
}
} }
} }
@@ -190,10 +190,9 @@ fun NormalChatNote(
nav("User/${it.pubkeyHex}") nav("User/${it.pubkeyHex}")
} }
}, },
actionMenu = { popupExpanded, onDismiss -> actionMenu = { onDismiss ->
NoteQuickActionMenu( NoteQuickActionMenu(
note = note, note = note,
popupExpanded = popupExpanded,
onDismiss = onDismiss, onDismiss = onDismiss,
onWantsToEditDraft = { onWantsToEditDraft(note) }, onWantsToEditDraft = { onWantsToEditDraft(note) },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -256,7 +255,7 @@ fun ChatBubbleLayout(
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
onClick: () -> Boolean, onClick: () -> Boolean,
onAuthorClick: () -> Unit, onAuthorClick: () -> Unit,
actionMenu: @Composable (popupExpanded: Boolean, onDismiss: () -> Unit) -> Unit, actionMenu: @Composable (onDismiss: () -> Unit) -> Unit,
detailRow: @Composable () -> Unit, detailRow: @Composable () -> Unit,
drawAuthorLine: @Composable () -> Unit, drawAuthorLine: @Composable () -> Unit,
inner: @Composable (MutableState<Color>) -> Unit, inner: @Composable (MutableState<Color>) -> Unit,
@@ -294,7 +293,7 @@ fun ChatBubbleLayout(
modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier,
horizontalArrangement = alignment, horizontalArrangement = alignment,
) { ) {
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded = remember { mutableStateOf(false) }
val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier 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) { if (popupExpanded.value) {
popupExpanded = false actionMenu {
popupExpanded.value = false
}
} }
} }
} }
@@ -381,7 +382,7 @@ private fun BubblePreview() {
parentBackgroundColor = backgroundBubbleColor, parentBackgroundColor = backgroundBubbleColor,
onClick = { false }, onClick = { false },
onAuthorClick = {}, onAuthorClick = {},
actionMenu = { popupExpanded, onDismiss -> actionMenu = { onDismiss ->
}, },
drawAuthorLine = { drawAuthorLine = {
UserDisplayNameLayout( UserDisplayNameLayout(
@@ -411,7 +412,7 @@ private fun BubblePreview() {
parentBackgroundColor = backgroundBubbleColor, parentBackgroundColor = backgroundBubbleColor,
onClick = { false }, onClick = { false },
onAuthorClick = {}, onAuthorClick = {},
actionMenu = { popupExpanded, onDismiss -> actionMenu = { onDismiss ->
}, },
drawAuthorLine = { drawAuthorLine = {
UserDisplayNameLayout( UserDisplayNameLayout(
@@ -40,7 +40,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier 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.HalfEndPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding 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.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp 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.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.coroutines.launch
@Composable @Composable
fun NoteCompose( fun NoteCompose(
@@ -416,22 +415,18 @@ fun ClickableNote(
nav: (String) -> Unit, nav: (String) -> Unit,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val scope = rememberCoroutineScope()
val updatedModifier = val updatedModifier =
remember(baseNote, backgroundColor.value) { remember(baseNote, backgroundColor.value) {
modifier modifier
.combinedClickable( .combinedClickable(
onClick = { onClick = {
scope.launch { val redirectToNote =
val redirectToNote = if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { baseNote.replyTo?.lastOrNull() ?: baseNote
baseNote.replyTo?.lastOrNull() ?: baseNote } else {
} else { baseNote
baseNote }
} routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
}
}, },
onLongClick = showPopup, onLongClick = showPopup,
).background(backgroundColor.value) ).background(backgroundColor.value)
@@ -465,9 +460,7 @@ fun InnerNoteWithReactions(
}, },
) { ) {
if (notBoostedNorQuote) { if (notBoostedNorQuote) {
Column(WidthAuthorPictureModifier) { AuthorAndRelayInformation(baseNote, accountViewModel, nav)
AuthorAndRelayInformation(baseNote, accountViewModel, nav)
}
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
} }
@@ -1064,12 +1057,14 @@ private fun AuthorAndRelayInformation(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
// Draws the boosted picture outside the boosted card. Column(WidthAuthorPictureModifier, verticalArrangement = RowColSpacing5dp) {
Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) { // Draws the boosted picture outside the boosted card.
RenderAuthorImages(baseNote, nav, accountViewModel) Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) {
} RenderAuthorImages(baseNote, nav, accountViewModel)
}
BadgeBox(baseNote, accountViewModel, nav) BadgeBox(baseNote, accountViewModel, nav)
}
} }
@Composable @Composable
@@ -137,23 +137,22 @@ fun LongPressToQuickAction(
content: @Composable (() -> Unit) -> Unit, content: @Composable (() -> Unit) -> Unit,
) { ) {
val popupExpanded = remember { mutableStateOf(false) } val popupExpanded = remember { mutableStateOf(false) }
val showPopup = remember { { popupExpanded.value = true } }
content(showPopup) content { popupExpanded.value = true }
NoteQuickActionMenu( if (popupExpanded.value) {
note = baseNote, NoteQuickActionMenu(
popupExpanded = popupExpanded.value, note = baseNote,
onDismiss = { popupExpanded.value = false }, onDismiss = { popupExpanded.value = false },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = {}, nav = {},
) )
}
} }
@Composable @Composable
fun NoteQuickActionMenu( fun NoteQuickActionMenu(
note: Note, note: Note,
popupExpanded: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -173,7 +172,6 @@ fun NoteQuickActionMenu(
NoteQuickActionMenu( NoteQuickActionMenu(
note = note, note = note,
popupExpanded = popupExpanded,
onDismiss = onDismiss, onDismiss = onDismiss,
onWantsToEditDraft = { editDraftDialog.value = true }, onWantsToEditDraft = { editDraftDialog.value = true },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -184,7 +182,6 @@ fun NoteQuickActionMenu(
@Composable @Composable
fun NoteQuickActionMenu( fun NoteQuickActionMenu(
note: Note, note: Note,
popupExpanded: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onWantsToEditDraft: () -> Unit, onWantsToEditDraft: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -195,17 +192,15 @@ fun NoteQuickActionMenu(
val showBlockAlertDialog = remember { mutableStateOf(false) } val showBlockAlertDialog = remember { mutableStateOf(false) }
val showReportDialog = remember { mutableStateOf(false) } val showReportDialog = remember { mutableStateOf(false) }
if (popupExpanded) { RenderMainPopup(
RenderMainPopup( accountViewModel,
accountViewModel, note,
note, onDismiss,
onDismiss, showBlockAlertDialog,
showBlockAlertDialog, showDeleteAlertDialog,
showDeleteAlertDialog, showReportDialog,
showReportDialog, onWantsToEditDraft,
onWantsToEditDraft, )
)
}
if (showSelectTextDialog.value) { if (showSelectTextDialog.value) {
val decryptedNote = remember { mutableStateOf<String?>(null) } val decryptedNote = remember { mutableStateOf<String?>(null) }
@@ -267,12 +262,12 @@ private fun RenderMainPopup(
val showToast = { stringResource: Int -> val showToast = { stringResource: Int ->
scope.launch { scope.launch {
Toast.makeText( Toast
context, .makeText(
context.getString(stringResource), context,
Toast.LENGTH_SHORT, context.getString(stringResource),
) Toast.LENGTH_SHORT,
.show() ).show()
} }
} }
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -40,10 +41,8 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer 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.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.noteComposeRelayBox
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun RelayBadges( fun RelayBadges(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
var expanded by remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
val relayList by baseNote.live().relayInfo.observeAsState(baseNote.relays) Crossfade(expanded.value, modifier = noteComposeRelayBox, label = "RelayBadges") {
if (it) {
Spacer(StdVertSpacer) RenderAllRelayList(baseNote, Modifier.fillMaxWidth(), accountViewModel = accountViewModel, nav = nav)
} else {
// FlowRow Seems to be a lot faster than LazyVerticalGrid Column {
Box(modifier = Modifier.fillMaxWidth().padding(start = 2.dp, end = 1.dp)) { RenderClosedRelayList(baseNote, Modifier.fillMaxWidth(), accountViewModel = accountViewModel, nav = nav)
FlowRow(modifier = Modifier.fillMaxWidth()) { ShouldShowExpandButton(baseNote, accountViewModel) { ShowMoreRelaysButton { expanded.value = true } }
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) }
} }
} }
} }
}
if (relayList.size > 3 && !expanded) { @OptIn(ExperimentalLayoutApi::class)
ShowMoreRelaysButton { expanded = true } @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) Spacer(StdVertSpacer)
// FlowRow Seems to be a lot faster than LazyVerticalGrid // 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 { FlowRow {
Box( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Box( Box(
Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black), Modifier
.size(13.dp)
.clip(shape = CircleShape)
.background(Color.Black),
) )
} }
Box( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, 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( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, 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( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, 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( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, 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( Box(
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
contentAlignment = Alignment.Center, 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),
)
} }
} }
} }
@@ -21,32 +21,24 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
@@ -70,42 +62,25 @@ public fun RelayBadgesHorizontal(
) { ) {
val expanded = remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
RenderRelayList(baseNote, expanded, accountViewModel, nav) if (expanded.value) {
RenderAllRelayList(baseNote, StdStartPadding, verticalArrangement = Arrangement.Center, accountViewModel, nav)
RenderExpandButton(baseNote, expanded) { ChatRelayExpandButton { expanded.value = true } } } else {
} RenderClosedRelayList(baseNote, StdStartPadding, verticalAlignment = Alignment.CenterVertically, accountViewModel = accountViewModel, nav = nav)
ShouldShowExpandButton(baseNote, accountViewModel) {
@OptIn(ExperimentalLayoutApi::class) ChatRelayExpandButton { expanded.value = true }
@Composable
fun RenderRelayList(
baseNote: Note,
expanded: MutableState<Boolean>,
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) }
} }
} }
} }
@Composable @Composable
fun RenderExpandButton( fun ShouldShowExpandButton(
baseNote: Note, baseNote: Note,
expanded: MutableState<Boolean>, accountViewModel: AccountViewModel,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val showExpandButton by val showExpandButton by accountViewModel.createMustShowExpandButtonFlows(baseNote).collectAsStateWithLifecycle()
baseNote.live().relays.map { it.note.relays.size > 3 }.observeAsState(baseNote.relays.size > 3)
if (showExpandButton && !expanded.value) { if (showExpandButton) {
content() content()
} }
} }
@@ -159,19 +134,11 @@ fun RenderRelay(
) )
} }
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size17dp)
val clickableModifier = val clickableModifier =
remember(relay) { remember(relay) {
Modifier Modifier
.size(Size17dp) .size(Size17dp)
.clickable( .clickable(
role = Role.Button,
interactionSource = interactionSource,
indication = ripple,
onClick = { onClick = {
accountViewModel.retrieveRelayDocument( accountViewModel.retrieveRelayDocument(
relay.url, relay.url,
@@ -179,40 +146,23 @@ fun RenderRelay(
openRelayDialog = true openRelayDialog = true
}, },
onError = { url, errorCode, exceptionMessage -> onError = { url, errorCode, exceptionMessage ->
val msg = accountViewModel.toast(
R.string.unable_to_download_relay_document,
when (errorCode) { when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString( R.string.relay_information_document_error_failed_to_assemble_url
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString( R.string.relay_information_document_error_failed_to_reach_server
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString( R.string.relay_information_document_error_failed_to_parse_response
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString( R.string.relay_information_document_error_failed_with_http
R.string.relay_information_document_error_assemble_url, },
url, url,
exceptionMessage, exceptionMessage ?: errorCode.toString(),
)
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg,
) )
}, },
) )
@@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -322,6 +323,23 @@ class AccountViewModel(
noteIsHiddenFlows.put(note, it) noteIsHiddenFlows.put(note, it)
} }
val noteMustShowExpandButtonFlows = LruCache<Note, StateFlow<Boolean>>(300)
fun createMustShowExpandButtonFlows(note: Note): StateFlow<Boolean> =
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( fun hasReactedTo(
baseNote: Note, baseNote: Note,
reaction: String, reaction: String,
@@ -26,8 +26,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -49,7 +47,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.LikeReaction
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay 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.ReplyReaction
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction
@@ -312,7 +309,9 @@ private fun RenderAuthorInformation(
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
Column( Column(
Modifier.height(65.dp).weight(1f), Modifier
.height(65.dp)
.weight(1f),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -332,7 +331,7 @@ private fun RenderAuthorInformation(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 2.dp), 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 @Composable
fun ReactionsColumn( fun ReactionsColumn(
baseNote: Note, baseNote: Note,
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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 ShowMoreRelaysButtonIconButtonModifier = Modifier.size(15.dp)
val ShowMoreRelaysButtonIconModifier = Modifier.size(20.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) 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 ChatPaddingInnerQuoteModifier = Modifier.padding(top = 10.dp)
val ChatPaddingModifier = val ChatPaddingModifier =
Modifier.fillMaxWidth(1f) Modifier
.fillMaxWidth(1f)
.padding( .padding(
start = 12.dp, start = 12.dp,
end = 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 authorNotePictureForImageHeader = Modifier.size(75.dp).padding(10.dp)
val normalWithTopMarginNoteModifier = val normalWithTopMarginNoteModifier =
Modifier.fillMaxWidth() Modifier
.fillMaxWidth()
.padding( .padding(
start = 12.dp, start = 12.dp,
end = 12.dp, end = 12.dp,
@@ -225,7 +228,8 @@ val normalWithTopMarginNoteModifier =
) )
val boostedNoteModifier = val boostedNoteModifier =
Modifier.fillMaxWidth() Modifier
.fillMaxWidth()
.padding( .padding(
start = 0.dp, start = 0.dp,
end = 0.dp, end = 0.dp,
@@ -258,3 +262,5 @@ val incognitoIconModifier =
.size(14.dp) .size(14.dp)
val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.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)
+6
View File
@@ -600,6 +600,12 @@
<string name="error_dialog_button_ok">OK</string> <string name="error_dialog_button_ok">OK</string>
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string> <string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
<string name="relay_information_document_error_failed_with_http">Relay declined requrest %1$s: %2$s</string>
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string> <string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string> <string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string> <string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>