mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 02:37:22 +01:00
Improves padding of the ListItem interfaces
This commit is contained in:
@@ -39,6 +39,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble
|
||||
@@ -47,57 +48,86 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
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
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ChannelNamePreview() {
|
||||
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)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
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())
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
Row {
|
||||
},
|
||||
secondRow = {
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("This is my author", Modifier.weight(1f))
|
||||
TimeAgo(TimeUtils.now())
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
Row {
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
|
||||
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.INav
|
||||
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.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
|
||||
@Composable
|
||||
fun UserCompose(
|
||||
baseUser: User,
|
||||
modifier: Modifier = StdPadding,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(
|
||||
SlimListItem(
|
||||
modifier = modifier.clickable { nav.nav(routeFor(baseUser)) },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
Column(modifier = remember { StdStartPadding.weight(1f) }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(baseUser, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
leadingContent = {
|
||||
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
},
|
||||
headlineContent = {
|
||||
UsernameDisplay(baseUser, accountViewModel = accountViewModel)
|
||||
},
|
||||
supportingContent = {
|
||||
AboutDisplay(baseUser, accountViewModel)
|
||||
}
|
||||
|
||||
Row(modifier = StdStartPadding) {
|
||||
UserActionOptions(baseUser, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UserActionOptions(baseUser, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,19 +37,24 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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 com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
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.routes.routeFor
|
||||
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.theme.BitcoinOrange
|
||||
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.quartz.nip57Zaps.LnZapEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -63,11 +68,11 @@ fun ZapNoteCompose(
|
||||
) {
|
||||
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) {
|
||||
baseNoteRequest?.note?.let {
|
||||
accountViewModel.decryptAmountMessage(it, baseReqResponse.zapEvent) { baseAuthor = it?.user }
|
||||
accountViewModel.decryptAmountMessage(baseNoteRequest.note, baseReqResponse.zapEvent) {
|
||||
baseAuthor = it?.user
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,49 +86,95 @@ fun ZapNoteCompose(
|
||||
),
|
||||
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
|
||||
private fun RenderZapNote(
|
||||
baseAuthor: User,
|
||||
zapNote: Note,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
remember {
|
||||
Modifier.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp,
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
Column(
|
||||
modifier = remember { StdStartPadding.weight(1f) },
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseAuthor, accountViewModel = accountViewModel) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) { AboutDisplay(baseAuthor, accountViewModel) }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = StdStartPadding,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
ListItem(
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = StdStartPadding) {
|
||||
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
|
||||
@@ -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
|
||||
fun AboutDisplay(
|
||||
baseAuthor: User,
|
||||
|
||||
@@ -20,13 +20,9 @@
|
||||
*/
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -36,20 +32,14 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
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.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.stringRes
|
||||
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.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBubbleModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
@@ -316,6 +308,7 @@ private fun UserRoomCompose(
|
||||
|
||||
val lastReadTime by accountViewModel.account.loadLastReadFlow("Room/${room.hashCode()}").collectAsStateWithLifecycle()
|
||||
if ((lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime) {
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
NewItemsBubble()
|
||||
}
|
||||
},
|
||||
@@ -412,6 +405,7 @@ fun ChannelName(
|
||||
}
|
||||
|
||||
if (hasNewMessages) {
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
NewItemsBubble()
|
||||
}
|
||||
},
|
||||
@@ -434,23 +428,5 @@ private fun TimeAgo(channelLastTime: Long?) {
|
||||
|
||||
@Composable
|
||||
fun NewItemsBubble() {
|
||||
Box(
|
||||
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),
|
||||
)
|
||||
}
|
||||
Box(MaterialTheme.colorScheme.newItemBubbleModifier)
|
||||
}
|
||||
|
||||
@@ -21,15 +21,12 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
@@ -82,11 +79,7 @@ private fun LnZapFeedLoaded(
|
||||
) {
|
||||
itemsIndexed(items, key = { _, item -> item.zapEvent.idHex }) { _, item ->
|
||||
ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,8 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -253,18 +251,28 @@ val DarkRelayIconModifier =
|
||||
|
||||
val darkLargeProfilePictureModifier =
|
||||
Modifier
|
||||
.width(120.dp)
|
||||
.height(120.dp)
|
||||
.size(120.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, DarkColorPalette.onBackground, CircleShape)
|
||||
|
||||
val lightLargeProfilePictureModifier =
|
||||
Modifier
|
||||
.width(120.dp)
|
||||
.height(120.dp)
|
||||
.size(120.dp)
|
||||
.clip(shape = 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 MarkDownStyleOnDark =
|
||||
@@ -462,6 +470,10 @@ val ColorScheme.selectedReactionBoxModifier: Modifier
|
||||
val ColorScheme.largeProfilePictureModifier: Modifier
|
||||
get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier
|
||||
|
||||
@Suppress("ModifierFactoryExtensionFunction")
|
||||
val ColorScheme.newItemBubbleModifier: Modifier
|
||||
get() = if (isLight) lightNewItemBubbleModifier else darkNewItemBubbleModifier
|
||||
|
||||
val chartLightColors =
|
||||
VicoTheme(
|
||||
candlestickCartesianLayerColors =
|
||||
|
||||
Reference in New Issue
Block a user