Refinements to ShopStr's event rendering.

This commit is contained in:
Vitor Pamplona
2023-11-29 14:56:42 -05:00
parent fcdb863c2b
commit 6b7bb209d4
5 changed files with 224 additions and 58 deletions
@@ -833,6 +833,44 @@ private fun DialogContent(
} }
} }
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun InlineCarrousel(
allImages: ImmutableList<String>,
imageUrl: String
) {
val pagerState: PagerState = rememberPagerState() { allImages.size }
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
launch {
val page = allImages.indexOf(imageUrl)
if (page > -1) {
pagerState.scrollToPage(page)
}
}
}
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState
) { index ->
AsyncImage(
model = allImages[index],
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
} else {
AsyncImage(
model = imageUrl,
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable @Composable
private fun CopyToClipboard( private fun CopyToClipboard(
content: ZoomableContent content: ZoomableContent
@@ -1206,7 +1206,8 @@ private fun RenderNoteRow(
RenderClassifieds( RenderClassifieds(
noteEvent, noteEvent,
baseNote, baseNote,
accountViewModel accountViewModel,
nav
) )
} }
@@ -3896,7 +3897,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
} }
@Composable @Composable
private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountViewModel: AccountViewModel) { private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val image = remember(noteEvent) { noteEvent.image() } val image = remember(noteEvent) { noteEvent.image() }
val title = remember(noteEvent) { noteEvent.title() } val title = remember(noteEvent) { noteEvent.title() }
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } } val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
@@ -3933,18 +3934,23 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
text = it, text = it,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
price?.let { price?.let {
val priceTag = remember(noteEvent) { val priceTag = remember(noteEvent) {
val newAmount = price.amount.toBigDecimalOrNull()?.let {
showAmount(it)
} ?: price.amount
if (price.frequency != null && price.currency != null) { if (price.frequency != null && price.currency != null) {
"${price.amount} ${price.currency}/${price.frequency}" "$newAmount ${price.currency}/${price.frequency}"
} else if (price.currency != null) { } else if (price.currency != null) {
"${price.amount} ${price.currency}" "$newAmount ${price.currency}"
} else { } else {
price.amount newAmount
} }
} }
@@ -3956,7 +3962,6 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
modifier = remember { modifier = remember {
Modifier Modifier
.clip(SmallBorder) .clip(SmallBorder)
.background(Color.Black)
.padding(start = 5.dp) .padding(start = 5.dp)
} }
) )
@@ -3977,16 +3982,46 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
) )
} }
location?.let { /*
Text( Column {
text = it, location?.let {
style = MaterialTheme.typography.bodySmall, Text(
color = Color.Gray, text = it,
maxLines = 1, style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis, color = Color.Gray,
modifier = Modifier.padding(start = 5.dp) maxLines = 1,
) overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(start = 5.dp)
)
}
Button(
modifier = Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
note.author?.let {
accountViewModel.createChatRoomFor(it) {
nav("Room/$it")
}
}
},
contentPadding = ZeroPadding,
colors = ButtonDefaults
.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
painter = painterResource(R.drawable.ic_dm),
stringResource(R.string.send_a_direct_message),
modifier = Modifier.size(20.dp),
tint = Color.White
)
}
} }
*/
} }
} }
@@ -6,6 +6,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -17,14 +18,18 @@ import androidx.compose.foundation.layout.size
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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.pullrefresh.PullRefreshIndicator import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState import androidx.compose.material3.pullrefresh.rememberPullRefreshState
@@ -38,15 +43,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
@@ -55,6 +63,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.note.AudioHeader import com.vitorpamplona.amethyst.ui.note.AudioHeader
import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader
@@ -87,13 +96,18 @@ import com.vitorpamplona.amethyst.ui.note.RenderPostApproval
import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
import com.vitorpamplona.amethyst.ui.note.Reward import com.vitorpamplona.amethyst.ui.note.Reward
import com.vitorpamplona.amethyst.ui.note.routeToMessage
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThinSendButton
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.selectedNote import com.vitorpamplona.amethyst.ui.theme.selectedNote
@@ -117,6 +131,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -388,7 +403,7 @@ fun NoteMaster(
} else if (noteEvent is LongTextNoteEvent) { } else if (noteEvent is LongTextNoteEvent) {
RenderLongFormHeaderForThread(noteEvent) RenderLongFormHeaderForThread(noteEvent)
} else if (noteEvent is ClassifiedsEvent) { } else if (noteEvent is ClassifiedsEvent) {
RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel) RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel, nav)
} }
Row( Row(
@@ -517,52 +532,64 @@ fun NoteMaster(
private fun RenderClassifiedsReaderForThread( private fun RenderClassifiedsReaderForThread(
noteEvent: ClassifiedsEvent, noteEvent: ClassifiedsEvent,
note: Note, note: Note,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
val image = remember(noteEvent) { noteEvent.image() } val images = remember(noteEvent) { noteEvent.images().toImmutableList() }
val title = remember(noteEvent) { noteEvent.title() } val title = remember(noteEvent) { noteEvent.title() }
val summary = val summary = remember(noteEvent) {
remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } } val sum = noteEvent.summary()
if (sum != noteEvent.content) {
sum
} else {
null
}
}
val price = remember(noteEvent) { noteEvent.price() } val price = remember(noteEvent) { noteEvent.price() }
val location = remember(noteEvent) { noteEvent.location() } val location = remember(noteEvent) { noteEvent.location() }
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
Column { Column {
Row() { if (images.isNotEmpty()) {
image?.let { Row() {
AsyncImage( InlineCarrousel(
model = it, images,
contentDescription = stringResource( images.first()
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
) )
} ?: CreateImageHeader(note, accountViewModel) }
} else {
CreateImageHeader(note, accountViewModel)
} }
Row( Row(
Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), Modifier.padding(top = 10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
title?.let { title?.let {
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
}
price?.let {
Row(
Modifier.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
val newAmount = price.amount.toBigDecimalOrNull()?.let {
showAmount(it)
} ?: price.amount
price?.let {
val priceTag = remember(noteEvent) { val priceTag = remember(noteEvent) {
if (price.frequency != null && price.currency != null) { if (price.frequency != null && price.currency != null) {
"${price.amount} ${price.currency}/${price.frequency}" "$newAmount ${price.currency}/${price.frequency}"
} else if (price.currency != null) { } else if (price.currency != null) {
"${price.amount} ${price.currency}" "$newAmount ${price.currency}"
} else { } else {
price.amount newAmount
} }
} }
@@ -571,19 +598,22 @@ private fun RenderClassifiedsReaderForThread(
maxLines = 1, maxLines = 1,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = remember { modifier = Modifier.weight(1f)
Modifier
.clip(SmallBorder)
.background(Color.Black)
.padding(start = 5.dp)
}
) )
location?.let {
Text(
text = it,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
} }
summary?.let { summary?.let {
Row( Row(
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), Modifier.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -596,19 +626,78 @@ private fun RenderClassifiedsReaderForThread(
} }
} }
location?.let { Row(
Row( Modifier
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), .padding(start = 20.dp, end = 20.dp, bottom = 5.dp, top = 15.dp)
verticalAlignment = Alignment.CenterVertically .fillMaxWidth(),
) { verticalAlignment = Alignment.CenterVertically
Text( ) {
text = it, Icon(
style = MaterialTheme.typography.bodySmall, painter = painterResource(R.drawable.ic_dm),
color = Color.Gray, stringResource(R.string.send_a_direct_message),
maxLines = 1, modifier = Modifier.size(20.dp),
overflow = TextOverflow.Ellipsis tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = StdHorzSpacer)
Text(stringResource(id = R.string.send_the_seller_a_message))
}
Row(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, bottom = 5.dp, top = 5.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val sellerName = note.author?.bestDisplayName() ?: note.author?.bestUsername()
val msg = if (sellerName != null) {
stringResource(
id = R.string.hi_seller_is_this_still_available,
sellerName
) )
} else {
stringResource(id = R.string.hi_there_is_this_still_available)
} }
var message by remember {
mutableStateOf(TextFieldValue(msg))
}
TextField(
value = message,
onValueChange = {
message = it
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
shape = EditFieldBorder,
modifier = Modifier.weight(1f, true),
placeholder = {
Text(
text = stringResource(R.string.reply_here),
color = MaterialTheme.colorScheme.placeholderText
)
},
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
trailingIcon = {
ThinSendButton(
isActive = message.text.isNotBlank(),
modifier = EditFieldTrailingIconModifier
) {
note.author?.let {
nav(routeToMessage(it, msg, accountViewModel))
}
}
},
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
} }
} }
} }
+3
View File
@@ -664,4 +664,7 @@
<string name="payment_required_title">Message from %1$s</string> <string name="payment_required_title">Message from %1$s</string>
<string name="thread_title">Thread</string> <string name="thread_title">Thread</string>
<string name="send_the_seller_a_message">Send the seller a message</string>
<string name="hi_seller_is_this_still_available">Hi %1$s, is this still available?</string>
<string name="hi_there_is_this_still_available">Hi there, is this still available?</string>
</resources> </resources>
@@ -19,6 +19,7 @@ class ClassifiedsEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun images() = tags.filter { it.size > 1 && it[0] == "image" }.map { it[1] }
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let { fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
Price(it[1], it.getOrNull(2), it.getOrNull(3)) Price(it[1], it.getOrNull(2), it.getOrNull(3))