1. Refactors the use of dividers out of components

2. Refactors composables to load events, check hidden and check report
This commit is contained in:
Vitor Pamplona
2024-03-20 13:47:47 -04:00
parent 943a4260ff
commit 3226e4e024
18 changed files with 309 additions and 492 deletions
@@ -47,7 +47,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@Composable @Composable
@@ -134,10 +133,5 @@ fun ChatHeaderLayout(
} }
} }
} }
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
} }
} }
@@ -64,7 +64,6 @@ fun BadgeCompose(
likeSetCard: BadgeCard, likeSetCard: BadgeCard,
isInnerNote: Boolean = false, isInnerNote: Boolean = false,
routeForLastRead: String, routeForLastRead: String,
showHidden: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -79,7 +78,7 @@ fun BadgeCompose(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if (note == null) { if (note == null) {
BlankNote(Modifier, !isInnerNote) BlankNote(Modifier)
} else { } else {
val backgroundColor = val backgroundColor =
calculateBackgroundColor( calculateBackgroundColor(
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -45,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@@ -60,7 +58,6 @@ fun BlankNotePreview() {
@Composable @Composable
fun BlankNote( fun BlankNote(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showDivider: Boolean = true,
idHex: String? = null, idHex: String? = null,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
@@ -84,13 +81,6 @@ fun BlankNote(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }
if (showDivider) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 10.dp),
thickness = DividerThickness,
)
}
} }
} }
} }
@@ -171,10 +161,6 @@ fun HiddenNote(
} }
} }
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
@@ -189,7 +175,6 @@ fun HiddenNoteByMePreview() {
@Composable @Composable
fun HiddenNoteByMe( fun HiddenNoteByMe(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isQuote: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
@@ -222,11 +207,5 @@ fun HiddenNoteByMe(
} }
} }
} }
if (!isQuote) {
HorizontalDivider(
thickness = DividerThickness,
)
}
} }
} }
@@ -0,0 +1,165 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun CheckHiddenFeedWatchBlockAndReport(
note: Note,
modifier: Modifier = Modifier,
showHiddenWarning: Boolean,
showHidden: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
if (showHidden) {
// Ignores reports as well
normalNote(true)
} else {
WatchBlockAndReport(note, showHiddenWarning, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun WatchBlockAndReport(
note: Note,
showHiddenWarning: Boolean,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by remember(note) {
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val showAnyway =
remember {
mutableStateOf(false)
}
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden) {
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe {
showAnyway.value = true
}
}
}
}
@Composable
private fun LoadReportsNoteCompose(
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var state by
remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
Crossfade(targetState = state, label = "LoadedNoteCompose") {
RenderReportState(state = it, note = note, modifier = modifier, accountViewModel = accountViewModel, nav = nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
private fun RenderReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var showReportedNote by remember(note) { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
normalNote(canPreview)
}
}
}
@Composable
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (AccountViewModel.NoteComposeReportState) -> Unit,
) {
val userFollowsState by accountViewModel.userFollows.observeAsState()
val noteReportsState by note.live().reports.observeAsState()
val userBlocks by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle()
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState, userBlocks) {
accountViewModel.isNoteAcceptable(note, onChange)
}
}
@@ -21,9 +21,7 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
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
@@ -36,7 +34,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -49,7 +46,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.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Alignment.Companion.TopEnd
@@ -81,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
@@ -106,7 +101,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ChannelCardCompose( fun ChannelCardCompose(
baseNote: Note, baseNote: Note,
@@ -114,160 +108,29 @@ fun ChannelCardCompose(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
forceEventKind: Int?, forceEventKind: Int?,
showHidden: Boolean = false, isHiddenFeed: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null) WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
Crossfade(targetState = hasEvent, label = "ChannelCardCompose") { CheckHiddenFeedWatchBlockAndReport(
if (it) { note = baseNote,
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) { modifier = modifier,
CheckHiddenChannelCardCompose( showHidden = isHiddenFeed,
baseNote, showHiddenWarning = false,
routeForLastRead, accountViewModel = accountViewModel,
modifier, nav = nav,
parentBackgroundColor, ) { canPreview ->
showHidden, NormalChannelCard(
accountViewModel, baseNote = baseNote,
nav, routeForLastRead = routeForLastRead,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
nav = nav,
) )
} }
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup,
->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
false,
)
}
}
}
}
@Composable
fun CheckHiddenChannelCardCompose(
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
showHidden: Boolean,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
if (showHidden) {
val state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
RenderChannelCardReportState(
state = state,
note = note,
routeForLastRead = routeForLastRead,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
} else {
val isHidden by
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
.observeAsState(accountViewModel.isNoteHidden(note))
Crossfade(targetState = isHidden, label = "CheckHiddenChannelCardCompose") {
if (!it) {
LoadedChannelCardCompose(
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
}
}
}
}
@Composable
fun LoadedChannelCardCompose(
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
val scope = rememberCoroutineScope()
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
scope.launch(Dispatchers.Main) { state = newState }
}
}
Crossfade(targetState = state, label = "CheckHiddenChannelCardCompose") {
RenderChannelCardReportState(
it,
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
}
}
@Composable
fun RenderChannelCardReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var showReportedNote by remember { mutableStateOf(false) }
Crossfade(
targetState = !state.isAcceptable && !showReportedNote,
label = "CheckHiddenChannelCardCompose",
) { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
NormalChannelCard(
note,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav,
)
} }
} }
} }
@@ -368,10 +231,6 @@ fun InnerCardRow(
) )
} }
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
@Composable @Composable
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -32,7 +31,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
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.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -61,9 +59,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -97,7 +94,6 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ChatroomMessageCompose( fun ChatroomMessageCompose(
baseNote: Note, baseNote: Note,
@@ -108,111 +104,13 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null) WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
WatchBlockAndReport(
Crossfade(targetState = hasEvent) { note = baseNote,
if (it) { showHiddenWarning = innerQuote,
CheckHiddenChatMessage( accountViewModel = accountViewModel,
baseNote, nav = nav,
routeForLastRead, ) { canPreview ->
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply,
)
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup,
->
BlankNote(
remember {
Modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
)
}
}
}
}
@Composable
fun CheckHiddenChatMessage(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
val isHidden by
remember {
accountViewModel.account.liveHiddenUsers
.map { baseNote.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(baseNote))
if (!isHidden) {
LoadedChatMessageCompose(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply,
)
}
}
@Composable
fun LoadedChatMessageCompose(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
var state by remember {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(baseNote, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by
remember(state, showReportedNote) {
derivedStateOf { !state.isAcceptable && !showReportedNote }
}
Crossfade(targetState = showHiddenNote) {
if (it) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
Modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview by
remember(state, showReportedNote) {
derivedStateOf { (!state.isAcceptable && showReportedNote) || state.canPreview }
}
NormalChatNote( NormalChatNote(
baseNote, baseNote,
routeForLastRead, routeForLastRead,
@@ -104,7 +104,7 @@ fun MessageSetCompose(
baseNote = baseNote, baseNote = baseNote,
routeForLastRead = null, routeForLastRead = null,
isBoostedNote = true, isBoostedNote = true,
showHidden = showHidden, isHiddenFeed = showHidden,
quotesLeft = 1, quotesLeft = 1,
parentBackgroundColor = backgroundColor, parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -145,7 +145,7 @@ fun MultiSetCompose(
routeForLastRead = null, routeForLastRead = null,
modifier = HalfTopPadding, modifier = HalfTopPadding,
isBoostedNote = true, isBoostedNote = true,
showHidden = showHidden, isHiddenFeed = showHidden,
quotesLeft = 1, quotesLeft = 1,
parentBackgroundColor = backgroundColor, parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@@ -42,7 +41,6 @@ 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.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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
@@ -51,7 +49,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@@ -176,7 +173,7 @@ fun NoteCompose(
isQuotedNote: Boolean = false, isQuotedNote: Boolean = false,
unPackReply: Boolean = true, unPackReply: Boolean = true,
makeItShort: Boolean = false, makeItShort: Boolean = false,
showHidden: Boolean = false, isHiddenFeed: Boolean = false,
quotesLeft: Int, quotesLeft: Int,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -185,13 +182,12 @@ fun NoteCompose(
WatchNoteEvent( WatchNoteEvent(
baseNote = baseNote, baseNote = baseNote,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showDivider = !isBoostedNote && !isQuotedNote,
modifier, modifier,
) { ) {
CheckHiddenNoteCompose( CheckHiddenFeedWatchBlockAndReport(
note = baseNote, note = baseNote,
modifier = modifier, modifier = modifier,
showHidden = showHidden, showHidden = isHiddenFeed,
showHiddenWarning = isQuotedNote || isBoostedNote, showHiddenWarning = isQuotedNote || isBoostedNote,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -214,171 +210,6 @@ fun NoteCompose(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WatchNoteEvent(
baseNote: Note,
accountViewModel: AccountViewModel,
showDivider: Boolean,
modifier: Modifier = Modifier,
onNoteEventFound: @Composable () -> Unit,
) {
if (baseNote.event != null) {
onNoteEventFound()
} else {
// avoid observing costs if already has an event.
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent, label = "Event presence") {
if (it) {
onNoteEventFound()
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
showDivider,
)
}
}
}
}
}
@Composable
fun CheckHiddenNoteCompose(
note: Note,
modifier: Modifier = Modifier,
showHiddenWarning: Boolean,
showHidden: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
if (showHidden) {
// Ignores reports as well
normalNote(true)
} else {
WatchIsHidden(note, showHiddenWarning, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun WatchIsHidden(
note: Note,
showHiddenWarning: Boolean,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
val isHiddenState by remember(note) {
accountViewModel.account.liveHiddenUsers
.map { note.isHiddenFor(it) }
.distinctUntilChanged()
}
.observeAsState(accountViewModel.isNoteHidden(note))
val showAnyway =
remember {
mutableStateOf(false)
}
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden) {
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
normalNote(canPreview)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe(
isQuote = true,
onClick = { showAnyway.value = true },
)
}
}
}
@Composable
fun LoadReportsNoteCompose(
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var state by
remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState(),
)
}
WatchForReports(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
Crossfade(targetState = state, label = "LoadedNoteCompose") {
RenderReportState(state = it, note = note, modifier = modifier, accountViewModel = accountViewModel, nav = nav) { canPreview ->
normalNote(canPreview)
}
}
}
@Composable
fun RenderReportState(
state: AccountViewModel.NoteComposeReportState,
note: Note,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
normalNote: @Composable (canPreview: Boolean) -> Unit,
) {
var showReportedNote by remember(note) { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showReportedNote = true },
)
} else {
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
normalNote(canPreview)
}
}
}
@Composable
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (AccountViewModel.NoteComposeReportState) -> Unit,
) {
val userFollowsState by accountViewModel.userFollows.observeAsState()
val noteReportsState by note.live().reports.observeAsState()
val userBlocks by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle()
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState, userBlocks) {
accountViewModel.isNoteAcceptable(note, onChange)
}
}
@Composable @Composable
fun AcceptableNote( fun AcceptableNote(
baseNote: Note, baseNote: Note,
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WatchNoteEvent(
baseNote: Note,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onNoteEventFound: @Composable () -> Unit,
) {
if (baseNote.event != null) {
onNoteEventFound()
} else {
// avoid observing costs if already has an event.
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent, label = "Event presence") {
if (it) {
onNoteEventFound()
} else {
LongPressToQuickAction(
baseNote = baseNote,
accountViewModel = accountViewModel,
) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = {},
onLongClick = showPopup,
)
},
)
}
}
}
}
}
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -54,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.WatchIsHiddenUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.WatchIsHiddenUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
@@ -77,11 +75,11 @@ fun ZapNoteCompose(
} }
} }
val route = remember(baseAuthor) { "User/${baseAuthor?.pubkeyHex}" }
if (baseAuthor == null) { if (baseAuthor == null) {
BlankNote() BlankNote()
} else { } else {
val route = remember(baseAuthor) { "User/${baseAuthor?.pubkeyHex}" }
Column( Column(
modifier = modifier =
Modifier.clickable( Modifier.clickable(
@@ -90,11 +88,6 @@ fun ZapNoteCompose(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
baseAuthor?.let { RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel) } baseAuthor?.let { RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel) }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }
} }
@@ -265,7 +265,6 @@ private fun RenderCardItem(
BadgeCompose( BadgeCompose(
item, item,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showHidden = showHidden,
nav = nav, nav = nav,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
) )
@@ -304,7 +303,7 @@ fun NoteCardCompose(
isQuotedNote = isQuotedNote, isQuotedNote = isQuotedNote,
unPackReply = unPackReply, unPackReply = unPackReply,
makeItShort = makeItShort, makeItShort = makeItShort,
showHidden = showHidden, isHiddenFeed = showHidden,
quotesLeft = 3, quotesLeft = 3,
parentBackgroundColor = parentBackgroundColor, parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -35,7 +36,9 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@Composable @Composable
@@ -109,6 +112,11 @@ private fun FeedLoaded(
nav = nav, nav = nav,
) )
} }
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
} }
} }
} }
@@ -228,7 +228,7 @@ private fun FeedLoaded(
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
modifier = Modifier, modifier = Modifier,
isBoostedNote = false, isBoostedNote = false,
showHidden = state.showHidden.value, isHiddenFeed = state.showHidden.value,
quotesLeft = 3, quotesLeft = 3,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -22,14 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable @Composable
@@ -72,6 +77,11 @@ private fun LnZapFeedLoaded(
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.zapEvent.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.zapEvent.idHex }) { _, item ->
ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav)
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }
} }
@@ -147,6 +147,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -288,11 +289,12 @@ fun ThreadFeedView(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
HorizontalDivider(
thickness = DividerThickness,
)
} }
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
} }
} }
} }
@@ -626,10 +628,6 @@ fun NoteMaster(
} }
ReactionsRow(note, true, editState, accountViewModel, nav) ReactionsRow(note, true, editState, accountViewModel, nav)
HorizontalDivider(
thickness = DividerThickness,
)
} }
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
@@ -37,6 +37,7 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
@@ -74,6 +75,7 @@ import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
@@ -352,6 +354,10 @@ private fun DiscoverFeedLoaded(
nav = nav, nav = nav,
) )
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }
@@ -384,6 +390,10 @@ private fun DiscoverFeedColumnsLoaded(
nav = nav, nav = nav,
) )
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }
@@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.findHashtags import com.vitorpamplona.quartz.events.findHashtags
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -401,6 +402,11 @@ private fun DisplaySearchResults(
loadProfilePicture = automaticallyShowProfilePicture, loadProfilePicture = automaticallyShowProfilePicture,
onClick = { nav("Channel/${item.idHex}") }, onClick = { nav("Channel/${item.idHex}") },
) )
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
} }
itemsIndexed( itemsIndexed(