Merge pull request #856 from believethehype/NIP90-ContentDiscovery
Nip90 Content Discovery
This commit is contained in:
@@ -89,6 +89,11 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
|||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90UserDiscoveryRequestEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90UserDiscoveryResponseEvent
|
||||||
import com.vitorpamplona.quartz.events.NNSEvent
|
import com.vitorpamplona.quartz.events.NNSEvent
|
||||||
import com.vitorpamplona.quartz.events.OtsEvent
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
@@ -384,6 +389,146 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: NIP90ContentDiscoveryResponseEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val replyTo = computeReplyTo(event)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
|
||||||
|
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach { it.addReply(note) }
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: NIP90ContentDiscoveryRequestEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val replyTo = computeReplyTo(event)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
|
||||||
|
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach { it.addReply(note) }
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: NIP90StatusEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val replyTo = computeReplyTo(event)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
|
||||||
|
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach { it.addReply(note) }
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: NIP90UserDiscoveryResponseEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val replyTo = computeReplyTo(event)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
|
||||||
|
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach { it.addReply(note) }
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: NIP90UserDiscoveryRequestEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val replyTo = computeReplyTo(event)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
|
||||||
|
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach { it.addReply(note) }
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: GitPatchEvent,
|
event: GitPatchEvent,
|
||||||
relay: Relay? = null,
|
relay: Relay? = null,
|
||||||
@@ -2299,6 +2444,11 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is LnZapRequestEvent -> consume(event)
|
is LnZapRequestEvent -> consume(event)
|
||||||
|
is NIP90StatusEvent -> consume(event, relay)
|
||||||
|
is NIP90ContentDiscoveryResponseEvent -> consume(event, relay)
|
||||||
|
is NIP90ContentDiscoveryRequestEvent -> consume(event, relay)
|
||||||
|
is NIP90UserDiscoveryResponseEvent -> consume(event, relay)
|
||||||
|
is NIP90UserDiscoveryRequestEvent -> consume(event, relay)
|
||||||
is LnZapPaymentRequestEvent -> consume(event)
|
is LnZapPaymentRequestEvent -> consume(event)
|
||||||
is LnZapPaymentResponseEvent -> consume(event)
|
is LnZapPaymentResponseEvent -> consume(event)
|
||||||
is LongTextNoteEvent -> consume(event, relay)
|
is LongTextNoteEvent -> consume(event, relay)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
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
|
||||||
@@ -34,6 +35,8 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
|||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -131,6 +134,61 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> {
|
||||||
|
return listOfNotNull(
|
||||||
|
TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter =
|
||||||
|
JsonFilter(
|
||||||
|
kinds = listOf(AppDefinitionEvent.KIND),
|
||||||
|
limit = 300,
|
||||||
|
tags = mapOf("k" to kTags),
|
||||||
|
since =
|
||||||
|
latestEOSEs.users[account.userProfile()]
|
||||||
|
?.followList
|
||||||
|
?.get(account.defaultDiscoveryFollowList.value)
|
||||||
|
?.relayList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNIP90ResponseFilter(): List<TypedFilter> {
|
||||||
|
return listOfNotNull(
|
||||||
|
TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter =
|
||||||
|
JsonFilter(
|
||||||
|
kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND),
|
||||||
|
limit = 300,
|
||||||
|
since =
|
||||||
|
latestEOSEs.users[account.userProfile()]
|
||||||
|
?.followList
|
||||||
|
?.get(account.defaultDiscoveryFollowList.value)
|
||||||
|
?.relayList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNIP90StatusFilter(): List<TypedFilter> {
|
||||||
|
return listOfNotNull(
|
||||||
|
TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter =
|
||||||
|
JsonFilter(
|
||||||
|
kinds = listOf(NIP90StatusEvent.KIND),
|
||||||
|
limit = 300,
|
||||||
|
since =
|
||||||
|
latestEOSEs.users[account.userProfile()]
|
||||||
|
?.followList
|
||||||
|
?.get(account.defaultDiscoveryFollowList.value)
|
||||||
|
?.relayList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||||
|
|
||||||
@@ -404,6 +462,9 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
discoveryFeedChannel.typedFilters =
|
discoveryFeedChannel.typedFilters =
|
||||||
createLiveStreamFilter()
|
createLiveStreamFilter()
|
||||||
|
.plus(createNIP89Filter(listOf("5300")))
|
||||||
|
.plus(createNIP90ResponseFilter())
|
||||||
|
.plus(createNIP90StatusFilter())
|
||||||
.plus(createPublicChatFilter())
|
.plus(createPublicChatFilter())
|
||||||
.plus(createMarketplaceFilter())
|
.plus(createMarketplaceFilter())
|
||||||
.plus(
|
.plus(
|
||||||
@@ -417,6 +478,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
createPublicChatsGeohashesFilter(),
|
createPublicChatsGeohashesFilter(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.toList()
|
||||||
.ifEmpty { null }
|
.ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 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.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
open class DiscoverNIP89FeedFilter(
|
||||||
|
val account: Account,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
val lastAnnounced = 90 * 24 * 60 * 60 // 90 Days ago
|
||||||
|
// TODO better than announced would be last active, as this requires the DVM provider to regularly update the NIP89 announcement
|
||||||
|
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + followList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun followList(): String {
|
||||||
|
return account.defaultDiscoveryFollowList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean {
|
||||||
|
return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||||
|
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val notes =
|
||||||
|
LocalCache.addressables.filterIntoSet { _, it ->
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is AppDefinitionEvent && noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sort(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
return innerApplyFilter(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams {
|
||||||
|
return FilterByListParams.create(
|
||||||
|
account.userProfile().pubkeyHex,
|
||||||
|
account.defaultDiscoveryFollowList.value,
|
||||||
|
account.liveDiscoveryFollowLists.value,
|
||||||
|
account.flowHiddenUsers.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
return collection.filterTo(HashSet()) {
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is AppDefinitionEvent && noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* 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.dal
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
|
|
||||||
|
open class NIP90ContentDiscoveryFilter(
|
||||||
|
val account: Account,
|
||||||
|
val dvmkey: String,
|
||||||
|
val request: String,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + request
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun followList(): String {
|
||||||
|
return account.defaultDiscoveryFollowList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean {
|
||||||
|
return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||||
|
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val notes =
|
||||||
|
LocalCache.notes.filterIntoSet { _, it ->
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is NIP90ContentDiscoveryResponseEvent && it.event!!.isTaggedEvent(request)
|
||||||
|
// it.event?.pubKey() == dvmkey && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted = sort(notes)
|
||||||
|
if (sorted.isNotEmpty()) {
|
||||||
|
var note = sorted.first()
|
||||||
|
|
||||||
|
var eventContent = note.event?.content()
|
||||||
|
|
||||||
|
var collection: HashSet<Note> = hashSetOf()
|
||||||
|
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
var json = mapper.readValue(eventContent, Array::class.java)
|
||||||
|
for (element in json) {
|
||||||
|
// var test = mapper.readValue(element.toString(), Array::class.java)
|
||||||
|
// TODO. This is ugly. how to Kotlin?
|
||||||
|
var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd()
|
||||||
|
var note = LocalCache.checkGetOrCreateNote(id)
|
||||||
|
if (note != null) {
|
||||||
|
collection.add(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection.toList()
|
||||||
|
} else {
|
||||||
|
return sort(notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
return innerApplyFilter(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams {
|
||||||
|
return FilterByListParams.create(
|
||||||
|
account.userProfile().pubkeyHex,
|
||||||
|
account.defaultDiscoveryFollowList.value,
|
||||||
|
account.liveDiscoveryFollowLists.value,
|
||||||
|
account.flowHiddenUsers.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
// val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val notes =
|
||||||
|
collection.filterTo(HashSet()) {
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is NIP90ContentDiscoveryResponseEvent && // &&
|
||||||
|
it.event!!.isTaggedEvent(request) // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sorted = sort(notes)
|
||||||
|
if (sorted.isNotEmpty()) {
|
||||||
|
var note = sorted.first()
|
||||||
|
|
||||||
|
var eventContent = note.event?.content()
|
||||||
|
println(eventContent)
|
||||||
|
|
||||||
|
val collection: HashSet<Note> = hashSetOf()
|
||||||
|
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
var json = mapper.readValue(eventContent, Array::class.java)
|
||||||
|
for (element in json) {
|
||||||
|
// var test = mapper.readValue(element.toString(), Array::class.java)
|
||||||
|
// TODO. This is ugly. how to Kotlin?
|
||||||
|
var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd()
|
||||||
|
|
||||||
|
val note = LocalCache.checkGetOrCreateNote(id)
|
||||||
|
if (note != null) {
|
||||||
|
collection.add(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection
|
||||||
|
} else {
|
||||||
|
return notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
return collection.toList() // collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* 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.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
|
|
||||||
|
open class NIP90StatusFilter(
|
||||||
|
val account: Account,
|
||||||
|
val dvmkey: String,
|
||||||
|
val request: String,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + followList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun followList(): String {
|
||||||
|
return account.defaultDiscoveryFollowList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean {
|
||||||
|
return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||||
|
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val status =
|
||||||
|
LocalCache.notes.filterIntoSet { _, it ->
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is NIP90StatusEvent && it.event?.pubKey() == dvmkey &&
|
||||||
|
it.event!!.isTaggedEvent(request)
|
||||||
|
// && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
if (status.isNotEmpty()) {
|
||||||
|
return listOf(status.first())
|
||||||
|
} else {
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
return innerApplyFilter(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams {
|
||||||
|
return FilterByListParams.create(
|
||||||
|
account.userProfile().pubkeyHex,
|
||||||
|
account.defaultDiscoveryFollowList.value,
|
||||||
|
account.liveDiscoveryFollowLists.value,
|
||||||
|
account.flowHiddenUsers.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
// val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val status =
|
||||||
|
LocalCache.notes.filterIntoSet { _, it ->
|
||||||
|
val noteEvent = it.event
|
||||||
|
noteEvent is NIP90StatusEvent && it.event?.pubKey() == dvmkey &&
|
||||||
|
it.event!!.isTaggedEvent(request)
|
||||||
|
// && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent)
|
||||||
|
}
|
||||||
|
if (status.isNotEmpty()) {
|
||||||
|
return setOf(status.first())
|
||||||
|
} else {
|
||||||
|
return setOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
return collection.toList() // collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ import com.vitorpamplona.quartz.events.HighlightEvent
|
|||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.events.RepostEvent
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
@@ -111,6 +114,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
it.event !is LnZapRequestEvent &&
|
it.event !is LnZapRequestEvent &&
|
||||||
it.event !is BadgeDefinitionEvent &&
|
it.event !is BadgeDefinitionEvent &&
|
||||||
it.event !is BadgeProfilesEvent &&
|
it.event !is BadgeProfilesEvent &&
|
||||||
|
it.event !is NIP90ContentDiscoveryResponseEvent && it.event !is NIP90StatusEvent && it.event !is NIP90ContentDiscoveryRequestEvent &&
|
||||||
it.event !is GiftWrapEvent &&
|
it.event !is GiftWrapEvent &&
|
||||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||||
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
|
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
@@ -67,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NIP90ContentDiscoveryScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||||
@@ -86,6 +88,7 @@ fun AppNavigation(
|
|||||||
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||||
videoFeedViewModel: NostrVideoFeedViewModel,
|
videoFeedViewModel: NostrVideoFeedViewModel,
|
||||||
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
||||||
|
discoverNip89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
|
||||||
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
||||||
@@ -173,6 +176,7 @@ fun AppNavigation(
|
|||||||
route.arguments,
|
route.arguments,
|
||||||
content = {
|
content = {
|
||||||
DiscoverScreen(
|
DiscoverScreen(
|
||||||
|
discoveryContentNIP89FeedViewModel = discoverNip89FeedViewModel,
|
||||||
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
|
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
|
||||||
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
@@ -215,8 +219,25 @@ fun AppNavigation(
|
|||||||
|
|
||||||
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
||||||
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
||||||
|
|
||||||
composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) })
|
composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) })
|
||||||
|
|
||||||
|
Route.ContentDiscovery.let { route ->
|
||||||
|
composable(
|
||||||
|
route.route,
|
||||||
|
route.arguments,
|
||||||
|
content = {
|
||||||
|
it.arguments?.getString("id")?.let { it1 ->
|
||||||
|
NIP90ContentDiscoveryScreen(
|
||||||
|
DVMID = it1,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Route.Profile.let { route ->
|
Route.Profile.let { route ->
|
||||||
composable(
|
composable(
|
||||||
route.route,
|
route.route,
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ private fun RenderTopRouteBar(
|
|||||||
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
|
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
|
||||||
Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack)
|
Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack)
|
||||||
Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack)
|
Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack)
|
||||||
|
Route.ContentDiscovery.base -> TopBarWithBackButton(stringResource(id = R.string.discover_content), navPopBack)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ sealed class Route(
|
|||||||
contentDescriptor = R.string.route_home,
|
contentDescriptor = R.string.route_home,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object ContentDiscovery :
|
||||||
|
Route(
|
||||||
|
icon = R.drawable.ic_bookmarks,
|
||||||
|
contentDescriptor = R.string.discover_content,
|
||||||
|
route = "ContentDiscovery/{id}",
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
object Drafts :
|
object Drafts :
|
||||||
Route(
|
Route(
|
||||||
route = "Drafts",
|
route = "Drafts",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ 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.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
@@ -213,6 +214,9 @@ fun InnerChannelCardWithReactions(
|
|||||||
is ClassifiedsEvent -> {
|
is ClassifiedsEvent -> {
|
||||||
InnerCardBox(baseNote, accountViewModel, nav)
|
InnerCardBox(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
is AppDefinitionEvent -> {
|
||||||
|
InnerCardRow(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +272,9 @@ private fun RenderNoteRow(
|
|||||||
is ChannelCreateEvent -> {
|
is ChannelCreateEvent -> {
|
||||||
RenderChannelThumb(baseNote, accountViewModel, nav)
|
RenderChannelThumb(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
is AppDefinitionEvent -> {
|
||||||
|
RenderContentDVMThumb(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +523,13 @@ data class CommunityCard(
|
|||||||
val moderators: ImmutableList<Participant>,
|
val moderators: ImmutableList<Participant>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DVMCard(
|
||||||
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
|
val cover: String?,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderCommunitiesThumb(
|
fun RenderCommunitiesThumb(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -715,6 +729,92 @@ private fun LoadParticipants(
|
|||||||
inner(participantUsers)
|
inner(participantUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderContentDVMThumb(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val noteEvent = baseNote.event as? AppDefinitionEvent ?: return
|
||||||
|
|
||||||
|
val card by
|
||||||
|
baseNote
|
||||||
|
.live()
|
||||||
|
.metadata
|
||||||
|
.map {
|
||||||
|
val noteEvent = it.note.event as? AppDefinitionEvent
|
||||||
|
|
||||||
|
DVMCard(
|
||||||
|
name = noteEvent?.appMetaData()?.name ?: "",
|
||||||
|
description = noteEvent?.appMetaData()?.about ?: "",
|
||||||
|
cover = noteEvent?.appMetaData()?.image?.ifBlank { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeAsState(
|
||||||
|
DVMCard(
|
||||||
|
name = noteEvent.appMetaData()?.name ?: "",
|
||||||
|
description = noteEvent.appMetaData()?.about ?: "",
|
||||||
|
cover = noteEvent.appMetaData()?.image?.ifBlank { null },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
LeftPictureLayout(
|
||||||
|
onImage = {
|
||||||
|
card.cover?.let {
|
||||||
|
Box(contentAlignment = BottomStart) {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize().clip(QuoteBorder),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run { DisplayAuthorBanner(baseNote) }
|
||||||
|
},
|
||||||
|
onTitleRow = {
|
||||||
|
Text(
|
||||||
|
text = card.name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
LikeReaction(
|
||||||
|
baseNote = baseNote,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
ZapReaction(
|
||||||
|
baseNote = baseNote,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDescription = {
|
||||||
|
card.description?.let {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBottomRow = {
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderChannelThumb(
|
fun RenderChannelThumb(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval
|
||||||
@@ -161,6 +163,8 @@ import com.vitorpamplona.quartz.events.HighlightEvent
|
|||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PinListEvent
|
import com.vitorpamplona.quartz.events.PinListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
@@ -420,13 +424,18 @@ fun ClickableNote(
|
|||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val redirectToNote =
|
if (baseNote.event is AppDefinitionEvent) {
|
||||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
// nav(Route.ContentDiscovery.route + "/${(baseNote.event as AppDefinitionEvent).pubKey()}")
|
||||||
baseNote.replyTo?.lastOrNull() ?: baseNote
|
nav("ContentDiscovery/${(baseNote.event as AppDefinitionEvent).pubKey()}")
|
||||||
} else {
|
} else {
|
||||||
baseNote
|
val redirectToNote =
|
||||||
}
|
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||||
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
|
baseNote.replyTo?.lastOrNull() ?: baseNote
|
||||||
|
} else {
|
||||||
|
baseNote
|
||||||
|
}
|
||||||
|
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = showPopup,
|
onLongClick = showPopup,
|
||||||
@@ -663,6 +672,32 @@ private fun RenderNoteRow(
|
|||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is NIP90ContentDiscoveryResponseEvent ->
|
||||||
|
RenderNIP90ContentDiscoveryResponse(
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
|
||||||
|
is NIP90StatusEvent ->
|
||||||
|
RenderNIP90Status(
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
|
||||||
is PollNoteEvent -> {
|
is PollNoteEvent -> {
|
||||||
RenderPoll(
|
RenderPoll(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.AppMetadata
|
||||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||||
import com.vitorpamplona.quartz.events.UserMetadata
|
|
||||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -85,7 +85,7 @@ fun RenderAppDefinition(
|
|||||||
) {
|
) {
|
||||||
val noteEvent = note.event as? AppDefinitionEvent ?: return
|
val noteEvent = note.event as? AppDefinitionEvent ?: return
|
||||||
|
|
||||||
var metadata by remember { mutableStateOf<UserMetadata?>(null) }
|
var metadata by remember { mutableStateOf<AppMetadata?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = noteEvent) {
|
LaunchedEffect(key1 = noteEvent) {
|
||||||
withContext(Dispatchers.Default) { metadata = noteEvent.appMetaData() }
|
withContext(Dispatchers.Default) { metadata = noteEvent.appMetaData() }
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* 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.types
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||||
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderNIP90ContentDiscoveryResponse(
|
||||||
|
note: Note,
|
||||||
|
makeItShort: Boolean,
|
||||||
|
canPreview: Boolean,
|
||||||
|
quotesLeft: Int,
|
||||||
|
unPackReply: Boolean,
|
||||||
|
backgroundColor: MutableState<Color>,
|
||||||
|
editState: State<GenericLoadable<EditState>>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event
|
||||||
|
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||||
|
|
||||||
|
val showReply by
|
||||||
|
remember(note) {
|
||||||
|
derivedStateOf {
|
||||||
|
noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showReply) {
|
||||||
|
val replyingDirectlyTo =
|
||||||
|
remember(note) {
|
||||||
|
if (noteEvent is BaseTextNoteEvent) {
|
||||||
|
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||||
|
if (replyingTo != null) {
|
||||||
|
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||||
|
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
|
||||||
|
newNote
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replyingDirectlyTo != null && unPackReply) {
|
||||||
|
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadDecryptedContent(
|
||||||
|
note,
|
||||||
|
accountViewModel,
|
||||||
|
) { body ->
|
||||||
|
val eventContent by
|
||||||
|
remember(note.event) {
|
||||||
|
derivedStateOf {
|
||||||
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||||
|
val newBody =
|
||||||
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
|
val state =
|
||||||
|
(editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow
|
||||||
|
state?.value?.event?.content() ?: body
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) {
|
||||||
|
"### $subject\n$newBody"
|
||||||
|
} else {
|
||||||
|
newBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isAuthorTheLoggedUser =
|
||||||
|
remember(note.event) { accountViewModel.isLoggedUser(note.author) }
|
||||||
|
|
||||||
|
SensitivityWarning(
|
||||||
|
note = note,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||||
|
val tags =
|
||||||
|
remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||||
|
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = eventContent,
|
||||||
|
canPreview = canPreview && !makeItShort,
|
||||||
|
quotesLeft = quotesLeft,
|
||||||
|
modifier = modifier,
|
||||||
|
tags = tags,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
id = note.idHex,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.event?.hasHashtags() == true) {
|
||||||
|
val hashtags =
|
||||||
|
remember(note.event) {
|
||||||
|
note.event?.hashtags()?.toImmutableList() ?: persistentListOf()
|
||||||
|
}
|
||||||
|
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* 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.types
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderNIP90Status(
|
||||||
|
note: Note,
|
||||||
|
makeItShort: Boolean,
|
||||||
|
canPreview: Boolean,
|
||||||
|
quotesLeft: Int,
|
||||||
|
unPackReply: Boolean,
|
||||||
|
backgroundColor: MutableState<Color>,
|
||||||
|
editState: State<GenericLoadable<EditState>>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event
|
||||||
|
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||||
|
|
||||||
|
val showReply by
|
||||||
|
remember(note) {
|
||||||
|
derivedStateOf {
|
||||||
|
noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showReply) {
|
||||||
|
val replyingDirectlyTo =
|
||||||
|
remember(note) {
|
||||||
|
if (noteEvent is BaseTextNoteEvent) {
|
||||||
|
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||||
|
if (replyingTo != null) {
|
||||||
|
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||||
|
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
|
||||||
|
newNote
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadDecryptedContent(
|
||||||
|
note,
|
||||||
|
accountViewModel,
|
||||||
|
) { body ->
|
||||||
|
val eventContent by
|
||||||
|
remember(note.event) {
|
||||||
|
derivedStateOf {
|
||||||
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||||
|
val newBody =
|
||||||
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
|
val state =
|
||||||
|
(editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow
|
||||||
|
state?.value?.event?.content() ?: body
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) {
|
||||||
|
"### $subject\n$newBody"
|
||||||
|
} else {
|
||||||
|
newBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = eventContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,21 @@ fun RefresheableFeedView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DVMStatusView(
|
||||||
|
viewModel: FeedViewModel,
|
||||||
|
routeForLastRead: String?,
|
||||||
|
enablePullRefresh: Boolean = true,
|
||||||
|
scrollStateKey: String? = null,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModel.invalidateData()
|
||||||
|
SaveableFeedState(viewModel, scrollStateKey) { listState ->
|
||||||
|
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RefresheableBox(
|
fun RefresheableBox(
|
||||||
viewModel: InvalidatableViewModel,
|
viewModel: InvalidatableViewModel,
|
||||||
@@ -286,3 +301,19 @@ fun FeedEmpty(onRefresh: () -> Unit) {
|
|||||||
OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) }
|
OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedEmptywithStatus(
|
||||||
|
status: String,
|
||||||
|
onRefresh: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(status)
|
||||||
|
// Spacer(modifier = StdVertSpacer)
|
||||||
|
// OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,12 +47,15 @@ import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverNIP89FeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.NIP90StatusFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
||||||
@@ -109,6 +112,17 @@ class NostrDiscoverMarketplaceFeedViewModel(val account: Account) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NostrDiscoverNIP89FeedViewModel(val account: Account) :
|
||||||
|
FeedViewModel(
|
||||||
|
DiscoverNIP89FeedFilter(account),
|
||||||
|
) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrDiscoverNIP89FeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverNIP89FeedViewModel>): NostrDiscoverNIP89FeedViewModel {
|
||||||
|
return NostrDiscoverNIP89FeedViewModel(account) as NostrDiscoverNIP89FeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NostrDiscoverLiveFeedViewModel(val account: Account) :
|
class NostrDiscoverLiveFeedViewModel(val account: Account) :
|
||||||
FeedViewModel(DiscoverLiveFeedFilter(account)) {
|
FeedViewModel(DiscoverLiveFeedFilter(account)) {
|
||||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
@@ -269,6 +283,26 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account, val dvmkey: String, val requestid: String) :
|
||||||
|
FeedViewModel(NIP90ContentDiscoveryFilter(account, dvmkey, requestid)) {
|
||||||
|
class Factory(val account: Account, val dvmkey: String, val requestid: String) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrNIP90ContentDiscoveryFeedViewModel : ViewModel> create(modelClass: Class<NostrNIP90ContentDiscoveryFeedViewModel>): NostrNIP90ContentDiscoveryFeedViewModel {
|
||||||
|
return NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey, requestid) as NostrNIP90ContentDiscoveryFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class NostrNIP90StatusFeedViewModel(val account: Account, val dvmkey: String, val requestid: String) :
|
||||||
|
FeedViewModel(NIP90StatusFilter(account, dvmkey, requestid)) {
|
||||||
|
class Factory(val account: Account, val dvmkey: String, val requestid: String) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrNIP90StatusFeedViewModel : ViewModel> create(modelClass: Class<NostrNIP90StatusFeedViewModel>): NostrNIP90StatusFeedViewModel {
|
||||||
|
return NostrNIP90StatusFeedViewModel(account, dvmkey, requestid) as NostrNIP90StatusFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NostrDraftEventsFeedViewModel(val account: Account) :
|
class NostrDraftEventsFeedViewModel(val account: Account) :
|
||||||
FeedViewModel(DraftEventsFeedFilter(account)) {
|
FeedViewModel(DraftEventsFeedFilter(account)) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ object ScrollStateKeys {
|
|||||||
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
||||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||||
|
|
||||||
|
val DISCOVER_CONTENT = Route.Home.base + "DiscoverContent"
|
||||||
val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace"
|
val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace"
|
||||||
val DISCOVER_LIVE = Route.Home.base + "Live"
|
val DISCOVER_LIVE = Route.Home.base + "Live"
|
||||||
val DISCOVER_COMMUNITY = Route.Home.base + "Communities"
|
val DISCOVER_COMMUNITY = Route.Home.base + "Communities"
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||||
@@ -78,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
@@ -89,6 +91,7 @@ import kotlinx.coroutines.launch
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoverScreen(
|
fun DiscoverScreen(
|
||||||
|
discoveryContentNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
|
||||||
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
||||||
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
@@ -100,12 +103,20 @@ fun DiscoverScreen(
|
|||||||
|
|
||||||
val tabs by
|
val tabs by
|
||||||
remember(
|
remember(
|
||||||
|
discoveryContentNIP89FeedViewModel,
|
||||||
discoveryLiveFeedViewModel,
|
discoveryLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel,
|
||||||
discoveryChatFeedViewModel,
|
discoveryChatFeedViewModel,
|
||||||
) {
|
) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
listOf(
|
listOf(
|
||||||
|
TabItem(
|
||||||
|
R.string.discover_content,
|
||||||
|
discoveryContentNIP89FeedViewModel,
|
||||||
|
Route.Discover.base + "DiscoverContent",
|
||||||
|
ScrollStateKeys.DISCOVER_CONTENT,
|
||||||
|
AppDefinitionEvent.KIND,
|
||||||
|
),
|
||||||
TabItem(
|
TabItem(
|
||||||
R.string.discover_marketplace,
|
R.string.discover_marketplace,
|
||||||
discoveryMarketplaceFeedViewModel,
|
discoveryMarketplaceFeedViewModel,
|
||||||
@@ -142,6 +153,7 @@ fun DiscoverScreen(
|
|||||||
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
|
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
|
||||||
|
|
||||||
WatchAccountForDiscoveryScreen(
|
WatchAccountForDiscoveryScreen(
|
||||||
|
discoverNIP89FeedViewModel = discoveryContentNIP89FeedViewModel,
|
||||||
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
|
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
|
||||||
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
@@ -310,6 +322,7 @@ private fun RenderDiscoverFeed(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WatchAccountForDiscoveryScreen(
|
fun WatchAccountForDiscoveryScreen(
|
||||||
|
discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
|
||||||
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
|
||||||
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
@@ -320,6 +333,7 @@ fun WatchAccountForDiscoveryScreen(
|
|||||||
|
|
||||||
LaunchedEffect(accountViewModel, listState) {
|
LaunchedEffect(accountViewModel, listState) {
|
||||||
NostrDiscoveryDataSource.resetFilters()
|
NostrDiscoveryDataSource.resetFilters()
|
||||||
|
discoverNIP89FeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
@@ -344,20 +358,30 @@ private fun DiscoverFeedLoaded(
|
|||||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||||
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
|
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
|
||||||
|
|
||||||
Row(defaultModifier) {
|
// TODO For now we avoid subscription based DVMs, as we need logic for these first if a user is not subscribed already.
|
||||||
ChannelCardCompose(
|
var avoid = false
|
||||||
baseNote = item,
|
if (item.event is AppDefinitionEvent) {
|
||||||
routeForLastRead = routeForLastRead,
|
if ((item.event as AppDefinitionEvent).appMetaData()?.subscription == true) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
avoid = true
|
||||||
forceEventKind = forceEventKind,
|
}
|
||||||
accountViewModel = accountViewModel,
|
}
|
||||||
nav = nav,
|
// TODO End
|
||||||
|
if (!avoid) {
|
||||||
|
Row(defaultModifier) {
|
||||||
|
ChannelCardCompose(
|
||||||
|
baseNote = item,
|
||||||
|
routeForLastRead = routeForLastRead,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
forceEventKind = forceEventKind,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = DividerThickness,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
@@ -207,6 +208,12 @@ fun MainScreen(
|
|||||||
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account),
|
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = "NostrDiscoveryNIP89FeedViewModel",
|
||||||
|
factory = NostrDiscoverNIP89FeedViewModel.Factory(accountViewModel.account),
|
||||||
|
)
|
||||||
|
|
||||||
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel =
|
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel =
|
||||||
viewModel(
|
viewModel(
|
||||||
key = "NostrDiscoveryLiveFeedViewModel",
|
key = "NostrDiscoveryLiveFeedViewModel",
|
||||||
@@ -411,6 +418,7 @@ fun MainScreen(
|
|||||||
knownFeedViewModel = knownFeedViewModel,
|
knownFeedViewModel = knownFeedViewModel,
|
||||||
newFeedViewModel = newFeedViewModel,
|
newFeedViewModel = newFeedViewModel,
|
||||||
videoFeedViewModel = videoFeedViewModel,
|
videoFeedViewModel = videoFeedViewModel,
|
||||||
|
discoverNip89FeedViewModel = discoverNIP89FeedViewModel,
|
||||||
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
|
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
|
||||||
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
|
|||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedEmptywithStatus
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrNIP90StatusFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||||
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NIP90ContentDiscoveryScreen(
|
||||||
|
DVMID: String,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
var requestID = ""
|
||||||
|
val thread =
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) {
|
||||||
|
Client.send(it)
|
||||||
|
requestID = it.id
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
val resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = "NostrNIP90ContentDiscoveryFeedViewModel",
|
||||||
|
factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID, requestid = requestID),
|
||||||
|
)
|
||||||
|
|
||||||
|
val statusFeedViewModel: NostrNIP90StatusFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = "NostrNIP90StatusFeedViewModel",
|
||||||
|
factory = NostrNIP90StatusFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID, requestid = requestID),
|
||||||
|
)
|
||||||
|
|
||||||
|
val userState by accountViewModel.account.decryptBookmarks.observeAsState() // TODO
|
||||||
|
|
||||||
|
LaunchedEffect(userState) {
|
||||||
|
resultFeedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNostrNIP90ContentDiscoveryScreen(DVMID, accountViewModel, nav, resultFeedViewModel, statusFeedViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
fun RenderNostrNIP90ContentDiscoveryScreen(
|
||||||
|
DVMID: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel,
|
||||||
|
statusFeedViewModel: NostrNIP90StatusFeedViewModel,
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
val pagerState = rememberPagerState { 2 }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
// TODO 1 Render a nice header with image and DVM name from the id
|
||||||
|
// TODO How do we get the event information here?
|
||||||
|
|
||||||
|
var dvminfo = "DVM " + DVMID
|
||||||
|
if (DVMID != null) {
|
||||||
|
val thread =
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
var note = LocalCache.checkGetOrCreateNote(DVMID)
|
||||||
|
if (note != null) {
|
||||||
|
dvminfo = ((note.event as AppDefinitionEvent).appMetaData()?.name ?: "DVM from note")
|
||||||
|
} else {
|
||||||
|
dvminfo = "DVM from not found"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO 2 Get the latest event from the statusFeedViewModel
|
||||||
|
// TODO How do we extract the latest event.content (or event.status) from statusFeedViewModel
|
||||||
|
// TODO We want to update dvmStatus with the content of the latest Status event
|
||||||
|
var dvmStatus = "DVM is processing..."
|
||||||
|
|
||||||
|
/* if (statusFeedViewModel.localFilter.feed().isNotEmpty()) {
|
||||||
|
statusFeedViewModel.localFilter.feed()[0].event?.let { Text(text = it.content()) }
|
||||||
|
} else {
|
||||||
|
Text(text = "Nah")
|
||||||
|
}
|
||||||
|
|
||||||
|
DVMStatusView(
|
||||||
|
statusFeedViewModel,
|
||||||
|
null,
|
||||||
|
enablePullRefresh = false,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)*/
|
||||||
|
|
||||||
|
// Text(text = dvminfo)
|
||||||
|
|
||||||
|
HorizontalPager(state = pagerState) {
|
||||||
|
RefresheableBox(resultFeedViewModel, false) {
|
||||||
|
SaveableFeedState(resultFeedViewModel, null) { listState ->
|
||||||
|
RenderFeedState(
|
||||||
|
resultFeedViewModel,
|
||||||
|
accountViewModel,
|
||||||
|
listState,
|
||||||
|
nav,
|
||||||
|
null,
|
||||||
|
onEmpty = {
|
||||||
|
FeedEmptywithStatus(status = dvmStatus) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -490,6 +490,7 @@
|
|||||||
|
|
||||||
<string name="relay_setup">Relays</string>
|
<string name="relay_setup">Relays</string>
|
||||||
|
|
||||||
|
<string name="discover_content">Note Discovery</string>
|
||||||
<string name="discover_marketplace">Marketplace</string>
|
<string name="discover_marketplace">Marketplace</string>
|
||||||
<string name="discover_live">Live</string>
|
<string name="discover_live">Live</string>
|
||||||
<string name="discover_community">Community</string>
|
<string name="discover_community">Community</string>
|
||||||
|
|||||||
@@ -22,11 +22,94 @@ package com.vitorpamplona.quartz.events
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class AppMetadata {
|
||||||
|
var name: String? = null
|
||||||
|
var username: String? = null
|
||||||
|
|
||||||
|
@JsonProperty("display_name")
|
||||||
|
var displayName: String? = null
|
||||||
|
var picture: String? = null
|
||||||
|
|
||||||
|
var banner: String? = null
|
||||||
|
var image: String? = null
|
||||||
|
var website: String? = null
|
||||||
|
var about: String? = null
|
||||||
|
var subscription: Boolean? = false
|
||||||
|
var cashuAccepted: Boolean? = false
|
||||||
|
var encryptionSupported: Boolean? = false
|
||||||
|
var personalized: Boolean? = false
|
||||||
|
var amount: String? = null
|
||||||
|
|
||||||
|
var nip05: String? = null
|
||||||
|
var domain: String? = null
|
||||||
|
var lud06: String? = null
|
||||||
|
var lud16: String? = null
|
||||||
|
|
||||||
|
var twitter: String? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var tags: ImmutableListOfLists<String>? = null
|
||||||
|
|
||||||
|
fun anyName(): String? {
|
||||||
|
return displayName ?: name ?: username
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anyNameStartsWith(prefix: String): Boolean {
|
||||||
|
return listOfNotNull(name, username, displayName, nip05, lud06, lud16).any {
|
||||||
|
it.contains(prefix, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lnAddress(): String? {
|
||||||
|
return lud16 ?: lud06
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bestName(): String? {
|
||||||
|
return displayName ?: name ?: username
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nip05(): String? {
|
||||||
|
return nip05
|
||||||
|
}
|
||||||
|
|
||||||
|
fun profilePicture(): String? {
|
||||||
|
return picture
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanBlankNames() {
|
||||||
|
if (picture?.isNotEmpty() == true) picture = picture?.trim()
|
||||||
|
if (nip05?.isNotEmpty() == true) nip05 = nip05?.trim()
|
||||||
|
|
||||||
|
if (displayName?.isNotEmpty() == true) displayName = displayName?.trim()
|
||||||
|
if (name?.isNotEmpty() == true) name = name?.trim()
|
||||||
|
if (username?.isNotEmpty() == true) username = username?.trim()
|
||||||
|
if (lud06?.isNotEmpty() == true) lud06 = lud06?.trim()
|
||||||
|
if (lud16?.isNotEmpty() == true) lud16 = lud16?.trim()
|
||||||
|
|
||||||
|
if (website?.isNotEmpty() == true) website = website?.trim()
|
||||||
|
if (domain?.isNotEmpty() == true) domain = domain?.trim()
|
||||||
|
|
||||||
|
if (picture?.isBlank() == true) picture = null
|
||||||
|
if (nip05?.isBlank() == true) nip05 = null
|
||||||
|
if (displayName?.isBlank() == true) displayName = null
|
||||||
|
if (name?.isBlank() == true) name = null
|
||||||
|
if (username?.isBlank() == true) username = null
|
||||||
|
if (lud06?.isBlank() == true) lud06 = null
|
||||||
|
if (lud16?.isBlank() == true) lud16 = null
|
||||||
|
|
||||||
|
if (website?.isBlank() == true) website = null
|
||||||
|
if (domain?.isBlank() == true) domain = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class AppDefinitionEvent(
|
class AppDefinitionEvent(
|
||||||
id: HexKey,
|
id: HexKey,
|
||||||
@@ -36,7 +119,7 @@ class AppDefinitionEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
@Transient private var cachedMetadata: UserMetadata? = null
|
@Transient private var cachedMetadata: AppMetadata? = null
|
||||||
|
|
||||||
fun appMetaData() =
|
fun appMetaData() =
|
||||||
if (cachedMetadata != null) {
|
if (cachedMetadata != null) {
|
||||||
@@ -46,7 +129,7 @@ class AppDefinitionEvent(
|
|||||||
val newMetadata =
|
val newMetadata =
|
||||||
mapper.readValue(
|
mapper.readValue(
|
||||||
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
||||||
UserMetadata::class.java,
|
AppMetadata::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedMetadata = newMetadata
|
cachedMetadata = newMetadata
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ class EventFactory {
|
|||||||
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
|
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
|
NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
NIP90UserDiscoveryRequestEvent.KIND -> NIP90UserDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
|
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
|
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
|
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 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.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
class NIP90ContentDiscoveryRequestEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 5300
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
addressedDVM: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val content = ""
|
||||||
|
val tags = mutableListOf<Array<String>>()
|
||||||
|
tags.add(arrayOf("p", addressedDVM))
|
||||||
|
tags.add(arrayOf("alt", "NIP90 Content Discovery request"))
|
||||||
|
tags.add(arrayOf("client", "Amethyst"))
|
||||||
|
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class NIP90ContentDiscoveryResponseEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 6300
|
||||||
|
const val ALT = "NIP90 Content Discovery reply"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (AppRecommendationEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("alt", ALT),
|
||||||
|
)
|
||||||
|
signer.sign(createdAt, KIND, tags, "", onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class NIP90StatusEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 7000
|
||||||
|
const val ALT = "NIP90 Status update"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (AppRecommendationEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("alt", ALT),
|
||||||
|
)
|
||||||
|
signer.sign(createdAt, KIND, tags, "", onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class NIP90UserDiscoveryRequestEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 5301
|
||||||
|
const val ALT = "NIP90 Content Discovery request"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (AppRecommendationEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("alt", ALT),
|
||||||
|
)
|
||||||
|
signer.sign(createdAt, KIND, tags, "", onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class NIP90UserDiscoveryResponseEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 6301
|
||||||
|
const val ALT = "NIP90 Content Discovery reply"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (AppRecommendationEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("alt", ALT),
|
||||||
|
)
|
||||||
|
signer.sign(createdAt, KIND, tags, "", onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user