Improves padding of the ListItem interfaces

This commit is contained in:
Vitor Pamplona
2025-11-06 14:15:59 -05:00
parent 34aca458ed
commit 6727bd0430
7 changed files with 919 additions and 141 deletions

View File

@@ -39,6 +39,7 @@ import androidx.compose.ui.layout.ContentScale
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 com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble
@@ -47,11 +48,13 @@ 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.ThemeComparisonColumn
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@Composable @Composable
@Preview @Preview
fun ChannelNamePreview() { fun ChannelNamePreview() {
ThemeComparisonColumn {
Column { Column {
ChatHeaderLayout( ChatHeaderLayout(
channelPicture = { channelPicture = {
@@ -67,6 +70,7 @@ fun ChannelNamePreview() {
}, },
secondRow = { secondRow = {
Text("This is a message from this person", Modifier.weight(1f)) Text("This is a message from this person", Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble() NewItemsBubble()
}, },
onClick = {}, onClick = {},
@@ -98,6 +102,32 @@ fun ChannelNamePreview() {
) )
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
SlimListItem(
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is my author", Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is a message from this person", Modifier.weight(1f))
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
}
} }
} }

View File

@@ -0,0 +1,703 @@
/**
* Copyright (c) 2025 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.layouts.listItem
import android.view.Surface
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.ui.layouts.listItem.ListTokens.ListItemContainerElevation
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.math.max
/**
* This is a copy of Material3's ListItemLayout.kt file, with the only change being the padding change from 16.dp to 10.dp
*/
@Composable
@Preview
fun ChannelNamePreview() {
ThemeComparisonColumn {
Column {
ChatHeaderLayout(
channelPicture = {
Image(
painter = painterRes(R.drawable.github, 1),
contentDescription = stringRes(id = R.string.profile_banner),
contentScale = ContentScale.FillWidth,
)
},
firstRow = {
Text("This is my author", Modifier.weight(1f))
TimeAgo(TimeUtils.now())
},
secondRow = {
Text("This is a message from this person", Modifier.weight(1f))
NewItemsBubble()
},
onClick = {},
)
HorizontalDivider(thickness = DividerThickness)
SlimListItem(
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is my author", Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is a message from this person", Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
ListItem(
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is my author", Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("This is a message from this person", Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
}
}
}
@Composable
fun SlimListItem(
headlineContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
overlineContent: @Composable (() -> Unit)? = null,
supportingContent: @Composable (() -> Unit)? = null,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
colors: ListItemColors = ListItemDefaults.colors(),
tonalElevation: Dp = ListItemContainerElevation,
shadowElevation: Dp = ListItemContainerElevation,
) {
val decoratedHeadlineContent: @Composable () -> Unit = {
ProvideTextStyleFromToken(
colors.headlineColor,
MaterialTheme.typography.bodyLarge,
headlineContent,
)
}
val decoratedSupportingContent: @Composable (() -> Unit)? =
supportingContent?.let {
{
Box(SupportingContentTopPadding) {
ProvideTextStyleFromToken(
colors.supportingTextColor,
MaterialTheme.typography.bodyMedium,
it,
)
}
}
}
val decoratedOverlineContent: @Composable (() -> Unit)? =
overlineContent?.let {
{
ProvideTextStyleFromToken(
colors.overlineColor,
MaterialTheme.typography.labelSmall,
it,
)
}
}
val decoratedLeadingContent: @Composable (() -> Unit)? =
leadingContent?.let {
{
Box(LeadingContentEndPadding) {
CompositionLocalProvider(
LocalContentColor provides colors.leadingIconColor,
content = it,
)
}
}
}
val decoratedTrailingContent: @Composable (() -> Unit)? =
trailingContent?.let {
{
Box(TrailingContentStartPadding) {
ProvideTextStyleFromToken(
colors.trailingIconColor,
MaterialTheme.typography.labelSmall,
content = it,
)
}
}
}
Surface(
modifier = Modifier.semantics(mergeDescendants = true) {}.then(modifier),
shape = ListItemDefaults.shape,
color = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
) {
SlimListItemLayout(
headline = decoratedHeadlineContent,
overline = decoratedOverlineContent,
supporting = decoratedSupportingContent,
leading = decoratedLeadingContent,
trailing = decoratedTrailingContent,
)
}
}
@Composable
private fun SlimListItemLayout(
leading: @Composable (() -> Unit)?,
trailing: @Composable (() -> Unit)?,
headline: @Composable () -> Unit,
overline: @Composable (() -> Unit)?,
supporting: @Composable (() -> Unit)?,
) {
val measurePolicy = remember { ListItemMeasurePolicy() }
Layout(
contents =
listOf(headline, overline ?: {}, supporting ?: {}, leading ?: {}, trailing ?: {}),
measurePolicy = measurePolicy,
)
}
private class ListItemMeasurePolicy : MultiContentMeasurePolicy {
override fun MeasureScope.measure(
measurables: List<List<Measurable>>,
constraints: Constraints,
): MeasureResult {
val (
headlineMeasurable,
overlineMeasurable,
supportingMeasurable,
leadingMeasurable,
trailingMeasurable,
) =
measurables
var currentTotalWidth = 0
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val startPadding = ListItemStartPadding
val endPadding = ListItemEndPadding
val horizontalPadding = (startPadding + endPadding).roundToPx()
// ListItem layout has a cycle in its dependencies which we use
// intrinsic measurements to break:
// 1. Intrinsic leading/trailing width
// 2. Intrinsic supporting height
// 3. Intrinsic vertical padding
// 4. Actual leading/trailing measurement
// 5. Actual supporting measurement
// 6. Actual vertical padding
val intrinsicLeadingWidth =
leadingMeasurable.firstOrNull()?.minIntrinsicWidth(constraints.maxHeight) ?: 0
val intrinsicTrailingWidth =
trailingMeasurable.firstOrNull()?.minIntrinsicWidth(constraints.maxHeight) ?: 0
val intrinsicSupportingWidthConstraint =
looseConstraints.maxWidth.subtractConstraintSafely(
intrinsicLeadingWidth + intrinsicTrailingWidth + horizontalPadding,
)
val intrinsicSupportingHeight =
supportingMeasurable
.firstOrNull()
?.minIntrinsicHeight(intrinsicSupportingWidthConstraint) ?: 0
val intrinsicIsSupportingMultiline =
isSupportingMultilineHeuristic(intrinsicSupportingHeight)
val intrinsicListItemType =
ListItemType(
hasOverline = overlineMeasurable.firstOrNull() != null,
hasSupporting = supportingMeasurable.firstOrNull() != null,
isSupportingMultiline = intrinsicIsSupportingMultiline,
)
val intrinsicVerticalPadding = (verticalPadding(intrinsicListItemType) * 2).roundToPx()
val paddedLooseConstraints =
looseConstraints.offset(
horizontal = -horizontalPadding,
vertical = -intrinsicVerticalPadding,
)
val leadingPlaceable = leadingMeasurable.firstOrNull()?.measure(paddedLooseConstraints)
currentTotalWidth += leadingPlaceable.widthOrZero
val trailingPlaceable =
trailingMeasurable
.firstOrNull()
?.measure(paddedLooseConstraints.offset(horizontal = -currentTotalWidth))
currentTotalWidth += trailingPlaceable.widthOrZero
var currentTotalHeight = 0
val headlinePlaceable =
headlineMeasurable
.firstOrNull()
?.measure(paddedLooseConstraints.offset(horizontal = -currentTotalWidth))
currentTotalHeight += headlinePlaceable.heightOrZero
val supportingPlaceable =
supportingMeasurable
.firstOrNull()
?.measure(
paddedLooseConstraints.offset(
horizontal = -currentTotalWidth,
vertical = -currentTotalHeight,
),
)
currentTotalHeight += supportingPlaceable.heightOrZero
val isSupportingMultiline =
supportingPlaceable != null &&
(supportingPlaceable[FirstBaseline] != supportingPlaceable[LastBaseline])
val overlinePlaceable =
overlineMeasurable
.firstOrNull()
?.measure(
paddedLooseConstraints.offset(
horizontal = -currentTotalWidth,
vertical = -currentTotalHeight,
),
)
val listItemType =
ListItemType(
hasOverline = overlinePlaceable != null,
hasSupporting = supportingPlaceable != null,
isSupportingMultiline = isSupportingMultiline,
)
val topPadding = verticalPadding(listItemType)
val verticalPadding = topPadding * 2
val width =
calculateWidth(
leadingWidth = leadingPlaceable.widthOrZero,
trailingWidth = trailingPlaceable.widthOrZero,
headlineWidth = headlinePlaceable.widthOrZero,
overlineWidth = overlinePlaceable.widthOrZero,
supportingWidth = supportingPlaceable.widthOrZero,
horizontalPadding = horizontalPadding,
constraints = constraints,
)
val height =
calculateHeight(
leadingHeight = leadingPlaceable.heightOrZero,
trailingHeight = trailingPlaceable.heightOrZero,
headlineHeight = headlinePlaceable.heightOrZero,
overlineHeight = overlinePlaceable.heightOrZero,
supportingHeight = supportingPlaceable.heightOrZero,
listItemType = listItemType,
verticalPadding = verticalPadding.roundToPx(),
constraints = constraints,
)
return place(
width = width,
height = height,
leadingPlaceable = leadingPlaceable,
trailingPlaceable = trailingPlaceable,
headlinePlaceable = headlinePlaceable,
overlinePlaceable = overlinePlaceable,
supportingPlaceable = supportingPlaceable,
isThreeLine = listItemType == ListItemType.ThreeLine,
startPadding = startPadding.roundToPx(),
endPadding = endPadding.roundToPx(),
topPadding = topPadding.roundToPx(),
)
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<List<IntrinsicMeasurable>>,
width: Int,
): Int = calculateIntrinsicHeight(measurables, width, IntrinsicMeasurable::maxIntrinsicHeight)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int,
): Int = calculateIntrinsicWidth(measurables, height, IntrinsicMeasurable::maxIntrinsicWidth)
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<List<IntrinsicMeasurable>>,
width: Int,
): Int = calculateIntrinsicHeight(measurables, width, IntrinsicMeasurable::minIntrinsicHeight)
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int,
): Int = calculateIntrinsicWidth(measurables, height, IntrinsicMeasurable::minIntrinsicWidth)
private fun IntrinsicMeasureScope.calculateIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int,
intrinsicMeasure: IntrinsicMeasurable.(height: Int) -> Int,
): Int {
val (
headlineMeasurable,
overlineMeasurable,
supportingMeasurable,
leadingMeasurable,
trailingMeasurable,
) =
measurables
return calculateWidth(
leadingWidth = leadingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0,
trailingWidth = trailingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0,
headlineWidth = headlineMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0,
overlineWidth = overlineMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0,
supportingWidth = supportingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0,
horizontalPadding = (ListItemStartPadding + ListItemEndPadding).roundToPx(),
constraints = Constraints(),
)
}
private fun IntrinsicMeasureScope.calculateIntrinsicHeight(
measurables: List<List<IntrinsicMeasurable>>,
width: Int,
intrinsicMeasure: IntrinsicMeasurable.(width: Int) -> Int,
): Int {
val (
headlineMeasurable,
overlineMeasurable,
supportingMeasurable,
leadingMeasurable,
trailingMeasurable,
) =
measurables
var remainingWidth =
width.subtractConstraintSafely((ListItemStartPadding + ListItemEndPadding).roundToPx())
val leadingHeight =
leadingMeasurable.firstOrNull()?.let {
val height = it.intrinsicMeasure(remainingWidth)
remainingWidth =
remainingWidth.subtractConstraintSafely(
it.maxIntrinsicWidth(Constraints.Infinity),
)
height
} ?: 0
val trailingHeight =
trailingMeasurable.firstOrNull()?.let {
val height = it.intrinsicMeasure(remainingWidth)
remainingWidth =
remainingWidth.subtractConstraintSafely(
it.maxIntrinsicWidth(Constraints.Infinity),
)
height
} ?: 0
val overlineHeight = overlineMeasurable.firstOrNull()?.intrinsicMeasure(remainingWidth) ?: 0
val headlineHeight = headlineMeasurable.firstOrNull()?.intrinsicMeasure(remainingWidth) ?: 0
val supportingHeight =
supportingMeasurable.firstOrNull()?.intrinsicMeasure(remainingWidth) ?: 0
val isSupportingMultiline = isSupportingMultilineHeuristic(supportingHeight)
val listItemType =
ListItemType(
hasOverline = overlineHeight > 0,
hasSupporting = supportingHeight > 0,
isSupportingMultiline = isSupportingMultiline,
)
return calculateHeight(
leadingHeight = leadingHeight,
trailingHeight = trailingHeight,
headlineHeight = headlineHeight,
overlineHeight = overlineHeight,
supportingHeight = supportingHeight,
listItemType = listItemType,
verticalPadding = (verticalPadding(listItemType) * 2).roundToPx(),
constraints = Constraints(),
)
}
}
private fun IntrinsicMeasureScope.calculateWidth(
leadingWidth: Int,
trailingWidth: Int,
headlineWidth: Int,
overlineWidth: Int,
supportingWidth: Int,
horizontalPadding: Int,
constraints: Constraints,
): Int {
if (constraints.hasBoundedWidth) {
return constraints.maxWidth
}
// Fallback behavior if width constraints are infinite
val mainContentWidth = maxOf(headlineWidth, overlineWidth, supportingWidth)
return horizontalPadding + leadingWidth + mainContentWidth + trailingWidth
}
private fun IntrinsicMeasureScope.calculateHeight(
leadingHeight: Int,
trailingHeight: Int,
headlineHeight: Int,
overlineHeight: Int,
supportingHeight: Int,
listItemType: ListItemType,
verticalPadding: Int,
constraints: Constraints,
): Int {
val defaultMinHeight =
when (listItemType) {
ListItemType.OneLine -> ListTokens.ListItemOneLineContainerHeight
ListItemType.TwoLine -> ListTokens.ListItemTwoLineContainerHeight
else -> ListTokens.ListItemThreeLineContainerHeight
}
val minHeight = max(constraints.minHeight, defaultMinHeight.roundToPx())
val mainContentHeight = headlineHeight + overlineHeight + supportingHeight
return max(minHeight, verticalPadding + maxOf(leadingHeight, mainContentHeight, trailingHeight))
.coerceAtMost(constraints.maxHeight)
}
private fun MeasureScope.place(
width: Int,
height: Int,
leadingPlaceable: Placeable?,
trailingPlaceable: Placeable?,
headlinePlaceable: Placeable?,
overlinePlaceable: Placeable?,
supportingPlaceable: Placeable?,
isThreeLine: Boolean,
startPadding: Int,
endPadding: Int,
topPadding: Int,
): MeasureResult =
layout(width, height) {
leadingPlaceable?.let {
it.placeRelative(
x = startPadding,
y = if (isThreeLine) topPadding else CenterVertically.align(it.height, height),
)
}
val mainContentX = startPadding + leadingPlaceable.widthOrZero
val mainContentY =
if (isThreeLine) {
topPadding
} else {
val totalHeight =
headlinePlaceable.heightOrZero +
overlinePlaceable.heightOrZero +
supportingPlaceable.heightOrZero
CenterVertically.align(totalHeight, height)
}
var currentY = mainContentY
overlinePlaceable?.placeRelative(mainContentX, currentY)
currentY += overlinePlaceable.heightOrZero
headlinePlaceable?.placeRelative(mainContentX, currentY)
currentY += headlinePlaceable.heightOrZero
supportingPlaceable?.placeRelative(mainContentX, currentY)
trailingPlaceable?.let {
it.placeRelative(
x = width - endPadding - it.width,
y = if (isThreeLine) topPadding else CenterVertically.align(it.height, height),
)
}
}
internal val Placeable?.widthOrZero: Int
get() = this?.width ?: 0
internal val Placeable?.heightOrZero: Int
get() = this?.height ?: 0
/**
* Subtracts one value from another, where both values represent constraints used in layout.
*
* Notably:
* - if [this] is [Constraints.Infinity], the result stays [Constraints.Infinity]
* - the result is coerced to be non-negative
*/
internal fun Int.subtractConstraintSafely(other: Int): Int {
if (this == Constraints.Infinity) {
return this
}
return (this - other).coerceAtLeast(0)
}
// In the actual layout phase, we can query supporting baselines,
// but for an intrinsic measurement pass, we have to estimate.
private fun Density.isSupportingMultilineHeuristic(estimatedSupportingHeight: Int): Boolean = estimatedSupportingHeight > 30.sp.roundToPx()
private fun verticalPadding(listItemType: ListItemType): Dp =
when (listItemType) {
ListItemType.ThreeLine -> ListItemThreeLineVerticalPadding
else -> ListItemVerticalPadding
}
@JvmInline
private value class ListItemType private constructor(
private val lines: Int,
) : Comparable<ListItemType> {
override operator fun compareTo(other: ListItemType) = lines.compareTo(other.lines)
companion object {
/** One line list item */
val OneLine = ListItemType(1)
/** Two line list item */
val TwoLine = ListItemType(2)
/** Three line list item */
val ThreeLine = ListItemType(3)
internal operator fun invoke(
hasOverline: Boolean,
hasSupporting: Boolean,
isSupportingMultiline: Boolean,
): ListItemType =
when {
(hasOverline && hasSupporting) || isSupportingMultiline -> ThreeLine
hasOverline || hasSupporting -> TwoLine
else -> OneLine
}
}
}
@Composable
internal fun ProvideTextStyleFromToken(
contentColor: Color,
textStyle: TextStyle,
content: @Composable () -> Unit,
) {
val mergedStyle = LocalTextStyle.current.merge(textStyle)
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides mergedStyle,
content = content,
)
}
object ListTokens {
val ListItemContainerElevation = 0.0.dp
val ListItemOneLineContainerHeight = 56.0.dp
val ListItemThreeLineContainerHeight = 88.0.dp
val ListItemTwoLineContainerHeight = 72.0.dp
}
// Container related defaults
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val ListItemVerticalPadding = 8.dp
@VisibleForTesting internal val ListItemThreeLineVerticalPadding = 12.dp
@VisibleForTesting internal val ListItemStartPadding = 10.dp
@VisibleForTesting internal val ListItemEndPadding = 10.dp
// Icon related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val LeadingContentEndPadding = Modifier.padding(end = 10.dp)
// Icon related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val SupportingContentTopPadding = Modifier.padding(top = 2.dp)
// Trailing related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val TrailingContentStartPadding = Modifier.padding(start = 10.dp)

View File

@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -45,33 +46,31 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
@Composable @Composable
fun UserCompose( fun UserCompose(
baseUser: User, baseUser: User,
modifier: Modifier = StdPadding, modifier: Modifier = Modifier,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
Row( SlimListItem(
modifier = modifier.clickable { nav.nav(routeFor(baseUser)) }, modifier = modifier.clickable { nav.nav(routeFor(baseUser)) },
verticalAlignment = Alignment.CenterVertically, leadingContent = {
) {
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav) UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
},
Column(modifier = remember { StdStartPadding.weight(1f) }) { headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseUser, accountViewModel = accountViewModel) UsernameDisplay(baseUser, accountViewModel = accountViewModel)
} },
supportingContent = {
AboutDisplay(baseUser, accountViewModel) AboutDisplay(baseUser, accountViewModel)
} },
trailingContent = {
Row(modifier = StdStartPadding) { Row(verticalAlignment = Alignment.CenterVertically) {
UserActionOptions(baseUser, accountViewModel, nav) UserActionOptions(baseUser, accountViewModel, nav)
} }
} },
)
} }
@Composable @Composable

View File

@@ -24,7 +24,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.material3.ListItem
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
@@ -37,19 +37,24 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.model.LocalCache
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.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -63,11 +68,11 @@ fun ZapNoteCompose(
) { ) {
val baseNoteRequest by observeNote(baseReqResponse.zapRequest, accountViewModel) val baseNoteRequest by observeNote(baseReqResponse.zapRequest, accountViewModel)
var baseAuthor by remember { mutableStateOf<User?>(null) } var baseAuthor by remember { mutableStateOf<User?>(baseReqResponse.zapRequest.author) }
LaunchedEffect(baseNoteRequest) { LaunchedEffect(baseNoteRequest) {
baseNoteRequest?.note?.let { accountViewModel.decryptAmountMessage(baseNoteRequest.note, baseReqResponse.zapEvent) {
accountViewModel.decryptAmountMessage(it, baseReqResponse.zapEvent) { baseAuthor = it?.user } baseAuthor = it?.user
} }
} }
@@ -81,49 +86,95 @@ fun ZapNoteCompose(
), ),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
baseAuthor?.let { RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel) } baseAuthor?.let { RenderZapNoteSlim(it, baseReqResponse.zapEvent, accountViewModel, nav) }
} }
} }
} }
@Preview
@Composable
fun RenderZapNotePreview() {
val accountViewModel = mockAccountViewModel()
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
val note1: Note = LocalCache.getOrCreateNote("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
ThemeComparisonColumn {
RenderZapNote(
user1,
note1,
accountViewModel,
EmptyNav,
)
}
}
@Preview
@Composable
fun RenderZapNoteSlimPreview() {
val accountViewModel = mockAccountViewModel()
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
val note1: Note = LocalCache.getOrCreateNote("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
ThemeComparisonColumn {
RenderZapNoteSlim(
user1,
note1,
accountViewModel,
EmptyNav,
)
}
}
@Composable @Composable
private fun RenderZapNote( private fun RenderZapNote(
baseAuthor: User, baseAuthor: User,
zapNote: Note, zapNote: Note,
nav: INav,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav,
) { ) {
Row( ListItem(
modifier = leadingContent = {
remember {
Modifier.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
)
},
verticalAlignment = Alignment.CenterVertically,
) {
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav) UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
},
Column( headlineContent = {
modifier = remember { StdStartPadding.weight(1f) }, UsernameDisplay(baseAuthor, accountViewModel = accountViewModel)
) { },
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseAuthor, accountViewModel = accountViewModel) } supportingContent = {
Row(verticalAlignment = Alignment.CenterVertically) { AboutDisplay(baseAuthor, accountViewModel) }
}
Column(
modifier = StdStartPadding,
verticalArrangement = Arrangement.Center,
) {
ZapAmount(zapNote, accountViewModel) ZapAmount(zapNote, accountViewModel)
} },
trailingContent = {
Row(modifier = StdStartPadding) { Row(verticalAlignment = Alignment.CenterVertically) {
UserActionOptions(baseAuthor, accountViewModel, nav) UserActionOptions(baseAuthor, accountViewModel, nav)
} }
},
)
} }
@Composable
private fun RenderZapNoteSlim(
baseAuthor: User,
zapNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
SlimListItem(
leadingContent = {
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
},
headlineContent = {
UsernameDisplay(baseAuthor, accountViewModel = accountViewModel)
},
supportingContent = {
ZapAmount(zapNote, accountViewModel)
},
trailingContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
UserActionOptions(baseAuthor, accountViewModel, nav)
}
},
)
} }
@Composable @Composable
@@ -154,6 +205,20 @@ private fun ZapAmount(
} }
} }
@Composable
fun AboutDisplayNoFormat(
baseAuthor: User,
accountViewModel: AccountViewModel,
) {
val aboutMe by observeUserAboutMe(baseAuthor, accountViewModel)
Text(
aboutMe,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Composable @Composable
fun AboutDisplay( fun AboutDisplay(
baseAuthor: User, baseAuthor: User,

View File

@@ -20,13 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -36,20 +32,14 @@ import androidx.compose.runtime.getValue
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
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.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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
@@ -76,8 +66,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.Room
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.newItemBubbleModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
@@ -316,6 +308,7 @@ private fun UserRoomCompose(
val lastReadTime by accountViewModel.account.loadLastReadFlow("Room/${room.hashCode()}").collectAsStateWithLifecycle() val lastReadTime by accountViewModel.account.loadLastReadFlow("Room/${room.hashCode()}").collectAsStateWithLifecycle()
if ((lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime) { if ((lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime) {
Spacer(modifier = Height4dpModifier)
NewItemsBubble() NewItemsBubble()
} }
}, },
@@ -412,6 +405,7 @@ fun ChannelName(
} }
if (hasNewMessages) { if (hasNewMessages) {
Spacer(modifier = Height4dpModifier)
NewItemsBubble() NewItemsBubble()
} }
}, },
@@ -434,23 +428,5 @@ private fun TimeAgo(channelLastTime: Long?) {
@Composable @Composable
fun NewItemsBubble() { fun NewItemsBubble() {
Box( Box(MaterialTheme.colorScheme.newItemBubbleModifier)
modifier =
Modifier
.padding(start = 3.dp)
.width(10.dp)
.height(10.dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center,
) {
Text(
"",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 12.sp,
maxLines = 1,
modifier = Modifier.wrapContentHeight().align(Alignment.Center),
)
}
} }

View File

@@ -21,15 +21,12 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
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.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.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
@@ -82,11 +79,7 @@ private fun LnZapFeedLoaded(
) { ) {
itemsIndexed(items, key = { _, item -> item.zapEvent.idHex }) { _, item -> itemsIndexed(items, key = { _, item -> item.zapEvent.idHex }) { _, item ->
ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav)
HorizontalDivider(thickness = DividerThickness)
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }
} }

View File

@@ -27,10 +27,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -253,18 +251,28 @@ val DarkRelayIconModifier =
val darkLargeProfilePictureModifier = val darkLargeProfilePictureModifier =
Modifier Modifier
.width(120.dp) .size(120.dp)
.height(120.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, DarkColorPalette.onBackground, CircleShape) .border(3.dp, DarkColorPalette.onBackground, CircleShape)
val lightLargeProfilePictureModifier = val lightLargeProfilePictureModifier =
Modifier Modifier
.width(120.dp) .size(120.dp)
.height(120.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, LightColorPalette.onBackground, CircleShape) .border(3.dp, LightColorPalette.onBackground, CircleShape)
val darkNewItemBubbleModifier =
Modifier
.size(10.dp)
.clip(shape = CircleShape)
.background(DarkColorPalette.primary)
val lightNewItemBubbleModifier =
Modifier
.size(10.dp)
.clip(shape = CircleShape)
.background(LightColorPalette.primary)
val RichTextDefaults = RichTextStyle().resolveDefaults() val RichTextDefaults = RichTextStyle().resolveDefaults()
val MarkDownStyleOnDark = val MarkDownStyleOnDark =
@@ -462,6 +470,10 @@ val ColorScheme.selectedReactionBoxModifier: Modifier
val ColorScheme.largeProfilePictureModifier: Modifier val ColorScheme.largeProfilePictureModifier: Modifier
get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier
@Suppress("ModifierFactoryExtensionFunction")
val ColorScheme.newItemBubbleModifier: Modifier
get() = if (isLight) lightNewItemBubbleModifier else darkNewItemBubbleModifier
val chartLightColors = val chartLightColors =
VicoTheme( VicoTheme(
candlestickCartesianLayerColors = candlestickCartesianLayerColors =