- Restructures Chat Message Rendering

- Adds Sensitive Content protections for chat renderings.
- Adjusts padding of the channel headers.
This commit is contained in:
Vitor Pamplona
2023-06-20 16:53:36 -04:00
parent 6addce3d20
commit 6a44d283f9
7 changed files with 413 additions and 231 deletions
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@@ -34,7 +33,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -69,19 +67,21 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size13dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -94,243 +94,407 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
val noteState by baseNote.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
val noteEvent = remember(noteState) { note.event }
var popupExpanded by remember { mutableStateOf(false) }
val noteEvent = remember(noteState) { noteState?.note?.event }
if (noteEvent == null) {
BlankNote(
Modifier.combinedClickable(
onClick = { },
onLongClick = { popupExpanded = true }
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
Modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
}
)
}
} else {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
}
}
@Composable
fun CheckHiddenChatMessage(
baseNote: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val isHidden by remember(accountState) {
derivedStateOf {
val isSensitive = baseNote.event?.isSensitive() ?: false
accountState?.account?.isHidden(baseNote.author!!) == true || (isSensitive && accountState?.account?.showSensitiveContent == false)
}
}
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(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = persistentSetOf()
)
)
}
note.let {
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
WatchForReports(baseNote, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
}
} else if (account.isHidden(noteForReports.author!!)) {
// Does nothing
}
var showReportedNote by remember { mutableStateOf(false) }
val showHiddenNote by remember(state, showReportedNote) {
derivedStateOf {
!state.isAcceptable && !showReportedNote
}
}
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
accountViewModel,
Modifier,
innerQuote,
nav,
onClick = { showReportedNote = true }
)
} else {
var showHiddenNote by remember { mutableStateOf(false) }
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
launch(Dispatchers.Default) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!(noteForReports.hasAnyReports())
val newIsAcceptable = account.isAcceptable(noteForReports)
if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) {
isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview)
}
}
val canPreview by remember(state, showReportedNote) {
derivedStateOf {
(!state.isAcceptable && showReportedNote) || state.canPreview
}
}
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
val reports = remember {
account.getRelevantReports(noteForReports).toImmutableSet()
}
HiddenNote(
reports,
accountViewModel,
Modifier,
innerQuote,
nav,
onClick = { showHiddenNote = true }
)
NormalChatNote(
baseNote,
routeForLastRead,
innerQuote,
canPreview,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NormalChatNote(
note: Note,
routeForLastRead: String?,
innerQuote: Boolean = false,
canPreview: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val drawAuthorInfo by remember {
derivedStateOf {
(innerQuote || !accountViewModel.isLoggedUser(note.author)) && note.event !is PrivateDmEvent
}
}
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
val otherColors = MaterialTheme.colors.subtleBorder
val defaultBackground = MaterialTheme.colors.background
val backgroundBubbleColor = remember {
if (accountViewModel.isLoggedUser(note.author)) {
mutableStateOf(loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
} else {
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
val otherColors = MaterialTheme.colors.subtleBorder
val defaultBackground = MaterialTheme.colors.background
mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
}
}
val alignment: Arrangement.Horizontal = remember {
if (accountViewModel.isLoggedUser(note.author)) {
Arrangement.End
} else {
Arrangement.Start
}
}
val shape: Shape = remember {
if (accountViewModel.isLoggedUser(note.author)) {
ChatBubbleShapeMe
} else {
ChatBubbleShapeThem
}
}
val backgroundBubbleColor = remember {
if (note.author == loggedIn) {
mutableStateOf(loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
} else {
mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
}
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(it, createdAt)
}
val alignment: Arrangement.Horizontal = remember {
if (note.author == loggedIn) {
Arrangement.End
} else {
Arrangement.Start
}
}
}
Column() {
val modif = remember {
if (innerQuote) {
Modifier.padding(top = 10.dp, end = 5.dp)
} else {
Modifier
.fillMaxWidth(1f)
.padding(
start = 12.dp,
end = 12.dp,
top = 5.dp,
bottom = 5.dp
)
}
val shape: Shape = remember {
if (note.author == loggedIn) {
ChatBubbleShapeMe
} else {
ChatBubbleShapeThem
}
}
Row(
modifier = modif,
horizontalArrangement = alignment
) {
val availableBubbleSize = remember { mutableStateOf(IntSize.Zero) }
var popupExpanded by remember { mutableStateOf(false) }
val modif2 = remember {
if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
}
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
scope.launch(Dispatchers.IO) {
val lastTime = accountViewModel.account.loadLastRead(it)
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(it, createdAt)
}
}
}
}
Column() {
val modif = remember {
if (innerQuote) {
Modifier.padding(top = 10.dp, end = 5.dp)
} else {
Modifier
.fillMaxWidth(1f)
.padding(
start = 12.dp,
end = 12.dp,
top = 5.dp,
bottom = 5.dp
)
}
}
Row(
modifier = modif,
horizontalArrangement = alignment
) {
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
Row(
horizontalArrangement = alignment,
modifier = modif2.onSizeChanged {
availableBubbleSize = it
}
) {
Surface(
color = backgroundBubbleColor.value,
shape = shape,
modifier = Modifier
.combinedClickable(
onClick = {
if (noteEvent is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
}
},
onLongClick = { popupExpanded = true }
)
) {
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
Column(
modifier = Modifier
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
.onSizeChanged {
bubbleSize = it
}
) {
if ((innerQuote || note.author != loggedIn) && noteEvent !is PrivateDmEvent) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = Modifier.height(5.dp))
}
val replyTo = note.replyTo
if (!innerQuote && !replyTo.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
replyTo.lastOrNull()?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
when (noteEvent) {
is ChannelCreateEvent -> {
RenderCreateChannelNote(note)
}
is ChannelMetadataEvent -> {
RenderChangeChannelMetadataNote(note)
}
else -> {
RenderRegularTextNote(
note,
isAcceptableAndCanPreview.second,
backgroundBubbleColor,
accountViewModel,
nav
)
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(top = 5.dp)
.then(
with(LocalDensity.current) {
Modifier.widthIn(
bubbleSize.width.toDp(),
availableBubbleSize.width.toDp()
)
}
)
) {
StatusRow(
baseNote,
accountViewModel,
onWantsToReply
)
}
val clickableModifier = remember {
Modifier
.combinedClickable(
onClick = {
if (note.event is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
}
}
}
},
onLongClick = { popupExpanded = true }
)
}
NoteQuickActionMenu(
Row(
horizontalArrangement = alignment,
modifier = modif2.onSizeChanged {
availableBubbleSize.value = it
}
) {
Surface(
color = backgroundBubbleColor.value,
shape = shape,
modifier = clickableModifier
) {
RenderBubble(
note,
popupExpanded,
{ popupExpanded = false },
accountViewModel
drawAuthorInfo,
alignment,
innerQuote,
backgroundBubbleColor,
onWantsToReply,
canPreview,
availableBubbleSize,
accountViewModel,
nav
)
}
}
NoteQuickActionMenu(
note = note,
popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false },
accountViewModel = accountViewModel
)
}
}
}
@Composable
private fun RenderBubble(
baseNote: Note,
drawAuthorInfo: Boolean,
alignment: Arrangement.Horizontal,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
onWantsToReply: (Note) -> Unit,
canPreview: Boolean,
availableBubbleSize: MutableState<IntSize>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val bubbleSize = remember { mutableStateOf(IntSize.Zero) }
val bubbleModifier = remember {
Modifier
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
.onSizeChanged {
bubbleSize.value = it
}
}
Column(modifier = bubbleModifier) {
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
nav
)
} else {
Spacer(modifier = StdVertSpacer)
}
RenderReplyRow(
note = baseNote,
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
NoteRow(
note = baseNote,
canPreview = canPreview,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav
)
ConstrainedStatusRow(
bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize
) {
StatusRow(
baseNote = baseNote,
accountViewModel = accountViewModel,
onWantsToReply = onWantsToReply
)
}
}
}
@Composable
private fun RenderReplyRow(
note: Note,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val replyTo by remember {
derivedStateOf {
note.replyTo?.lastOrNull()
}
}
if (!innerQuote && replyTo != null) {
Row(verticalAlignment = Alignment.CenterVertically) {
replyTo?.let { note ->
ChatroomMessageCompose(
note,
null,
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply
)
}
}
}
}
@Composable
private fun NoteRow(
note: Note,
canPreview: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically) {
when (remember(note) { note.event }) {
is ChannelCreateEvent -> {
RenderCreateChannelNote(note)
}
is ChannelMetadataEvent -> {
RenderChangeChannelMetadataNote(note)
}
else -> {
RenderRegularTextNote(
note,
canPreview,
backgroundBubbleColor,
accountViewModel,
nav
)
}
}
}
}
@Composable
private fun ConstrainedStatusRow(
bubbleSize: MutableState<IntSize>,
availableBubbleSize: MutableState<IntSize>,
content: @Composable () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(top = 5.dp)
.then(
with(LocalDensity.current) {
Modifier.widthIn(
bubbleSize.value.width.toDp(),
availableBubbleSize.value.width.toDp()
)
}
)
) {
content()
}
}
@Composable
private fun StatusRow(
baseNote: Note,
@@ -343,15 +507,15 @@ private fun StatusRow(
Row(verticalAlignment = Alignment.CenterVertically) {
ChatTimeAgo(time)
RelayBadges(baseNote)
Spacer(modifier = Modifier.width(10.dp))
Spacer(modifier = DoubleHorzSpacer)
}
Row(verticalAlignment = Alignment.CenterVertically) {
LikeReaction(baseNote, grayTint, accountViewModel)
Spacer(modifier = Modifier.width(5.dp))
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote, grayTint, accountViewModel)
Spacer(modifier = Modifier.width(5.dp))
ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false, iconSize = 16.dp) {
Spacer(modifier = StdHorzSpacer)
ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false, iconSize = Size16dp) {
onWantsToReply(baseNote)
}
}
@@ -333,7 +333,7 @@ fun LoadedNoteCompose(
}
@Composable
private fun WatchForReports(
fun WatchForReports(
note: Note,
accountViewModel: AccountViewModel,
onChange: (Boolean, Boolean, Set<Note>) -> Unit
@@ -380,7 +380,7 @@ fun NormalNote(
val channelHex = remember { baseNote.channelHex() }
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
ChannelHeader(channelHex = channelHex, showVideo = !makeItShort, accountViewModel = accountViewModel, nav = nav)
ChannelHeader(channelHex = channelHex, showVideo = !makeItShort, showBottomDiviser = true, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) {
@@ -1668,7 +1668,8 @@ private fun ReplyRow(
ChannelHeader(
channelHex = channelHex,
showVideo = false,
modifier = remember { Modifier.padding(vertical = 10.dp) },
showBottomDiviser = false,
modifier = remember { Modifier.padding(vertical = 5.dp) },
accountViewModel = accountViewModel,
nav = nav
)
@@ -1677,7 +1678,6 @@ private fun ReplyRow(
val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() }
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
Spacer(modifier = Modifier.height(5.dp))
}
}
}
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
@@ -21,6 +22,7 @@ import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
@@ -135,11 +137,16 @@ fun ReplyInformationChannel(
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
sortedMentions = mentions
val newSortedMentions = mentions
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { accountViewModel.account.isFollowing(it) }
.toImmutableList()
.ifEmpty { null }
if (newSortedMentions != sortedMentions) {
sortedMentions = newSortedMentions
}
}
}
@@ -151,6 +158,7 @@ fun ReplyInformationChannel(
nav("User/${it.pubkeyHex}")
}
)
Spacer(modifier = StdVertSpacer)
}
}
@@ -378,7 +378,7 @@ fun NoteMaster(
) {
Column() {
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) {
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, accountViewModel = accountViewModel, nav = nav)
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote)
} else if (noteEvent is FileStorageHeaderEvent) {
@@ -209,6 +209,7 @@ fun ChannelScreen(
ChannelHeader(
baseChannel = channel,
showVideo = true,
showBottomDiviser = true,
accountViewModel = accountViewModel,
nav = nav
)
@@ -480,6 +481,7 @@ fun MyTextField(
fun ChannelHeader(
channelHex: String,
showVideo: Boolean,
showBottomDiviser: Boolean,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -497,6 +499,7 @@ fun ChannelHeader(
ChannelHeader(
it,
showVideo,
showBottomDiviser,
modifier,
accountViewModel,
nav
@@ -508,6 +511,7 @@ fun ChannelHeader(
fun ChannelHeader(
baseChannel: Channel,
showVideo: Boolean,
showBottomDiviser: Boolean,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -559,7 +563,9 @@ fun ChannelHeader(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
channel.toBestDisplayName(),
fontWeight = FontWeight.Bold
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
@@ -589,10 +595,11 @@ fun ChannelHeader(
}
}
Divider(
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
thickness = 0.25.dp
)
if (showBottomDiviser) {
Divider(
thickness = 0.25.dp
)
}
}
}
@@ -185,8 +185,7 @@ private fun FeedLoaded(
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
top = 10.dp
),
state = listState
) {
@@ -194,6 +193,7 @@ private fun FeedLoaded(
ChannelHeader(
channelHex = item.idHex,
showVideo = false,
showBottomDiviser = true,
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
accountViewModel = accountViewModel,
nav = nav
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -23,10 +24,12 @@ val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
val StdButtonSizeModifier = Modifier.size(20.dp)
val StdHorzSpacer = Modifier.width(5.dp)
val StdVertSpacer = Modifier.height(5.dp)
val DoubleHorzSpacer = Modifier.width(10.dp)
val Size35dp = 35.dp
val Size13dp = 13.dp
val Size16dp = 16.dp
val StdPadding = Modifier.padding(10.dp)