kopia lustrzana https://github.com/vitorpamplona/amethyst
1. Adds Classified creation from Amethyst.
2. Adds relay information for Replaceable events.pull/714/head
rodzic
8f1fbe10e9
commit
f458f00edc
|
@ -3,30 +3,39 @@
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
|
@ -78,7 +78,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||||
- [x] Moderated Communities (NIP-72)
|
- [x] Moderated Communities (NIP-72)
|
||||||
- [x] Emoji Packs (Kind:30030)
|
- [x] Emoji Packs (Kind:30030)
|
||||||
- [x] Personal Emoji Lists (Kind:10030)
|
- [x] Personal Emoji Lists (Kind:10030)
|
||||||
- [x] Classifieds (Kind:30403)
|
- [x] Classifieds (Kind:30402)
|
||||||
- [x] Private Messages and Small Groups (NIP-24)
|
- [x] Private Messages and Small Groups (NIP-24)
|
||||||
- [x] Gift Wraps & Seals (NIP-59)
|
- [x] Gift Wraps & Seals (NIP-59)
|
||||||
- [x] Versioned Encrypted Payloads (NIP-44)
|
- [x] Versioned Encrypted Payloads (NIP-44)
|
||||||
|
|
|
@ -4,6 +4,8 @@ import android.content.res.Resources
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
|
@ -30,6 +32,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.Contact
|
import com.vitorpamplona.quartz.events.Contact
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||||
|
@ -55,6 +58,7 @@ import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.NIP24Factory
|
import com.vitorpamplona.quartz.events.NIP24Factory
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.Price
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||||
|
@ -91,6 +95,7 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
val DefaultChannels = setOf(
|
val DefaultChannels = setOf(
|
||||||
|
@ -447,7 +452,7 @@ class Account(
|
||||||
val contactList = userProfile().latestContactList
|
val contactList = userProfile().latestContactList
|
||||||
|
|
||||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||||
val event = ContactListEvent.updateRelayList(
|
ContactListEvent.updateRelayList(
|
||||||
earlierVersion = contactList,
|
earlierVersion = contactList,
|
||||||
relayUse = relays,
|
relayUse = relays,
|
||||||
signer = signer
|
signer = signer
|
||||||
|
@ -456,7 +461,7 @@ class Account(
|
||||||
LocalCache.justConsume(it, null)
|
LocalCache.justConsume(it, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val event = ContactListEvent.createFromScratch(
|
ContactListEvent.createFromScratch(
|
||||||
followUsers = listOf(),
|
followUsers = listOf(),
|
||||||
followTags = listOf(),
|
followTags = listOf(),
|
||||||
followGeohashes = listOf(),
|
followGeohashes = listOf(),
|
||||||
|
@ -1025,6 +1030,64 @@ class Account(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendClassifieds(
|
||||||
|
title: String,
|
||||||
|
price: Price,
|
||||||
|
condition: ClassifiedsEvent.CONDITION,
|
||||||
|
location: String,
|
||||||
|
category: String,
|
||||||
|
message: String,
|
||||||
|
replyTo: List<Note>?,
|
||||||
|
mentions: List<User>?,
|
||||||
|
directMentions: Set<HexKey>,
|
||||||
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
|
wantsToMarkAsSensitive: Boolean,
|
||||||
|
zapRaiserAmount: Long? = null,
|
||||||
|
relayList: List<Relay>? = null,
|
||||||
|
geohash: String? = null
|
||||||
|
) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
|
||||||
|
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||||
|
val addresses = replyTo?.mapNotNull { it.address() }
|
||||||
|
|
||||||
|
ClassifiedsEvent.create(
|
||||||
|
dTag = UUID.randomUUID().toString(),
|
||||||
|
title = title,
|
||||||
|
price = price,
|
||||||
|
condition = condition,
|
||||||
|
summary = message,
|
||||||
|
image = null,
|
||||||
|
location = location,
|
||||||
|
category = category,
|
||||||
|
message = message,
|
||||||
|
replyTos = repliesToHex,
|
||||||
|
mentions = mentionsHex,
|
||||||
|
addresses = addresses,
|
||||||
|
zapReceiver = zapReceiver,
|
||||||
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
|
directMentions = directMentions,
|
||||||
|
geohash = geohash,
|
||||||
|
signer = signer
|
||||||
|
) {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
|
||||||
|
replyTo?.forEach {
|
||||||
|
it.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sendPost(
|
fun sendPost(
|
||||||
message: String,
|
message: String,
|
||||||
replyTo: List<Note>?,
|
replyTo: List<Note>?,
|
||||||
|
@ -1500,7 +1563,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MuteListEvent.createListWithWord(
|
MuteListEvent.createListWithWord(
|
||||||
|
@ -1509,7 +1572,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1525,7 +1588,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1539,7 +1602,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1555,7 +1618,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MuteListEvent.createListWithUser(
|
MuteListEvent.createListWithUser(
|
||||||
|
@ -1564,7 +1627,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1580,7 +1643,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1594,7 +1657,7 @@ class Account(
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.consume(it)
|
LocalCache.consume(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -276,7 +276,7 @@ object LocalCache {
|
||||||
// avoids processing empty contact lists.
|
// avoids processing empty contact lists.
|
||||||
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
|
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
|
||||||
user.updateContactList(event)
|
user.updateContactList(event)
|
||||||
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
// Log.d("CL", "Consumed contact list ${user.toNostrUri()} ${event.relays()?.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,16 +426,16 @@ object LocalCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: MuteListEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: MuteListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
fun consume(event: PeopleListEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: PeopleListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: AdvertisedRelayListEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: AdvertisedRelayListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event) }
|
private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
fun consume(event: EmojiPackSelectionEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: EmojiPackSelectionEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: EmojiPackEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: EmojiPackEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: ClassifiedsEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: ClassifiedsEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: PinListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: RelaySetEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: AudioTrackEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
fun consume(event: StatusEvent, relay: Relay?) {
|
fun consume(event: StatusEvent, relay: Relay?) {
|
||||||
val version = getOrCreateNote(event.id)
|
val version = getOrCreateNote(event.id)
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
@ -458,7 +458,7 @@ object LocalCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: BadgeDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
|
|
||||||
fun consume(event: BadgeProfilesEvent) {
|
fun consume(event: BadgeProfilesEvent) {
|
||||||
val version = getOrCreateNote(event.id)
|
val version = getOrCreateNote(event.id)
|
||||||
|
@ -504,14 +504,14 @@ object LocalCache {
|
||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun comsume(event: NNSEvent) { consumeBaseReplaceable(event) }
|
private fun comsume(event: NNSEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
fun consume(event: AppDefinitionEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: AppDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: CalendarEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: CalendarEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: CalendarDateSlotEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: CalendarDateSlotEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: CalendarTimeSlotEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: CalendarTimeSlotEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
private fun consume(event: CalendarRSVPEvent) { consumeBaseReplaceable(event) }
|
private fun consume(event: CalendarRSVPEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
|
|
||||||
private fun consumeBaseReplaceable(event: BaseAddressableEvent) {
|
private fun consumeBaseReplaceable(event: BaseAddressableEvent, relay: Relay?) {
|
||||||
val version = getOrCreateNote(event.id)
|
val version = getOrCreateNote(event.id)
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
@ -521,6 +521,11 @@ object LocalCache {
|
||||||
version.moveAllReferencesTo(note)
|
version.moveAllReferencesTo(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event?.id() == event.id()) return
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
@ -531,7 +536,7 @@ object LocalCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: AppRecommendationEvent) { consumeBaseReplaceable(event) }
|
fun consume(event: AppRecommendationEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun consume(event: RecommendRelayEvent) {
|
fun consume(event: RecommendRelayEvent) {
|
||||||
|
@ -1511,26 +1516,26 @@ object LocalCache {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AdvertisedRelayListEvent -> consume(event)
|
is AdvertisedRelayListEvent -> consume(event, relay)
|
||||||
is AppDefinitionEvent -> consume(event)
|
is AppDefinitionEvent -> consume(event, relay)
|
||||||
is AppRecommendationEvent -> consume(event)
|
is AppRecommendationEvent -> consume(event, relay)
|
||||||
is AudioHeaderEvent -> consume(event, relay)
|
is AudioHeaderEvent -> consume(event, relay)
|
||||||
is AudioTrackEvent -> consume(event)
|
is AudioTrackEvent -> consume(event, relay)
|
||||||
is BadgeAwardEvent -> consume(event)
|
is BadgeAwardEvent -> consume(event)
|
||||||
is BadgeDefinitionEvent -> consume(event)
|
is BadgeDefinitionEvent -> consume(event, relay)
|
||||||
is BadgeProfilesEvent -> consume(event)
|
is BadgeProfilesEvent -> consume(event)
|
||||||
is BookmarkListEvent -> consume(event)
|
is BookmarkListEvent -> consume(event)
|
||||||
is CalendarEvent -> consume(event)
|
is CalendarEvent -> consume(event, relay)
|
||||||
is CalendarDateSlotEvent -> consume(event)
|
is CalendarDateSlotEvent -> consume(event, relay)
|
||||||
is CalendarTimeSlotEvent -> consume(event)
|
is CalendarTimeSlotEvent -> consume(event, relay)
|
||||||
is CalendarRSVPEvent -> consume(event)
|
is CalendarRSVPEvent -> consume(event, relay)
|
||||||
is ChannelCreateEvent -> consume(event)
|
is ChannelCreateEvent -> consume(event)
|
||||||
is ChannelHideMessageEvent -> consume(event)
|
is ChannelHideMessageEvent -> consume(event)
|
||||||
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 ChatMessageEvent -> consume(event, relay)
|
is ChatMessageEvent -> consume(event, relay)
|
||||||
is ClassifiedsEvent -> consume(event)
|
is ClassifiedsEvent -> consume(event, relay)
|
||||||
is CommunityDefinitionEvent -> consume(event, relay)
|
is CommunityDefinitionEvent -> consume(event, relay)
|
||||||
is CommunityPostApprovalEvent -> {
|
is CommunityPostApprovalEvent -> {
|
||||||
event.containedPost()?.let {
|
event.containedPost()?.let {
|
||||||
|
@ -1540,8 +1545,8 @@ object LocalCache {
|
||||||
}
|
}
|
||||||
is ContactListEvent -> consume(event)
|
is ContactListEvent -> consume(event)
|
||||||
is DeletionEvent -> consume(event)
|
is DeletionEvent -> consume(event)
|
||||||
is EmojiPackEvent -> consume(event)
|
is EmojiPackEvent -> consume(event, relay)
|
||||||
is EmojiPackSelectionEvent -> consume(event)
|
is EmojiPackSelectionEvent -> consume(event, relay)
|
||||||
is SealedGossipEvent -> consume(event, relay)
|
is SealedGossipEvent -> consume(event, relay)
|
||||||
|
|
||||||
is FileHeaderEvent -> consume(event, relay)
|
is FileHeaderEvent -> consume(event, relay)
|
||||||
|
@ -1563,15 +1568,15 @@ object LocalCache {
|
||||||
is LnZapPaymentResponseEvent -> consume(event)
|
is LnZapPaymentResponseEvent -> consume(event)
|
||||||
is LongTextNoteEvent -> consume(event, relay)
|
is LongTextNoteEvent -> consume(event, relay)
|
||||||
is MetadataEvent -> consume(event)
|
is MetadataEvent -> consume(event)
|
||||||
is MuteListEvent -> consume(event)
|
is MuteListEvent -> consume(event, relay)
|
||||||
is NNSEvent -> comsume(event)
|
is NNSEvent -> comsume(event, relay)
|
||||||
is PrivateDmEvent -> consume(event, relay)
|
is PrivateDmEvent -> consume(event, relay)
|
||||||
is PinListEvent -> consume(event)
|
is PinListEvent -> consume(event, relay)
|
||||||
is PeopleListEvent -> consume(event)
|
is PeopleListEvent -> consume(event, relay)
|
||||||
is PollNoteEvent -> consume(event, relay)
|
is PollNoteEvent -> consume(event, relay)
|
||||||
is ReactionEvent -> consume(event)
|
is ReactionEvent -> consume(event)
|
||||||
is RecommendRelayEvent -> consume(event)
|
is RecommendRelayEvent -> consume(event)
|
||||||
is RelaySetEvent -> consume(event)
|
is RelaySetEvent -> consume(event, relay)
|
||||||
is ReportEvent -> consume(event, relay)
|
is ReportEvent -> consume(event, relay)
|
||||||
is RepostEvent -> {
|
is RepostEvent -> {
|
||||||
event.containedPost()?.let {
|
event.containedPost()?.let {
|
||||||
|
|
|
@ -42,6 +42,7 @@ import androidx.compose.material.icons.filled.Bolt
|
||||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||||
import androidx.compose.material.icons.filled.LocationOff
|
import androidx.compose.material.icons.filled.LocationOff
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material.icons.filled.Sell
|
||||||
import androidx.compose.material.icons.filled.ShowChart
|
import androidx.compose.material.icons.filled.ShowChart
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
@ -93,6 +94,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
@ -152,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||||
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
@ -345,6 +348,15 @@ fun NewPostView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (postViewModel.wantsProduct) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||||
|
) {
|
||||||
|
SellProduct(postViewModel = postViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MessageField(postViewModel)
|
MessageField(postViewModel)
|
||||||
|
|
||||||
if (postViewModel.wantsPoll) {
|
if (postViewModel.wantsPoll) {
|
||||||
|
@ -530,6 +542,16 @@ fun NewPostView(
|
||||||
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||||
AddPollButton(postViewModel.wantsPoll) {
|
AddPollButton(postViewModel.wantsPoll) {
|
||||||
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
||||||
|
if (postViewModel.wantsPoll) {
|
||||||
|
postViewModel.wantsProduct = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddClassifiedsButton(postViewModel) {
|
||||||
|
postViewModel.wantsProduct = !postViewModel.wantsProduct
|
||||||
|
if (postViewModel.wantsProduct) {
|
||||||
|
postViewModel.wantsPoll = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,7 +657,11 @@ private fun MessageField(
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.what_s_on_your_mind),
|
text = if (postViewModel.wantsProduct) {
|
||||||
|
stringResource(R.string.description)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.what_s_on_your_mind)
|
||||||
|
},
|
||||||
color = MaterialTheme.colorScheme.placeholderText
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -776,6 +802,227 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SellProduct(postViewModel: NewPostViewModel) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_title),
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
|
||||||
|
MyTextField(
|
||||||
|
value = postViewModel.title,
|
||||||
|
onValueChange = {
|
||||||
|
postViewModel.title = it
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_title_placeholder),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
|
)
|
||||||
|
},
|
||||||
|
visualTransformation = UrlUserTagTransformation(
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_price),
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
|
||||||
|
MyTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.price,
|
||||||
|
onValueChange = {
|
||||||
|
runCatching {
|
||||||
|
if (it.text.isEmpty()) {
|
||||||
|
postViewModel.price = TextFieldValue("")
|
||||||
|
} else if (it.text.toLongOrNull() != null) {
|
||||||
|
postViewModel.price = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "1000",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
keyboardType = KeyboardType.Number
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_condition),
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
|
||||||
|
val conditionTypes = listOf(
|
||||||
|
Triple(ClassifiedsEvent.CONDITION.NEW, stringResource(id = R.string.classifieds_condition_new), stringResource(id = R.string.classifieds_condition_new_explainer)),
|
||||||
|
Triple(ClassifiedsEvent.CONDITION.USED_LIKE_NEW, stringResource(id = R.string.classifieds_condition_like_new), stringResource(id = R.string.classifieds_condition_like_new_explainer)),
|
||||||
|
Triple(ClassifiedsEvent.CONDITION.USED_GOOD, stringResource(id = R.string.classifieds_condition_good), stringResource(id = R.string.classifieds_condition_good_explainer)),
|
||||||
|
Triple(ClassifiedsEvent.CONDITION.USED_FAIR, stringResource(id = R.string.classifieds_condition_fair), stringResource(id = R.string.classifieds_condition_fair_explainer))
|
||||||
|
)
|
||||||
|
|
||||||
|
val conditionOptions = remember { conditionTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||||
|
|
||||||
|
TextSpinner(
|
||||||
|
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
|
||||||
|
options = conditionOptions,
|
||||||
|
onSelect = {
|
||||||
|
postViewModel.condition = conditionTypes[it].first
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 5.dp, bottom = 1.dp)
|
||||||
|
) { currentOption, modifier ->
|
||||||
|
MyTextField(
|
||||||
|
value = TextFieldValue(currentOption),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
modifier = modifier,
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_category),
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
|
||||||
|
val categoryList = listOf(
|
||||||
|
R.string.classifieds_category_clothing,
|
||||||
|
R.string.classifieds_category_accessories,
|
||||||
|
R.string.classifieds_category_electronics,
|
||||||
|
R.string.classifieds_category_furniture,
|
||||||
|
R.string.classifieds_category_collectibles,
|
||||||
|
R.string.classifieds_category_books,
|
||||||
|
R.string.classifieds_category_pets,
|
||||||
|
R.string.classifieds_category_sports,
|
||||||
|
R.string.classifieds_category_fitness,
|
||||||
|
R.string.classifieds_category_art,
|
||||||
|
R.string.classifieds_category_crafts,
|
||||||
|
R.string.classifieds_category_home,
|
||||||
|
R.string.classifieds_category_office,
|
||||||
|
R.string.classifieds_category_food,
|
||||||
|
R.string.classifieds_category_misc,
|
||||||
|
R.string.classifieds_category_other
|
||||||
|
)
|
||||||
|
|
||||||
|
val categoryTypes = categoryList.map {
|
||||||
|
Triple(it, stringResource(id = it), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val categoryOptions = remember { categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList() }
|
||||||
|
TextSpinner(
|
||||||
|
placeholder = categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second ?: "",
|
||||||
|
options = categoryOptions,
|
||||||
|
onSelect = {
|
||||||
|
postViewModel.category = TextFieldValue(categoryTypes[it].second)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 5.dp, bottom = 1.dp)
|
||||||
|
) { currentOption, modifier ->
|
||||||
|
MyTextField(
|
||||||
|
value = TextFieldValue(currentOption),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
modifier = modifier,
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_location),
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.W500
|
||||||
|
)
|
||||||
|
|
||||||
|
MyTextField(
|
||||||
|
value = postViewModel.locationText,
|
||||||
|
onValueChange = {
|
||||||
|
postViewModel.locationText = it
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.classifieds_location_placeholder),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
|
)
|
||||||
|
},
|
||||||
|
visualTransformation = UrlUserTagTransformation(
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
|
fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -1213,6 +1460,36 @@ private fun ForwardZapTo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddClassifiedsButton(
|
||||||
|
postViewModel: NewPostViewModel,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!postViewModel.wantsProduct) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Sell,
|
||||||
|
contentDescription = stringResource(R.string.classifieds),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Sell,
|
||||||
|
contentDescription = stringResource(id = R.string.classifieds),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp),
|
||||||
|
tint = BitcoinOrange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MarkAsSensitive(
|
private fun MarkAsSensitive(
|
||||||
postViewModel: NewPostViewModel,
|
postViewModel: NewPostViewModel,
|
||||||
|
|
|
@ -33,7 +33,9 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.Price
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||||
|
@ -94,6 +96,14 @@ open class NewPostViewModel() : ViewModel() {
|
||||||
var isValidConsensusThreshold = mutableStateOf(true)
|
var isValidConsensusThreshold = mutableStateOf(true)
|
||||||
var isValidClosedAt = 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<ClassifiedsEvent.CONDITION>(ClassifiedsEvent.CONDITION.USED_LIKE_NEW)
|
||||||
|
|
||||||
// Invoices
|
// Invoices
|
||||||
var canAddInvoice by mutableStateOf(false)
|
var canAddInvoice by mutableStateOf(false)
|
||||||
var wantsInvoice by mutableStateOf(false)
|
var wantsInvoice by mutableStateOf(false)
|
||||||
|
@ -273,6 +283,23 @@ open class NewPostViewModel() : ViewModel() {
|
||||||
relayList,
|
relayList,
|
||||||
geoHash
|
geoHash
|
||||||
)
|
)
|
||||||
|
} else if (wantsProduct) {
|
||||||
|
account?.sendClassifieds(
|
||||||
|
title = title.text,
|
||||||
|
price = Price(price.text, "SATS", null),
|
||||||
|
condition = condition,
|
||||||
|
message = tagger.message,
|
||||||
|
replyTo = tagger.replyTos,
|
||||||
|
mentions = tagger.mentions,
|
||||||
|
location = locationText.text,
|
||||||
|
category = category.text,
|
||||||
|
directMentions = tagger.directMentions,
|
||||||
|
zapReceiver = zapReceiver,
|
||||||
|
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||||
|
zapRaiserAmount = localZapRaiserAmount,
|
||||||
|
relayList = relayList,
|
||||||
|
geohash = geoHash
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// adds markers
|
// adds markers
|
||||||
val rootId =
|
val rootId =
|
||||||
|
@ -338,6 +365,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
|
Log.e("ImageUploader", "Failed to upload the image / video", it)
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
@ -381,6 +409,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||||
wantsZapraiser = false
|
wantsZapraiser = false
|
||||||
zapRaiserAmount = null
|
zapRaiserAmount = null
|
||||||
|
|
||||||
|
wantsProduct = false
|
||||||
|
condition = ClassifiedsEvent.CONDITION.USED_LIKE_NEW
|
||||||
|
price = TextFieldValue("")
|
||||||
|
|
||||||
wantsForwardZapTo = false
|
wantsForwardZapTo = false
|
||||||
wantsToMarkAsSensitive = false
|
wantsToMarkAsSensitive = false
|
||||||
wantsToAddGeoHash = false
|
wantsToAddGeoHash = false
|
||||||
|
@ -527,6 +559,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||||
(!wantsDirectMessage || !toUsers.text.isNullOrBlank()) &&
|
(!wantsDirectMessage || !toUsers.text.isNullOrBlank()) &&
|
||||||
(!wantsPoll || (pollOptions.values.all { it.isNotEmpty() } && isValidvalueMinimum.value && isValidvalueMaximum.value)) &&
|
(!wantsPoll || (pollOptions.values.all { it.isNotEmpty() } && isValidvalueMinimum.value && isValidvalueMaximum.value)) &&
|
||||||
|
(!wantsProduct || (!title.text.isNullOrBlank() && !price.text.isNullOrBlank() && !category.text.isNullOrBlank())) &&
|
||||||
contentToAddUrl == null
|
contentToAddUrl == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
@ -43,6 +44,30 @@ fun TextSpinner(
|
||||||
options: ImmutableList<TitleExplainer>,
|
options: ImmutableList<TitleExplainer>,
|
||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
TextSpinner(
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
onSelect,
|
||||||
|
modifier
|
||||||
|
) { currentOption, modifier ->
|
||||||
|
OutlinedTextField(
|
||||||
|
value = currentOption,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { label?.let { Text(it) } },
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextSpinner(
|
||||||
|
placeholder: String,
|
||||||
|
options: ImmutableList<TitleExplainer>,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
mainElement: @Composable (currentOption: String, modifier: Modifier) -> Unit
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
@ -50,16 +75,16 @@ fun TextSpinner(
|
||||||
var currentText by remember { mutableStateOf(placeholder) }
|
var currentText by remember { mutableStateOf(placeholder) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
mainElement(
|
||||||
value = currentText,
|
currentText,
|
||||||
onValueChange = {},
|
remember {
|
||||||
readOnly = true,
|
Modifier
|
||||||
label = { label?.let { Text(it) } },
|
.fillMaxWidth()
|
||||||
modifier = Modifier
|
.focusRequester(focusRequester)
|
||||||
.fillMaxWidth()
|
}
|
||||||
.focusRequester(focusRequester)
|
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -673,4 +673,44 @@
|
||||||
<string name="send_the_seller_a_message">Send the seller a message</string>
|
<string name="send_the_seller_a_message">Send the seller a message</string>
|
||||||
<string name="hi_seller_is_this_still_available">Hi %1$s, is this still available?</string>
|
<string name="hi_seller_is_this_still_available">Hi %1$s, is this still available?</string>
|
||||||
<string name="hi_there_is_this_still_available">Hi there, is this still available?</string>
|
<string name="hi_there_is_this_still_available">Hi there, is this still available?</string>
|
||||||
|
|
||||||
|
<string name="classifieds">Sell an Item</string>
|
||||||
|
|
||||||
|
<string name="classifieds_title">Title</string>
|
||||||
|
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||||
|
<string name="classifieds_condition">Condition</string>
|
||||||
|
<string name="classifieds_category">Category</string>
|
||||||
|
<string name="classifieds_price">Price (in Sats)</string>
|
||||||
|
<string name="classifieds_price_placeholder">1000</string>
|
||||||
|
<string name="classifieds_location">Location</string>
|
||||||
|
<string name="classifieds_location_placeholder">City, State, Country</string>
|
||||||
|
|
||||||
|
<string name="classifieds_condition_new">New</string>
|
||||||
|
<string name="classifieds_condition_new_explainer">It\'s a brand new unit, in the original box</string>
|
||||||
|
|
||||||
|
<string name="classifieds_condition_like_new">Like New</string>
|
||||||
|
<string name="classifieds_condition_like_new_explainer">It\'s used, but there are no signs of usage</string>
|
||||||
|
|
||||||
|
<string name="classifieds_condition_good">Good</string>
|
||||||
|
<string name="classifieds_condition_good_explainer">It has some superficial usage marks</string>
|
||||||
|
|
||||||
|
<string name="classifieds_condition_fair">Fair</string>
|
||||||
|
<string name="classifieds_condition_fair_explainer">It is still in acceptable and functional shape</string>
|
||||||
|
|
||||||
|
<string name="classifieds_category_clothing">Clothing</string>
|
||||||
|
<string name="classifieds_category_accessories">Accessories</string>
|
||||||
|
<string name="classifieds_category_electronics">Electronics</string>
|
||||||
|
<string name="classifieds_category_furniture">Furniture</string>
|
||||||
|
<string name="classifieds_category_collectibles">Collectibles</string>
|
||||||
|
<string name="classifieds_category_books">Books</string>
|
||||||
|
<string name="classifieds_category_pets">Pets</string>
|
||||||
|
<string name="classifieds_category_sports">Sports</string>
|
||||||
|
<string name="classifieds_category_fitness">Fitness</string>
|
||||||
|
<string name="classifieds_category_art">Art</string>
|
||||||
|
<string name="classifieds_category_crafts">Crafts</string>
|
||||||
|
<string name="classifieds_category_home">Home</string>
|
||||||
|
<string name="classifieds_category_office">Office</string>
|
||||||
|
<string name="classifieds_category_food">Food</string>
|
||||||
|
<string name="classifieds_category_misc">Miscellaneous</string>
|
||||||
|
<string name="classifieds_category_other">Other</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -19,6 +19,7 @@ class ClassifiedsEvent(
|
||||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||||
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||||
|
fun condition() = tags.firstOrNull { it.size > 1 && it[0] == "condition" }?.get(1)
|
||||||
fun images() = tags.filter { it.size > 1 && it[0] == "image" }.map { it[1] }
|
fun images() = tags.filter { it.size > 1 && it[0] == "image" }.map { it[1] }
|
||||||
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
||||||
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
|
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
|
||||||
|
@ -32,23 +33,65 @@ class ClassifiedsEvent(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class CONDITION(val value: String){
|
||||||
|
NEW("new"),
|
||||||
|
USED_LIKE_NEW("like new"),
|
||||||
|
USED_GOOD("good"),
|
||||||
|
USED_FAIR("fair"),
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30402
|
const val kind = 30402
|
||||||
|
private val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
dTag: String,
|
dTag: String,
|
||||||
title: String?,
|
title: String?,
|
||||||
image: String?,
|
image: String?,
|
||||||
summary: String?,
|
summary: String?,
|
||||||
|
message: String,
|
||||||
price: Price?,
|
price: Price?,
|
||||||
location: String?,
|
location: String?,
|
||||||
publishedAt: Long?,
|
category: String?,
|
||||||
|
condition: ClassifiedsEvent.CONDITION?,
|
||||||
|
publishedAt: Long? = TimeUtils.now(),
|
||||||
|
replyTos: List<String>?,
|
||||||
|
addresses: List<ATag>?,
|
||||||
|
mentions: List<String>?,
|
||||||
|
directMentions: Set<HexKey>,
|
||||||
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
|
markAsSensitive: Boolean,
|
||||||
|
zapRaiserAmount: Long?,
|
||||||
|
geohash: String? = null,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (ClassifiedsEvent) -> Unit
|
onReady: (ClassifiedsEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val tags = mutableListOf<Array<String>>()
|
val tags = mutableListOf<Array<String>>()
|
||||||
|
|
||||||
|
replyTos?.forEach {
|
||||||
|
if (it in directMentions) {
|
||||||
|
tags.add(arrayOf("e", it, "", "mention"))
|
||||||
|
} else {
|
||||||
|
tags.add(arrayOf("e", it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mentions?.forEach {
|
||||||
|
if (it in directMentions) {
|
||||||
|
tags.add(arrayOf("p", it, "", "mention"))
|
||||||
|
} else {
|
||||||
|
tags.add(arrayOf("p", it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
val aTag = it.toTag()
|
||||||
|
if (aTag in directMentions) {
|
||||||
|
tags.add(arrayOf("a", aTag, "", "mention"))
|
||||||
|
} else {
|
||||||
|
tags.add(arrayOf("a", aTag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tags.add(arrayOf("d", dTag))
|
tags.add(arrayOf("d", dTag))
|
||||||
title?.let { tags.add(arrayOf("title", it)) }
|
title?.let { tags.add(arrayOf("title", it)) }
|
||||||
image?.let { tags.add(arrayOf("image", it)) }
|
image?.let { tags.add(arrayOf("image", it)) }
|
||||||
|
@ -62,11 +105,36 @@ class ClassifiedsEvent(
|
||||||
tags.add(arrayOf("price", it.amount))
|
tags.add(arrayOf("price", it.amount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
category?.let { tags.add(arrayOf("t", it)) }
|
||||||
location?.let { tags.add(arrayOf("location", it)) }
|
location?.let { tags.add(arrayOf("location", it)) }
|
||||||
publishedAt?.let { tags.add(arrayOf("publishedAt", it.toString())) }
|
publishedAt?.let { tags.add(arrayOf("publishedAt", it.toString())) }
|
||||||
title?.let { tags.add(arrayOf("title", it)) }
|
condition?.let { tags.add(arrayOf("condition", it.value)) }
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), "", onReady)
|
findHashtags(message).forEach {
|
||||||
|
tags.add(arrayOf("t", it))
|
||||||
|
tags.add(arrayOf("t", it.lowercase()))
|
||||||
|
}
|
||||||
|
zapReceiver?.forEach {
|
||||||
|
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||||
|
}
|
||||||
|
findURLs(message).forEach {
|
||||||
|
val removedParamsFromUrl = it.split("?")[0].lowercase()
|
||||||
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
|
tags.add(arrayOf("image", it))
|
||||||
|
}
|
||||||
|
tags.add(arrayOf("r", it))
|
||||||
|
}
|
||||||
|
if (markAsSensitive) {
|
||||||
|
tags.add(arrayOf("content-warning", ""))
|
||||||
|
}
|
||||||
|
zapRaiserAmount?.let {
|
||||||
|
tags.add(arrayOf("zapraiser", "$it"))
|
||||||
|
}
|
||||||
|
geohash?.let {
|
||||||
|
tags.addAll(geohashMipMap(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue