Support for Classifieds
This commit is contained in:
@@ -402,6 +402,25 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun consume(event: ClassifiedsEvent) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
version.moveAllReferencesTo(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun consume(event: PinListEvent) {
|
private fun consume(event: PinListEvent) {
|
||||||
val version = getOrCreateNote(event.id)
|
val version = getOrCreateNote(event.id)
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
@@ -1306,6 +1325,7 @@ object LocalCache {
|
|||||||
is ChannelMessageEvent -> consume(event, relay)
|
is ChannelMessageEvent -> consume(event, relay)
|
||||||
is ChannelMetadataEvent -> consume(event)
|
is ChannelMetadataEvent -> consume(event)
|
||||||
is ChannelMuteUserEvent -> consume(event)
|
is ChannelMuteUserEvent -> consume(event)
|
||||||
|
is ClassifiedsEvent -> consume(event)
|
||||||
is CommunityDefinitionEvent -> consume(event, relay)
|
is CommunityDefinitionEvent -> consume(event, relay)
|
||||||
is CommunityPostApprovalEvent -> {
|
is CommunityPostApprovalEvent -> {
|
||||||
event.containedPost()?.let {
|
event.containedPost()?.let {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||||
@@ -66,6 +67,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
TextNoteEvent.kind,
|
TextNoteEvent.kind,
|
||||||
RepostEvent.kind,
|
RepostEvent.kind,
|
||||||
GenericRepostEvent.kind,
|
GenericRepostEvent.kind,
|
||||||
|
ClassifiedsEvent.kind,
|
||||||
LongTextNoteEvent.kind,
|
LongTextNoteEvent.kind,
|
||||||
PollNoteEvent.kind,
|
PollNoteEvent.kind,
|
||||||
HighlightEvent.kind,
|
HighlightEvent.kind,
|
||||||
@@ -89,7 +91,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||||
tags = mapOf(
|
tags = mapOf(
|
||||||
"t" to hashToLoad.map {
|
"t" to hashToLoad.map {
|
||||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ClassifiedsEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||||
|
|
||||||
|
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||||
|
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
|
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 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))
|
||||||
|
}
|
||||||
|
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
|
||||||
|
|
||||||
|
fun publishedAt() = try {
|
||||||
|
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 30402
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
dTag: String,
|
||||||
|
title: String?,
|
||||||
|
image: String?,
|
||||||
|
summary: String?,
|
||||||
|
price: Price?,
|
||||||
|
location: String?,
|
||||||
|
publishedAt: Long?,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = TimeUtils.now()
|
||||||
|
): ClassifiedsEvent {
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
|
||||||
|
tags.add(listOf("d", dTag))
|
||||||
|
title?.let { tags.add(listOf("title", it)) }
|
||||||
|
image?.let { tags.add(listOf("image", it)) }
|
||||||
|
summary?.let { tags.add(listOf("summary", it)) }
|
||||||
|
price?.let {
|
||||||
|
if (it.frequency != null && it.currency != null) {
|
||||||
|
tags.add(listOf("price", it.amount, it.currency, it.frequency))
|
||||||
|
} else if (it.currency != null) {
|
||||||
|
tags.add(listOf("price", it.amount, it.currency))
|
||||||
|
} else {
|
||||||
|
tags.add(listOf("price", it.amount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
location?.let { tags.add(listOf("location", it)) }
|
||||||
|
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
|
||||||
|
title?.let { tags.add(listOf("title", it)) }
|
||||||
|
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Price(val amount: String, val currency: String?, val frequency: String?)
|
||||||
@@ -267,6 +267,7 @@ open class Event(
|
|||||||
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
@@ -42,7 +43,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it ->
|
.filter { it ->
|
||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
(noteEvent is TextNoteEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
||||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) &&
|
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true &&
|
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true &&
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||||
@@ -169,6 +170,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||||
@@ -840,7 +842,8 @@ fun ClickableNote(
|
|||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val redirectToNote = if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
val redirectToNote =
|
||||||
|
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||||
baseNote.replyTo?.lastOrNull() ?: baseNote
|
baseNote.replyTo?.lastOrNull() ?: baseNote
|
||||||
} else {
|
} else {
|
||||||
baseNote
|
baseNote
|
||||||
@@ -1003,7 +1006,8 @@ private fun RenderNoteRow(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
when (baseNote.event) {
|
val noteEvent = baseNote.event
|
||||||
|
when (noteEvent) {
|
||||||
is AppDefinitionEvent -> {
|
is AppDefinitionEvent -> {
|
||||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@@ -1067,6 +1071,14 @@ private fun RenderNoteRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ClassifiedsEvent -> {
|
||||||
|
RenderClassifieds(
|
||||||
|
noteEvent,
|
||||||
|
baseNote,
|
||||||
|
accountViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is HighlightEvent -> {
|
is HighlightEvent -> {
|
||||||
RenderHighlight(
|
RenderHighlight(
|
||||||
baseNote,
|
baseNote,
|
||||||
@@ -3447,6 +3459,106 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountViewModel: AccountViewModel) {
|
||||||
|
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() }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape = QuoteBorder)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.subtleBorder,
|
||||||
|
QuoteBorder
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row() {
|
||||||
|
image?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.preview_card_image_for,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
} ?: CreateImageHeader(note, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
price?.let {
|
||||||
|
val priceTag = remember(noteEvent) {
|
||||||
|
if (price.frequency != null && price.currency != null) {
|
||||||
|
"${price.amount} ${price.currency}/${price.frequency}"
|
||||||
|
} else if (price.currency != null) {
|
||||||
|
"${price.amount} ${price.currency}"
|
||||||
|
} else {
|
||||||
|
price.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = priceTag,
|
||||||
|
maxLines = 1,
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(start = 5.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary != null || location != null) {
|
||||||
|
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
location?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(start = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateImageHeader(
|
fun CreateImageHeader(
|
||||||
note: Note,
|
note: Note,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -40,6 +41,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -48,6 +50,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.em
|
import androidx.compose.ui.unit.em
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -59,6 +62,7 @@ import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||||
@@ -89,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
|||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||||
@@ -352,40 +357,9 @@ fun NoteMaster(
|
|||||||
if (noteEvent is BadgeDefinitionEvent) {
|
if (noteEvent is BadgeDefinitionEvent) {
|
||||||
BadgeDisplay(baseNote = note)
|
BadgeDisplay(baseNote = note)
|
||||||
} else if (noteEvent is LongTextNoteEvent) {
|
} else if (noteEvent is LongTextNoteEvent) {
|
||||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
RenderLongFormHeaderForThread(noteEvent)
|
||||||
Column {
|
} else if (noteEvent is ClassifiedsEvent) {
|
||||||
noteEvent.image()?.let {
|
RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel)
|
||||||
AsyncImage(
|
|
||||||
model = it,
|
|
||||||
contentDescription = stringResource(
|
|
||||||
R.string.preview_card_image_for,
|
|
||||||
it
|
|
||||||
),
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
noteEvent.title()?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
fontSize = 28.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
noteEvent.summary()?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -492,3 +466,142 @@ fun NoteMaster(
|
|||||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderClassifiedsReaderForThread(
|
||||||
|
noteEvent: ClassifiedsEvent,
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel
|
||||||
|
) {
|
||||||
|
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() }
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
} ?: CreateImageHeader(note, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
price?.let {
|
||||||
|
val priceTag = remember(noteEvent) {
|
||||||
|
if (price.frequency != null && price.currency != null) {
|
||||||
|
"${price.amount} ${price.currency}/${price.frequency}"
|
||||||
|
} else if (price.currency != null) {
|
||||||
|
"${price.amount} ${price.currency}"
|
||||||
|
} else {
|
||||||
|
price.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = priceTag,
|
||||||
|
maxLines = 1,
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(start = 5.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary?.let {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = Color.Gray,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location?.let {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) {
|
||||||
|
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||||
|
Column {
|
||||||
|
noteEvent.image()?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.preview_card_image_for,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
noteEvent.title()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
noteEvent.summary()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user