diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index e2b714d28..cb543a4e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -833,6 +833,44 @@ private fun DialogContent( } } +@Composable +@OptIn(ExperimentalFoundationApi::class) +fun InlineCarrousel( + allImages: ImmutableList, + 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 private fun CopyToClipboard( content: ZoomableContent diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 42a3c1792..a342dafb5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1206,7 +1206,8 @@ private fun RenderNoteRow( RenderClassifieds( noteEvent, baseNote, - accountViewModel + accountViewModel, + nav ) } @@ -3896,7 +3897,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView } @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 title = remember(noteEvent) { noteEvent.title() } 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, style = MaterialTheme.typography.bodyLarge, maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) } price?.let { val priceTag = remember(noteEvent) { + val newAmount = price.amount.toBigDecimalOrNull()?.let { + showAmount(it) + } ?: price.amount + if (price.frequency != null && price.currency != null) { - "${price.amount} ${price.currency}/${price.frequency}" + "$newAmount ${price.currency}/${price.frequency}" } else if (price.currency != null) { - "${price.amount} ${price.currency}" + "$newAmount ${price.currency}" } else { - price.amount + newAmount } } @@ -3956,7 +3962,6 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi modifier = remember { Modifier .clip(SmallBorder) - .background(Color.Black) .padding(start = 5.dp) } ) @@ -3977,16 +3982,46 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi ) } - location?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(start = 5.dp) - ) + /* + Column { + location?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + 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 + ) + } } + + */ } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 91e15f061..20d1125a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -6,6 +6,7 @@ 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 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle 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.pullRefresh import androidx.compose.material3.pullrefresh.rememberPullRefreshState @@ -38,15 +43,18 @@ 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.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle 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.unit.dp import androidx.compose.ui.unit.em @@ -55,6 +63,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R 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.note.AudioHeader 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.RenderTextEvent 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.screen.loggedIn.AccountViewModel 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.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.SmallBorder +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.placeholderText 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.RelaySetEvent import com.vitorpamplona.quartz.events.RepostEvent +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -388,7 +403,7 @@ fun NoteMaster( } else if (noteEvent is LongTextNoteEvent) { RenderLongFormHeaderForThread(noteEvent) } else if (noteEvent is ClassifiedsEvent) { - RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel) + RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel, nav) } Row( @@ -517,52 +532,64 @@ fun NoteMaster( private fun RenderClassifiedsReaderForThread( noteEvent: ClassifiedsEvent, 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 summary = - remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } } + val summary = remember(noteEvent) { + val sum = noteEvent.summary() + if (sum != noteEvent.content) { + sum + } else { + null + } + } val price = remember(noteEvent) { noteEvent.price() } val location = remember(noteEvent) { noteEvent.location() } Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { Column { - Row() { - image?.let { - AsyncImage( - model = it, - contentDescription = stringResource( - R.string.preview_card_image_for, - it - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() + if (images.isNotEmpty()) { + Row() { + InlineCarrousel( + images, + images.first() ) - } ?: CreateImageHeader(note, accountViewModel) + } + } else { + CreateImageHeader(note, accountViewModel) } Row( - Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), + Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically ) { title?.let { Text( text = it, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, + style = MaterialTheme.typography.headlineSmall, 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) { if (price.frequency != null && price.currency != null) { - "${price.amount} ${price.currency}/${price.frequency}" + "$newAmount ${price.currency}/${price.frequency}" } else if (price.currency != null) { - "${price.amount} ${price.currency}" + "$newAmount ${price.currency}" } else { - price.amount + newAmount } } @@ -571,19 +598,22 @@ private fun RenderClassifiedsReaderForThread( maxLines = 1, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, - modifier = remember { - Modifier - .clip(SmallBorder) - .background(Color.Black) - .padding(start = 5.dp) - } + modifier = Modifier.weight(1f) ) + + location?.let { + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } } summary?.let { Row( - Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), + Modifier.padding(top = 5.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -596,19 +626,78 @@ private fun RenderClassifiedsReaderForThread( } } - location?.let { - Row( - Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis + Row( + Modifier + .padding(start = 20.dp, end = 20.dp, bottom = 5.dp, top = 15.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_dm), + stringResource(R.string.send_a_direct_message), + modifier = Modifier.size(20.dp), + 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 + ) + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c9116a9c..b774b9547 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -664,4 +664,7 @@ Message from %1$s Thread + Send the seller a message + Hi %1$s, is this still available? + Hi there, is this still available? diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index 71c88d730..107e46f46 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -19,6 +19,7 @@ class ClassifiedsEvent( ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { 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 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 price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let { Price(it[1], it.getOrNull(2), it.getOrNull(3))