Merge pull request #856 from believethehype/NIP90-ContentDiscovery

Nip90 Content Discovery
This commit is contained in:
Vitor Pamplona
2024-05-15 10:17:10 -04:00
committed by GitHub
28 changed files with 1611 additions and 23 deletions
@@ -89,6 +89,11 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
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.OtsEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
@@ -384,6 +389,146 @@ object LocalCache {
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(
event: GitPatchEvent,
relay: Relay? = null,
@@ -2299,6 +2444,11 @@ object LocalCache {
}
}
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 LnZapPaymentResponseEvent -> consume(event)
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.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
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.LiveActivitiesChatMessageEvent
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.Job
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> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
@@ -404,6 +462,9 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters =
createLiveStreamFilter()
.plus(createNIP89Filter(listOf("5300")))
.plus(createNIP90ResponseFilter())
.plus(createNIP90StatusFilter())
.plus(createPublicChatFilter())
.plus(createMarketplaceFilter())
.plus(
@@ -417,6 +478,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
createPublicChatsGeohashesFilter(),
),
)
.toList()
.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.LnZapRequestEvent
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.ReactionEvent
import com.vitorpamplona.quartz.events.RepostEvent
@@ -111,6 +114,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
it.event !is LnZapRequestEvent &&
it.event !is BadgeDefinitionEvent &&
it.event !is BadgeProfilesEvent &&
it.event !is NIP90ContentDiscoveryResponseEvent && it.event !is NIP90StatusEvent && it.event !is NIP90ContentDiscoveryRequestEvent &&
it.event !is GiftWrapEvent &&
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
(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.NostrDiscoverLiveFeedViewModel
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.NostrHomeRepliesFeedViewModel
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.HomeScreen
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.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
@@ -86,6 +88,7 @@ fun AppNavigation(
newFeedViewModel: NostrChatroomListNewFeedViewModel,
videoFeedViewModel: NostrVideoFeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoverNip89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@@ -173,6 +176,7 @@ fun AppNavigation(
route.arguments,
content = {
DiscoverScreen(
discoveryContentNIP89FeedViewModel = discoverNip89FeedViewModel,
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
@@ -215,8 +219,25 @@ fun AppNavigation(
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
composable(Route.Bookmarks.route, content = { BookmarkListScreen(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 ->
composable(
route.route,
@@ -188,6 +188,8 @@ private fun RenderTopRouteBar(
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack)
Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack)
Route.ContentDiscovery.base -> TopBarWithBackButton(stringResource(id = R.string.discover_content), navPopBack)
else -> {
if (id != null) {
when (currentRoute) {
@@ -148,6 +148,14 @@ sealed class Route(
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 :
Route(
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.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
@@ -213,6 +214,9 @@ fun InnerChannelCardWithReactions(
is ClassifiedsEvent -> {
InnerCardBox(baseNote, accountViewModel, nav)
}
is AppDefinitionEvent -> {
InnerCardRow(baseNote, accountViewModel, nav)
}
}
}
@@ -268,6 +272,9 @@ private fun RenderNoteRow(
is ChannelCreateEvent -> {
RenderChannelThumb(baseNote, accountViewModel, nav)
}
is AppDefinitionEvent -> {
RenderContentDVMThumb(baseNote, accountViewModel, nav)
}
}
}
@@ -516,6 +523,13 @@ data class CommunityCard(
val moderators: ImmutableList<Participant>,
)
@Immutable
data class DVMCard(
val name: String,
val description: String?,
val cover: String?,
)
@Composable
fun RenderCommunitiesThumb(
baseNote: Note,
@@ -715,6 +729,92 @@ private fun LoadParticipants(
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
fun RenderChannelThumb(
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.RenderLiveActivityEvent
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.RenderPoll
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.LiveActivitiesEvent
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.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
@@ -420,13 +424,18 @@ fun ClickableNote(
.combinedClickable(
onClick = {
scope.launch {
val redirectToNote =
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
baseNote.replyTo?.lastOrNull() ?: baseNote
} else {
baseNote
}
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
if (baseNote.event is AppDefinitionEvent) {
// nav(Route.ContentDiscovery.route + "/${(baseNote.event as AppDefinitionEvent).pubKey()}")
nav("ContentDiscovery/${(baseNote.event as AppDefinitionEvent).pubKey()}")
} else {
val redirectToNote =
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
baseNote.replyTo?.lastOrNull() ?: baseNote
} else {
baseNote
}
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) }
}
}
},
onLongClick = showPopup,
@@ -663,6 +672,32 @@ private fun RenderNoteRow(
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 -> {
RenderPoll(
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.placeholderText
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AppMetadata
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -85,7 +85,7 @@ fun RenderAppDefinition(
) {
val noteEvent = note.event as? AppDefinitionEvent ?: return
var metadata by remember { mutableStateOf<UserMetadata?>(null) }
var metadata by remember { mutableStateOf<AppMetadata?>(null) }
LaunchedEffect(key1 = noteEvent) {
withContext(Dispatchers.Default) { metadata = noteEvent.appMetaData() }
@@ -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
fun RefresheableBox(
viewModel: InvalidatableViewModel,
@@ -286,3 +301,19 @@ fun FeedEmpty(onRefresh: () -> Unit) {
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.DiscoverLiveFeedFilter
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.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
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.UserProfileAppRecommendationsFeedFilter
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) :
FeedViewModel(DiscoverLiveFeedFilter(account)) {
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
class NostrDraftEventsFeedViewModel(val account: Account) :
FeedViewModel(DraftEventsFeedFilter(account)) {
@@ -43,6 +43,7 @@ object ScrollStateKeys {
val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
val DISCOVER_CONTENT = Route.Home.base + "DiscoverContent"
val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace"
val DISCOVER_LIVE = Route.Home.base + "Live"
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.NostrDiscoverLiveFeedViewModel
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.RefresheableBox
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.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
@@ -89,6 +91,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DiscoverScreen(
discoveryContentNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
@@ -100,12 +103,20 @@ fun DiscoverScreen(
val tabs by
remember(
discoveryContentNIP89FeedViewModel,
discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel,
) {
mutableStateOf(
listOf(
TabItem(
R.string.discover_content,
discoveryContentNIP89FeedViewModel,
Route.Discover.base + "DiscoverContent",
ScrollStateKeys.DISCOVER_CONTENT,
AppDefinitionEvent.KIND,
),
TabItem(
R.string.discover_marketplace,
discoveryMarketplaceFeedViewModel,
@@ -142,6 +153,7 @@ fun DiscoverScreen(
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen(
discoverNIP89FeedViewModel = discoveryContentNIP89FeedViewModel,
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
@@ -310,6 +322,7 @@ private fun RenderDiscoverFeed(
@Composable
fun WatchAccountForDiscoveryScreen(
discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
@@ -320,6 +333,7 @@ fun WatchAccountForDiscoveryScreen(
LaunchedEffect(accountViewModel, listState) {
NostrDiscoveryDataSource.resetFilters()
discoverNIP89FeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
@@ -344,20 +358,30 @@ private fun DiscoverFeedLoaded(
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
Row(defaultModifier) {
ChannelCardCompose(
baseNote = item,
routeForLastRead = routeForLastRead,
modifier = Modifier.fillMaxWidth(),
forceEventKind = forceEventKind,
accountViewModel = accountViewModel,
nav = nav,
// TODO For now we avoid subscription based DVMs, as we need logic for these first if a user is not subscribed already.
var avoid = false
if (item.event is AppDefinitionEvent) {
if ((item.event as AppDefinitionEvent).appMetaData()?.subscription == true) {
avoid = true
}
}
// 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.NostrDiscoverLiveFeedViewModel
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.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
@@ -207,6 +208,12 @@ fun MainScreen(
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account),
)
val discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel =
viewModel(
key = "NostrDiscoveryNIP89FeedViewModel",
factory = NostrDiscoverNIP89FeedViewModel.Factory(accountViewModel.account),
)
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel =
viewModel(
key = "NostrDiscoveryLiveFeedViewModel",
@@ -411,6 +418,7 @@ fun MainScreen(
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
videoFeedViewModel = videoFeedViewModel,
discoverNip89FeedViewModel = discoverNIP89FeedViewModel,
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
@@ -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) {
}
},
)
}
}
}
}
}
+1
View File
@@ -490,6 +490,7 @@
<string name="relay_setup">Relays</string>
<string name="discover_content">Note Discovery</string>
<string name="discover_marketplace">Marketplace</string>
<string name="discover_live">Live</string>
<string name="discover_community">Community</string>
@@ -22,11 +22,94 @@ package com.vitorpamplona.quartz.events
import android.util.Log
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.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
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
class AppDefinitionEvent(
id: HexKey,
@@ -36,7 +119,7 @@ class AppDefinitionEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var cachedMetadata: UserMetadata? = null
@Transient private var cachedMetadata: AppMetadata? = null
fun appMetaData() =
if (cachedMetadata != null) {
@@ -46,7 +129,7 @@ class AppDefinitionEvent(
val newMetadata =
mapper.readValue(
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
UserMetadata::class.java,
AppMetadata::class.java,
)
cachedMetadata = newMetadata
@@ -114,6 +114,11 @@ class EventFactory {
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
MuteListEvent.KIND -> MuteListEvent(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)
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}