- Separates new Product Screen from the generic new Post screen.
- Adds New product button on the Marketplace tab - Adds imeta tags for images and urls inside the content of the Classifieds. - Shows multiple images on the post and thread view. - Removes the option for NIP-95 images on Classifieds. - Creates a new route for new products - Adjusts quarts to process images with iMeta tags
This commit is contained in:
@@ -579,6 +579,21 @@ class Account(
|
||||
normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()),
|
||||
)
|
||||
|
||||
fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set<String> {
|
||||
val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
|
||||
return event?.writeRelays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
val normalizedNIP65WriteRelayList =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65WriteRelayListWithBackup(it.note) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
normalizeNIP65WriteRelayListWithBackup(getNIP65RelayListNote()),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveKind3FollowsFlow: Flow<LiveFollowList> =
|
||||
userProfile().flow().follows.stateFlow.transformLatest {
|
||||
|
||||
@@ -37,7 +37,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.imageExtensions
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -129,9 +128,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -198,14 +194,6 @@ open class NewPostViewModel :
|
||||
var isValidConsensusThreshold = mutableStateOf(true)
|
||||
var isValidClosedAt = mutableStateOf(true)
|
||||
|
||||
// Classifieds
|
||||
var wantsProduct by mutableStateOf(false)
|
||||
var title by mutableStateOf(TextFieldValue(""))
|
||||
var price by mutableStateOf(TextFieldValue(""))
|
||||
var locationText by mutableStateOf(TextFieldValue(""))
|
||||
var category by mutableStateOf(TextFieldValue(""))
|
||||
var condition by mutableStateOf<ConditionTag.CONDITION>(ConditionTag.CONDITION.USED_LIKE_NEW)
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
var wantsInvoice by mutableStateOf(false)
|
||||
@@ -474,49 +462,6 @@ open class NewPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
wantsProduct = draftEvent.kind == 30402
|
||||
|
||||
title =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags
|
||||
.filter { it.size > 1 && it[0] == "title" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
price =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags
|
||||
.filter { it.size > 1 && it[0] == "price" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
category =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags
|
||||
.filter { it.size > 1 && it[0] == "t" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
locationText =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags
|
||||
.filter { it.size > 1 && it[0] == "location" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
condition = ConditionTag.CONDITION.entries.firstOrNull {
|
||||
it.value ==
|
||||
draftEvent
|
||||
.tags
|
||||
.filter { it.size > 1 && it[0] == "condition" }
|
||||
.map { it[1] }
|
||||
.firstOrNull()
|
||||
} ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
@@ -891,49 +836,6 @@ open class NewPostViewModel :
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
|
||||
account?.signAndSend(localDraft, template, relayList, quotes)
|
||||
} else if (wantsProduct) {
|
||||
val images =
|
||||
urls.mapNotNull {
|
||||
val removedParamsFromUrl =
|
||||
if (it.contains("?")) {
|
||||
it.split("?")[0].lowercase()
|
||||
} else if (it.contains("#")) {
|
||||
it.split("#")[0].lowercase()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val quotes = findNostrUris(tagger.message)
|
||||
|
||||
val template =
|
||||
ClassifiedsEvent.build(
|
||||
title.text,
|
||||
PriceTag(price.text, "SATS", null),
|
||||
tagger.message,
|
||||
locationText.text.ifBlank { null },
|
||||
condition,
|
||||
images,
|
||||
) {
|
||||
hashtags(listOfNotNull(category.text.ifBlank { null }) + findHashtags(tagger.message))
|
||||
quotes(quotes)
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
localZapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
|
||||
account?.signAndSend(localDraft, template, relayList, quotes)
|
||||
} else {
|
||||
val replyToSet =
|
||||
@@ -1078,13 +980,6 @@ open class NewPostViewModel :
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount.value = null
|
||||
|
||||
wantsProduct = false
|
||||
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
locationText = TextFieldValue("")
|
||||
title = TextFieldValue("")
|
||||
category = TextFieldValue("")
|
||||
price = TextFieldValue("")
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
wantsToAddGeoHash = false
|
||||
@@ -1215,14 +1110,6 @@ open class NewPostViewModel :
|
||||
isValidvalueMaximum.value
|
||||
)
|
||||
) &&
|
||||
(
|
||||
!wantsProduct ||
|
||||
(
|
||||
!title.text.isNullOrBlank() &&
|
||||
!price.text.isNullOrBlank() &&
|
||||
!category.text.isNullOrBlank()
|
||||
)
|
||||
) &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
@@ -1326,36 +1213,5 @@ open class NewPostViewModel :
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateTitle(it: TextFieldValue) {
|
||||
title = it
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updatePrice(it: TextFieldValue) {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
price = it
|
||||
}
|
||||
}
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateCondition(newCondition: ConditionTag.CONDITION) {
|
||||
condition = newCondition
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateCategory(value: TextFieldValue) {
|
||||
category = value
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateLocation(it: TextFieldValue) {
|
||||
locationText = it
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
}
|
||||
|
||||
-38
@@ -65,7 +65,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.net.toUri
|
||||
import coil3.compose.AsyncImage
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
@@ -342,43 +341,6 @@ private fun saveMediaToGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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
|
||||
private fun RenderImageOrVideo(
|
||||
content: BaseMediaContent,
|
||||
|
||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28P
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.marketplace.NewProductScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
||||
@@ -134,6 +135,17 @@ fun AppNavigation(
|
||||
|
||||
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.NewProduct> {
|
||||
NewProductScreen(
|
||||
message = it.message,
|
||||
attachment = it.attachment?.ifBlank { null }?.toUri(),
|
||||
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.NewPost> {
|
||||
NewPostScreen(
|
||||
message = it.message,
|
||||
|
||||
@@ -124,6 +124,14 @@ sealed class Route {
|
||||
val id: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class NewProduct(
|
||||
val message: String? = null,
|
||||
val attachment: String? = null,
|
||||
val quote: String? = null,
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class NewPost(
|
||||
val message: String? = null,
|
||||
@@ -173,6 +181,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||
dest.hasRoute<Route.Nip47NWCSetup>() -> entry.toRoute<Route.Nip47NWCSetup>()
|
||||
dest.hasRoute<Route.Room>() -> entry.toRoute<Route.Room>()
|
||||
dest.hasRoute<Route.NewPost>() -> entry.toRoute<Route.NewPost>()
|
||||
dest.hasRoute<Route.NewProduct>() -> entry.toRoute<Route.NewProduct>()
|
||||
|
||||
else -> {
|
||||
null
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.note.creators.products
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Sell
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
|
||||
@Composable
|
||||
fun AddClassifiedsButton(
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onClick() },
|
||||
) {
|
||||
if (!isActive) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Sell,
|
||||
contentDescription = stringRes(R.string.classifieds),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Sell,
|
||||
contentDescription = stringRes(id = R.string.cancel_classifieds),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -82,17 +82,34 @@ fun ImageVideoDescription(
|
||||
onDelete: (SelectedMediaProcessing) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
ImageVideoDescription(uris, defaultServer, true, onAdd, onDelete, onCancel, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageVideoDescription(
|
||||
uris: MultiOrchestrator,
|
||||
defaultServer: ServerName,
|
||||
includeNIP95: Boolean,
|
||||
onAdd: (String, ServerName, Boolean, Int) -> Unit,
|
||||
onDelete: (SelectedMediaProcessing) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
|
||||
|
||||
val fileServers by accountViewModel.account.liveServerList.collectAsState()
|
||||
|
||||
val fileServerOptions =
|
||||
remember(fileServers) {
|
||||
remember(fileServers, includeNIP95) {
|
||||
fileServers
|
||||
.map {
|
||||
.mapNotNull {
|
||||
if (it.type == ServerType.NIP95) {
|
||||
TitleExplainer(it.name, nip95description)
|
||||
if (includeNIP95) {
|
||||
TitleExplainer(it.name, nip95description)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
TitleExplainer(it.name, it.baseUrl)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import android.R.attr.maxLines
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -38,19 +38,20 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun RenderClassifieds(
|
||||
@@ -59,12 +60,22 @@ fun RenderClassifieds(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary =
|
||||
remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
||||
val price = remember(noteEvent) { noteEvent.price() }
|
||||
val location = remember(noteEvent) { noteEvent.location() }
|
||||
val imageSet =
|
||||
noteEvent.imageMetas().ifEmpty { null }?.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = note.toNostrUri(),
|
||||
mimeType = it.mimeType,
|
||||
)
|
||||
}
|
||||
val title = noteEvent.title()
|
||||
val summary = noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null }
|
||||
val price = noteEvent.price()
|
||||
val location = noteEvent.location()
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
@@ -78,17 +89,16 @@ fun RenderClassifieds(
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
image?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
R.string.preview_card_image_for,
|
||||
it,
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
imageSet?.let { images ->
|
||||
AutoNonlazyGrid(images.size) {
|
||||
ZoomableContentView(
|
||||
content = images[it],
|
||||
images = images.toImmutableList(),
|
||||
roundedCorner = false,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
DefaultImageHeader(note, accountViewModel)
|
||||
}
|
||||
@@ -100,7 +110,7 @@ fun RenderClassifieds(
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
text = "test'",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+1
-22
@@ -108,8 +108,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.products.AddClassifiedsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.products.SellProduct
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||
@@ -336,15 +334,6 @@ fun NewPostScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsProduct) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
SellProduct(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
@@ -354,7 +343,7 @@ fun NewPostScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
MessageField(
|
||||
if (postViewModel.wantsProduct) R.string.description else R.string.what_s_on_your_mind,
|
||||
R.string.what_s_on_your_mind,
|
||||
postViewModel,
|
||||
)
|
||||
}
|
||||
@@ -526,16 +515,6 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
|
||||
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||
AddPollButton(postViewModel.wantsPoll) {
|
||||
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
||||
if (postViewModel.wantsPoll) {
|
||||
postViewModel.wantsProduct = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddClassifiedsButton(postViewModel.wantsProduct) {
|
||||
postViewModel.wantsProduct = !postViewModel.wantsProduct
|
||||
if (postViewModel.wantsProduct) {
|
||||
postViewModel.wantsPoll = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -68,6 +68,10 @@ class IMetaAttachments {
|
||||
}
|
||||
}
|
||||
|
||||
fun add(imeta: IMetaTag) {
|
||||
replace(imeta.url, imeta)
|
||||
}
|
||||
|
||||
fun addAll(imetas: List<IMetaTag>) {
|
||||
imetas.forEach {
|
||||
replace(it.url, it)
|
||||
|
||||
+35
@@ -25,6 +25,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
@@ -34,7 +35,12 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
@@ -75,6 +81,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
@@ -226,6 +234,11 @@ private fun DiscoverPages(
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingButton = {
|
||||
if (tabs[pagerState.currentPage].resource == R.string.discover_marketplace) {
|
||||
NewProductButton(accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
HorizontalPager(state = pagerState, contentPadding = it) { page ->
|
||||
@@ -299,6 +312,28 @@ private fun RenderDiscoverFeed(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewProductButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.NewProduct())
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringRes(id = R.string.new_product),
|
||||
modifier = Size26Modifier,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDiscoverFeed(
|
||||
feedContentState: FeedContentState,
|
||||
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.screen.loggedIn.discover.marketplace
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
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.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialogEasy
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisplayPreviews
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun NewProductScreen(
|
||||
message: String? = null,
|
||||
attachment: Uri? = null,
|
||||
quote: Note? = null,
|
||||
draft: Note? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val postViewModel: NewProductViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.reloadRelaySet()
|
||||
draft?.let {
|
||||
postViewModel.editFromDraft(it)
|
||||
}
|
||||
quote?.let {
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
}
|
||||
attachment?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mediaType = context.contentResolver.getType(it)
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NewProductScreen(
|
||||
postViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun NewProductScreen(
|
||||
postViewModel: NewProductViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var showRelaysDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = CenterVertically,
|
||||
) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
onClick = { showRelaysDialog = true },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = stringRes(id = R.string.relay_list_selector),
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
nav.popBack()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost(),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Row {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(
|
||||
onPress = {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
postViewModel.sendDraftSync()
|
||||
postViewModel.cancel()
|
||||
}
|
||||
}
|
||||
nav.popBack()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
if (showRelaysDialog) {
|
||||
RelaySelectionDialogEasy(
|
||||
preSelectedList = postViewModel.relayList ?: persistentListOf(),
|
||||
onClose = { showRelaysDialog = false },
|
||||
onPost = { postViewModel.relayList = it.map { it.url }.toImmutableList() },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
NewProductBody(
|
||||
postViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewProductBody(
|
||||
postViewModel: NewProductViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Size10dp)
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
SellProduct(postViewModel = postViewModel)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = Size10dp),
|
||||
) {
|
||||
BaseUserPicture(
|
||||
accountViewModel.userProfile(),
|
||||
Size35dp,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
MessageField(R.string.description, postViewModel)
|
||||
}
|
||||
|
||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ContentSensitivityExplainer()
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToAddGeoHash) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
LocationAsHash(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp),
|
||||
) {
|
||||
ForwardZapTo(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
postViewModel.multiOrchestrator?.let {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ImageVideoDescription(
|
||||
uris = it,
|
||||
defaultServer = accountViewModel.account.settings.defaultFileServer,
|
||||
includeNIP95 = false,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
}
|
||||
},
|
||||
onDelete = postViewModel::deleteMediaToUpload,
|
||||
onCancel = { postViewModel.multiOrchestrator = null },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsInvoice) {
|
||||
postViewModel.lnAddress()?.let { lud16 ->
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
accountViewModel.account.userProfile().pubkeyHex,
|
||||
accountViewModel,
|
||||
stringRes(id = R.string.lightning_invoice),
|
||||
stringRes(id = R.string.lightning_create_and_add_invoice),
|
||||
onSuccess = {
|
||||
postViewModel.insertAtCursor(it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsSecretEmoji) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
SecretEmojiRequest {
|
||||
postViewModel.insertAtCursor(it)
|
||||
postViewModel.wantsSecretEmoji = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ZapRaiserRequest(
|
||||
stringRes(id = R.string.zapraiser),
|
||||
postViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postViewModel.userSuggestions?.let {
|
||||
ShowUserSuggestionList(
|
||||
it,
|
||||
postViewModel::autocompleteWithUser,
|
||||
accountViewModel,
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
)
|
||||
}
|
||||
|
||||
postViewModel.emojiSuggestions?.let {
|
||||
ShowEmojiSuggestionList(
|
||||
it,
|
||||
postViewModel::autocompleteWithEmoji,
|
||||
postViewModel::autocompleteWithEmojiUrl,
|
||||
accountViewModel,
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp),
|
||||
)
|
||||
}
|
||||
|
||||
BottomRowActions(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayPreviews(
|
||||
postViewModel: NewProductViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val urlPreviews by postViewModel.urlPreviews.results.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (urlPreviews.isNotEmpty()) {
|
||||
Row(HalfHorzPadding) {
|
||||
if (urlPreviews.size > 1) {
|
||||
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
||||
items(urlPreviews) {
|
||||
Box(SquaredQuoteBorderModifier) {
|
||||
PreviewUrl(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(FillWidthQuoteBorderModifier) {
|
||||
PreviewUrlFillWidth(urlPreviews[0], accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.horizontalScroll(scrollState)
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
verticalAlignment = CenterVertically,
|
||||
) {
|
||||
SelectFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
postViewModel.selectImage(it)
|
||||
}
|
||||
|
||||
TakePictureButton(
|
||||
onPictureTaken = {
|
||||
postViewModel.selectImage(it)
|
||||
},
|
||||
)
|
||||
|
||||
ForwardZapToButton(postViewModel.wantsForwardZapTo) {
|
||||
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
|
||||
}
|
||||
|
||||
if (postViewModel.canAddZapRaiser) {
|
||||
AddZapraiserButton(postViewModel.wantsZapraiser) {
|
||||
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
|
||||
}
|
||||
}
|
||||
|
||||
MarkAsSensitiveButton(postViewModel.wantsToMarkAsSensitive) {
|
||||
postViewModel.toggleMarkAsSensitive()
|
||||
}
|
||||
|
||||
AddGeoHashButton(postViewModel.wantsToAddGeoHash) {
|
||||
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
|
||||
}
|
||||
|
||||
AddSecretEmojiButton(postViewModel.wantsSecretEmoji) {
|
||||
postViewModel.wantsSecretEmoji = !postViewModel.wantsSecretEmoji
|
||||
}
|
||||
|
||||
if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) {
|
||||
AddLnInvoiceButton(postViewModel.wantsInvoice) {
|
||||
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+667
@@ -0,0 +1,667 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.screen.loggedIn.discover.marketplace
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.UserSuggestionAnchor
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.IZapRaiser
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.IZapField
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ProductImageMeta
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.image
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
open class NewProductViewModel :
|
||||
ViewModel(),
|
||||
ILocationGrabber,
|
||||
IMessageField,
|
||||
IZapField,
|
||||
IZapRaiser {
|
||||
val draftTag = DraftTagState()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
draftTag.versions.collectLatest {
|
||||
sendDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
|
||||
var productImages by mutableStateOf<List<ProductImageMeta>>(emptyList())
|
||||
val iMetaDescription = IMetaAttachments()
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
|
||||
var userSuggestions: UserSuggestionState? = null
|
||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
// Images and Videos
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
|
||||
// Classifieds
|
||||
var title by mutableStateOf(TextFieldValue(""))
|
||||
var price by mutableStateOf(TextFieldValue(""))
|
||||
var locationText by mutableStateOf(TextFieldValue(""))
|
||||
var category by mutableStateOf(TextFieldValue(""))
|
||||
var condition by mutableStateOf<ConditionTag.CONDITION>(ConditionTag.CONDITION.USED_LIKE_NEW)
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
var wantsInvoice by mutableStateOf(false)
|
||||
|
||||
var wantsSecretEmoji by mutableStateOf(false)
|
||||
|
||||
// Forward Zap to
|
||||
var wantsForwardZapTo by mutableStateOf(false)
|
||||
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
|
||||
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
|
||||
|
||||
// NSFW, Sensitive
|
||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
||||
|
||||
// GeoHash
|
||||
var wantsToAddGeoHash by mutableStateOf(false)
|
||||
var location: StateFlow<LocationState.LocationResult>? = null
|
||||
|
||||
// ZapRaiser
|
||||
var canAddZapRaiser by mutableStateOf(false)
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||
|
||||
var relayList by mutableStateOf<ImmutableList<String>?>(null)
|
||||
|
||||
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
|
||||
|
||||
fun user(): User? = account?.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
this.account = accountVM.account
|
||||
this.canAddInvoice = hasLnAddress()
|
||||
this.canAddZapRaiser = hasLnAddress()
|
||||
|
||||
this.userSuggestions?.reset()
|
||||
this.userSuggestions = UserSuggestionState(accountVM)
|
||||
|
||||
this.emojiSuggestions?.reset()
|
||||
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
||||
}
|
||||
|
||||
fun editFromDraft(draft: Note) {
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent) { innerNote ->
|
||||
if (innerNote != null) {
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
if (oldTag != null) {
|
||||
draftTag.set(oldTag)
|
||||
}
|
||||
loadFromDraft(innerNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
|
||||
quote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
|
||||
forwardZapTo.value.addItem(quotedUser)
|
||||
}
|
||||
if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
|
||||
forwardZapTo.value.addItem(accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
val pos = forwardZapTo.value.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
|
||||
forwardZapTo.value.updatePercentage(pos, 0.9f)
|
||||
}
|
||||
}
|
||||
|
||||
if (!forwardZapTo.value.items.isEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
if (draftEvent !is ClassifiedsEvent) return
|
||||
|
||||
loadFromDraft(draftEvent)
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: ClassifiedsEvent) {
|
||||
val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
|
||||
forwardZapTo.value = SplitBuilder()
|
||||
localfowardZapTo.forEach {
|
||||
val user = LocalCache.getOrCreateUser(it[1])
|
||||
val value = it.last().toFloatOrNull() ?: 0f
|
||||
forwardZapTo.value.addItem(user, value)
|
||||
}
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
||||
|
||||
wantsToMarkAsSensitive = draftEvent.isSensitive()
|
||||
|
||||
val geohash = draftEvent.getGeoHash()
|
||||
wantsToAddGeoHash = geohash != null
|
||||
|
||||
val zapraiser = draftEvent.zapraiserAmount()
|
||||
wantsZapraiser = zapraiser != null
|
||||
zapRaiserAmount.value = null
|
||||
if (zapraiser != null) {
|
||||
zapRaiserAmount.value = zapraiser
|
||||
}
|
||||
|
||||
title = TextFieldValue(draftEvent.title() ?: "")
|
||||
price = TextFieldValue(draftEvent.price()?.amount ?: "")
|
||||
category = TextFieldValue(draftEvent.categories().firstOrNull() ?: "")
|
||||
locationText = TextFieldValue(draftEvent.location() ?: "")
|
||||
condition = draftEvent.conditionValid() ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
|
||||
val imageSet = draftEvent.images().toMutableSet()
|
||||
|
||||
draftEvent.imetas().forEach {
|
||||
if (it.url in imageSet) {
|
||||
productImages = productImages + ProductImageMeta.parse(it)
|
||||
imageSet.remove(it.url)
|
||||
} else {
|
||||
iMetaDescription.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
imageSet.forEach {
|
||||
productImages = productImages + ProductImageMeta(it)
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
|
||||
urlPreviews.update(message)
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
sendPostSync()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
val template = createTemplate() ?: return
|
||||
|
||||
accountViewModel?.account?.signAndSendPrivatelyOrBroadcast(
|
||||
template,
|
||||
relayList = { relayList },
|
||||
)
|
||||
|
||||
accountViewModel?.deleteDraft(draftTag.current)
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun sendDraft() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
sendDraftSync()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
if (message.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraft(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val accountViewModel = accountViewModel ?: return null
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val emojis = findEmoji(tagger.message, account?.myEmojis?.value)
|
||||
val urls = findURLs(tagger.message)
|
||||
val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() }
|
||||
|
||||
val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString()
|
||||
|
||||
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
|
||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
||||
|
||||
val quotes = findNostrUris(tagger.message)
|
||||
|
||||
val template =
|
||||
ClassifiedsEvent.build(
|
||||
title.text,
|
||||
PriceTag(price.text, "SATS", null),
|
||||
tagger.message,
|
||||
locationText.text.ifBlank { null },
|
||||
condition,
|
||||
) {
|
||||
productImages.forEach { image(it.url) }
|
||||
|
||||
hashtags(listOfNotNull(category.text.ifBlank { null }) + findHashtags(tagger.message))
|
||||
quotes(quotes)
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
localZapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
references(urls)
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
fun findEmoji(
|
||||
message: String,
|
||||
myEmojiSet: List<Account.EmojiMedia>?,
|
||||
): List<EmojiUrlTag> {
|
||||
if (myEmojiSet == null) return emptyList()
|
||||
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) }
|
||||
}
|
||||
}
|
||||
|
||||
fun upload(
|
||||
alt: String?,
|
||||
contentWarningReason: String?,
|
||||
mediaQuality: Int,
|
||||
server: ServerName,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val myAccount = account ?: return@launch
|
||||
|
||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||
|
||||
isUploadingImage = true
|
||||
|
||||
val results =
|
||||
myMultiOrchestrator.upload(
|
||||
viewModelScope,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
MediaCompressor.intToCompressorQuality(mediaQuality),
|
||||
server,
|
||||
myAccount,
|
||||
context,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
results.successful.forEach {
|
||||
if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
if (it.result.fileHeader.mimeType
|
||||
?.startsWith("image") == true
|
||||
) {
|
||||
productImages = productImages +
|
||||
ProductImageMeta(
|
||||
it.result.url,
|
||||
it.result.fileHeader.mimeType,
|
||||
it.result.fileHeader.blurHash
|
||||
?.blurhash,
|
||||
it.result.fileHeader.dim,
|
||||
alt,
|
||||
it.result.fileHeader.hash,
|
||||
it.result.fileHeader.size,
|
||||
)
|
||||
} else {
|
||||
iMetaDescription.add(it.result, alt, contentWarningReason)
|
||||
|
||||
message = message.insertUrlAtCursor(it.result.url)
|
||||
urlPreviews.update(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multiOrchestrator = null
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
}
|
||||
|
||||
isUploadingImage = false
|
||||
}
|
||||
}
|
||||
|
||||
open fun cancel() {
|
||||
message = TextFieldValue("")
|
||||
|
||||
multiOrchestrator = null
|
||||
isUploadingImage = false
|
||||
|
||||
wantsInvoice = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount.value = null
|
||||
|
||||
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
locationText = TextFieldValue("")
|
||||
title = TextFieldValue("")
|
||||
category = TextFieldValue("")
|
||||
price = TextFieldValue("")
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
wantsToAddGeoHash = false
|
||||
wantsSecretEmoji = false
|
||||
|
||||
forwardZapTo.value = SplitBuilder()
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
|
||||
urlPreviews.reset()
|
||||
|
||||
userSuggestions?.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
productImages = emptyList()
|
||||
iMetaDescription.reset()
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
reloadRelaySet()
|
||||
|
||||
draftTag.rotate()
|
||||
}
|
||||
|
||||
fun reloadRelaySet() {
|
||||
val account = accountViewModel?.account ?: return
|
||||
|
||||
val nip65 = account.normalizedNIP65WriteRelayList.value
|
||||
val private = account.normalizedPrivateOutBoxRelaySet.value
|
||||
val local = account.settings.localRelayServers
|
||||
|
||||
relayList =
|
||||
if (nip65.isEmpty()) {
|
||||
account.activeWriteRelays().map { it.url }.toImmutableList()
|
||||
} else {
|
||||
val combined: Set<String> = (nip65 + private + local)
|
||||
combined.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
override fun updateMessage(it: TextFieldValue) {
|
||||
message = it
|
||||
urlPreviews.update(message)
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
|
||||
emojiSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
|
||||
forwardZapToEditting.value = newZapForwardTo
|
||||
if (newZapForwardTo.selection.collapsed) {
|
||||
val lastWord = newZapForwardTo.text
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
userSuggestions.reset()
|
||||
}
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmoji(item: Account.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
|
||||
val wordToInsert = item.url.url + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaDescription.downloadAndPrepare(
|
||||
item.url.url,
|
||||
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) },
|
||||
)
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||
title.text.isNotBlank() &&
|
||||
price.text.isNotBlank() &&
|
||||
category.text.isNotBlank() &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
multiOrchestrator = MultiOrchestrator(uris)
|
||||
}
|
||||
|
||||
override fun locationFlow(): StateFlow<LocationState.LocationResult> {
|
||||
if (location == null) {
|
||||
location = locationManager().geohashStateFlow
|
||||
}
|
||||
|
||||
return location!!
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
override fun updateZapPercentage(
|
||||
index: Int,
|
||||
sliderValue: Float,
|
||||
) {
|
||||
forwardZapTo.value.updatePercentage(index, sliderValue)
|
||||
}
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
forwardZapTo.value.addItem(taggedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateZapRaiserAmount(newAmount: Long?) {
|
||||
zapRaiserAmount.value = newAmount
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun toggleMarkAsSensitive() {
|
||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateTitle(it: TextFieldValue) {
|
||||
title = it
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updatePrice(it: TextFieldValue) {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
price = it
|
||||
}
|
||||
}
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateCondition(newCondition: ConditionTag.CONDITION) {
|
||||
condition = newCondition
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateCategory(value: TextFieldValue) {
|
||||
category = value
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateLocation(it: TextFieldValue) {
|
||||
locationText = it
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
}
|
||||
+44
-6
@@ -18,12 +18,18 @@
|
||||
* 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.note.creators.products
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.marketplace
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -34,28 +40,62 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
fun SellProductPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
val postViewModel = NewProductViewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
ThemeComparisonColumn {
|
||||
SellProduct(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (!postViewModel.productImages.isEmpty()) {
|
||||
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
||||
items(postViewModel.productImages) {
|
||||
Box(SquaredQuoteBorderModifier) {
|
||||
AsyncImage(
|
||||
model = it.url,
|
||||
contentDescription = it.alt ?: it.url,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -233,9 +273,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
|
||||
}
|
||||
TextSpinner(
|
||||
placeholder =
|
||||
categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second
|
||||
?: "",
|
||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text }?.second ?: "",
|
||||
options = categoryOptions,
|
||||
onSelect = {
|
||||
postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
|
||||
+24
-7
@@ -73,12 +73,14 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
@@ -726,7 +728,19 @@ private fun RenderClassifiedsReaderForThread(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val images = remember(noteEvent) { noteEvent.images().toImmutableList() }
|
||||
val imageSet =
|
||||
noteEvent.imageMetas().ifEmpty { null }?.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = note.toNostrUri(),
|
||||
mimeType = it.mimeType,
|
||||
)
|
||||
}
|
||||
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary =
|
||||
remember(noteEvent) {
|
||||
@@ -742,11 +756,14 @@ private fun RenderClassifiedsReaderForThread(
|
||||
|
||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||
Column {
|
||||
if (images.isNotEmpty()) {
|
||||
Row {
|
||||
InlineCarrousel(
|
||||
images,
|
||||
images.first(),
|
||||
if (imageSet != null && imageSet.isNotEmpty()) {
|
||||
AutoNonlazyGrid(imageSet.size) {
|
||||
ZoomableContentView(
|
||||
content = imageSet[it],
|
||||
images = imageSet.toImmutableList(),
|
||||
roundedCorner = false,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -964,6 +964,7 @@
|
||||
<string name="new_post">New Post</string>
|
||||
<string name="new_short">New Shorts: images or videos</string>
|
||||
<string name="new_community_note">New Community Note</string>
|
||||
<string name="new_product">New Product</string>
|
||||
|
||||
<string name="open_all_reactions_to_this_post">Open all reactions to this post</string>
|
||||
<string name="close_all_reactions_to_this_post">Close all reactions to this post</string>
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class IMetaTag(
|
||||
val url: String,
|
||||
val properties: Map<String, List<String>>,
|
||||
val properties: Map<String, List<String>> = emptyMap(),
|
||||
) {
|
||||
fun toTagArray() =
|
||||
arrayOf(TAG_NAME, "$ANCHOR_PROPERTY $url") +
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.LocationTag
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag
|
||||
@@ -55,6 +56,8 @@ class ClassifiedsEvent(
|
||||
|
||||
fun condition() = tags.firstNotNullOfOrNull(ConditionTag::parse)
|
||||
|
||||
fun conditionValid() = tags.firstNotNullOfOrNull(ConditionTag::parseCondition)
|
||||
|
||||
fun images() = tags.mapNotNull(ImageTag::parse)
|
||||
|
||||
fun status() = tags.firstNotNullOfOrNull(StatusTag::parse)
|
||||
@@ -71,6 +74,22 @@ class ClassifiedsEvent(
|
||||
|
||||
fun isWellFormed() = tags.containsAllTagNamesWithValues(REQUIRED_FIELDS)
|
||||
|
||||
fun imageMetas(): List<ProductImageMeta> {
|
||||
val images = images()
|
||||
val imetas = imetas()
|
||||
|
||||
val imetaSet = imetas.associate { it.url to it }
|
||||
|
||||
return images.map {
|
||||
val imeta = imetaSet.get(it)
|
||||
if (imeta != null) {
|
||||
ProductImageMeta.parse(imeta)
|
||||
} else {
|
||||
ProductImageMeta(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 30402
|
||||
const val ALT_DESCRIPTION = "Classifieds listing"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.quartz.nip99Classifieds
|
||||
|
||||
import com.vitorpamplona.quartz.nip68Picture.alt
|
||||
import com.vitorpamplona.quartz.nip68Picture.blurhash
|
||||
import com.vitorpamplona.quartz.nip68Picture.dims
|
||||
import com.vitorpamplona.quartz.nip68Picture.hash
|
||||
import com.vitorpamplona.quartz.nip68Picture.mimeType
|
||||
import com.vitorpamplona.quartz.nip68Picture.size
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
|
||||
data class ProductImageMeta(
|
||||
val url: String,
|
||||
val mimeType: String? = null,
|
||||
val blurhash: String? = null,
|
||||
val dimension: DimensionTag? = null,
|
||||
val alt: String? = null,
|
||||
val hash: String? = null,
|
||||
val size: Int? = null,
|
||||
) {
|
||||
fun toIMeta(): IMetaTag =
|
||||
IMetaTagBuilder(url)
|
||||
.apply {
|
||||
mimeType?.let { mimeType(it) }
|
||||
alt?.let { alt(it) }
|
||||
hash?.let { hash(it) }
|
||||
size?.let { size(it) }
|
||||
dimension?.let { dims(it) }
|
||||
blurhash?.let { blurhash(it) }
|
||||
}.build()
|
||||
|
||||
fun toIMetaArray(): Array<String> = toIMeta().toTagArray()
|
||||
|
||||
companion object {
|
||||
fun parse(iMeta: IMetaTag): ProductImageMeta =
|
||||
ProductImageMeta(
|
||||
iMeta.url,
|
||||
iMeta.mimeType()?.firstOrNull(),
|
||||
iMeta.blurhash()?.firstOrNull(),
|
||||
iMeta.dims()?.firstOrNull()?.let { DimensionTag.parse(it) },
|
||||
iMeta.alt()?.firstOrNull(),
|
||||
iMeta.hash()?.firstOrNull(),
|
||||
iMeta.size()?.firstOrNull()?.toIntOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ fun TagArrayBuilder<ClassifiedsEvent>.summary(summary: String) = addUnique(Summa
|
||||
|
||||
fun TagArrayBuilder<ClassifiedsEvent>.location(location: String) = addUnique(LocationTag.assemble(location))
|
||||
|
||||
fun TagArrayBuilder<ClassifiedsEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
|
||||
fun TagArrayBuilder<ClassifiedsEvent>.image(imageUrl: String) = add(ImageTag.assemble(imageUrl))
|
||||
|
||||
fun TagArrayBuilder<ClassifiedsEvent>.images(imageUrls: List<String>) = addAll(imageUrls.map { ImageTag.assemble(it) })
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ConditionTag {
|
||||
enum class CONDITION(
|
||||
val value: String,
|
||||
val code: String,
|
||||
) {
|
||||
NEW("new"),
|
||||
USED_LIKE_NEW("like new"),
|
||||
@@ -34,6 +34,10 @@ class ConditionTag {
|
||||
;
|
||||
|
||||
fun toTagArray() = assemble(this)
|
||||
|
||||
companion object {
|
||||
fun parse(cond: String) = entries.firstOrNull { it.code == cond }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -48,6 +52,9 @@ class ConditionTag {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(condition: CONDITION) = arrayOf(TAG_NAME, condition.value)
|
||||
fun parseCondition(tag: Array<String>): CONDITION? = parse(tag)?.let { CONDITION.parse(it) }
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(condition: CONDITION) = arrayOf(TAG_NAME, condition.code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user