This commit is contained in:
Vitor Pamplona 2024-05-16 13:04:17 -04:00
commit d9de0d2798
31 changed files with 1811 additions and 24 deletions

View File

@ -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)

View File

@ -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 }
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,132 @@
/**
* 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: MutableSet<Note> = mutableSetOf()
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
var etags = mapper.readValue(eventContent, List::class.java)
for (element in etags) {
var tag = mapper.readValue(mapper.writeValueAsString(element), Array::class.java)
val note = LocalCache.checkGetOrCreateNote(tag[1].toString())
if (note != null) {
collection.add(note)
}
}
return collection.toList()
} 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 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: MutableSet<Note> = mutableSetOf()
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
var etags = mapper.readValue(eventContent, Array::class.java)
for (element in etags) {
var tag = mapper.readValue(mapper.writeValueAsString(element), Array::class.java)
val note = LocalCache.checkGetOrCreateNote(tag[1].toString())
if (note != null) {
collection.add(note)
}
}
return collection
} else {
return hashSetOf()
}
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.toList() // collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
}

View File

@ -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()
}
}

View File

@ -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) &&

View File

@ -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,

View File

@ -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) {

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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() }

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)) }
}
}

View File

@ -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)) {

View File

@ -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"

View File

@ -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,
)
}
}
}

View File

@ -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,

View File

@ -0,0 +1,156 @@
/**
* 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.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
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.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 (Optional) this now shows the first status update but there might be a better way
var dvmState = stringResource(R.string.dvm_waiting_status)
var dvmNoState = stringResource(R.string.dvm_no_status)
val thread =
Thread {
var count = 0
while (resultFeedViewModel.localFilter.feed().isEmpty()) {
try {
if (statusFeedViewModel.localFilter.feed().isNotEmpty()) {
statusFeedViewModel.localFilter.feed()[0].event?.let { dvmState = it.content() }
println(dvmState)
break
} else if (count > 1000) {
dvmState = dvmNoState
// Might not be the best way, but we want to avoid hanging in the loop forever
} else {
count++
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
thread.start()
thread.join()
// TODO (Optional) Maybe render a nice header with image and DVM name from the dvmID
// TODO (Optional) How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty
// TODO (Optional) otherwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData()
// Text(text = dvminfo)
HorizontalPager(state = pagerState) {
RefresheableBox(resultFeedViewModel, false) {
SaveableFeedState(resultFeedViewModel, null) { listState ->
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
RenderFeedState(
resultFeedViewModel,
accountViewModel,
listState,
nav,
null,
onEmpty = {
// TODO (Optional) Maybe also show some dvm image/text while waiting for the notes in this custom component
FeedEmptywithStatus(status = dvmState) {
}
},
)
}
}
}
}
}

View File

@ -54,6 +54,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@ -91,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.findHashtags
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
@ -295,6 +298,15 @@ private fun SearchTextField(
searchBarViewModel: SearchBarViewModel,
onTextChanges: (String) -> Unit,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
launch {
delay(100)
focusRequester.requestFocus()
}
}
Row(
modifier = Modifier.padding(10.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@ -312,7 +324,11 @@ private fun SearchTextField(
capitalization = KeyboardCapitalization.Sentences,
),
leadingIcon = { SearchIcon(modifier = Size20Modifier, Color.Unspecified) },
modifier = Modifier.weight(1f, true).defaultMinSize(minHeight = 20.dp),
modifier =
Modifier
.weight(1f, true)
.defaultMinSize(minHeight = 20.dp)
.focusRequester(focusRequester),
placeholder = {
Text(
text = stringResource(R.string.npub_hex_username),

View File

@ -429,6 +429,7 @@
<string name="are_you_sure_you_want_to_log_out">Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ?</string>
<string name="followed_tags">Tags suivis</string>
<string name="relay_setup">Relais</string>
<string name="discover_content">Découverte de notes</string>
<string name="discover_marketplace">Place de Marché</string>
<string name="discover_live">Direct</string>
<string name="discover_community">Communauté</string>
@ -669,6 +670,10 @@
<string name="show_npub_as_a_qr_code">Afficher npub en tant que QR code</string>
<string name="invalid_nip19_uri">Adresse invalide</string>
<string name="invalid_nip19_uri_description">Amethyst a reçu une URI à ouvrir mais cette URI était invalide : %1$s</string>
<string name="dm_relays_not_found">Configurer vos relais de messagerie privée</string>
<string name="dm_relays_not_found_description">Ce paramètre informe tout le monde les relais à utiliser pour vous envoyer des messages. Sans eux, vous risquez de manquer certains messages.</string>
<string name="dm_relays_not_found_explanation">Les relais de messagerie DM acceptent n\'importe quel message de n\'importe qui, mais vous permet seulement de les télécharger. Par exemple, inbox.nostr.wine fonctionne de cette façon. </string>
<string name="dm_relays_not_found_create_now">Configurer maintenant</string>
<string name="zap_the_devs_title">Zap les Devs !</string>
<string name="zap_the_devs_description">Votre don nous aide à faire la différence. Chaque sat compte !</string>
<string name="donate_now">Faire un don maintenant</string>

View File

@ -19,32 +19,80 @@
<string name="relay_icon">Ikona retransmitera</string>
<string name="unknown_author">Autor nieznany</string>
<string name="copy_text">Skopiuj tekst</string>
<string name="copy_user_pubkey">Kopiuj identyfikator autora</string>
<string name="copy_note_id">Kopiuj identyfikator notatki</string>
<string name="block_report">Zablokuj / Zgłoś</string>
<string name="block_hide_user"><![CDATA[Zablokuj i ukryj użytkownika]]></string>
<string name="report_spam_scam">Zgłoś spam/oszustwo</string>
<string name="report_impersonation">Zgłoś podszywanie się</string>
<string name="report_explicit_content">Zgłoś niedozwoloną zawartość</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc odpowiedzieć</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby zwiększyć liczbę postów</string>
<string name="login_with_a_private_key_to_like_posts">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby polubić posty</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc wysyłać zapy</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc obserwować</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc przestać obserwować</string>
<string name="login_with_a_private_key_to_be_able_to_hide_word">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc ukryć słowo lub zdanie</string>
<string name="login_with_a_private_key_to_be_able_to_show_word">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc pokazać słowo lub zdanie</string>
<string name="zaps">Zapy</string>
<string name="view_count">Liczba wyświetleń</string>
<string name="boost">Zwiększ</string>
<string name="boosted">zwiększony</string>
<string name="edited">edytowano</string>
<string name="edited_number">edytuj #%1$s</string>
<string name="original">oryginalny</string>
<string name="new_amount_in_sats">Nowa kwota w Satsach</string>
<string name="add">Dodaj</string>
<string name="and">" i "</string>
<string name="in_channel">"na kanale "</string>
<string name="profile_banner">Baner profilu</string>
<string name="payment_successful">Płatność zakończona pomyślnie</string>
<string name="error_parsing_error_message">Błąd podczas analizowania komunikatu o błędzie</string>
<string name="following">" Obserwowani"</string>
<string name="followers">" Obserwujący"</string>
<string name="profile">Profil</string>
<string name="security_filters">Filtry bezpieczeństwa</string>
<string name="log_out">Wyloguj się</string>
<string name="show_more">Pokaż Więcej</string>
<string name="pay">Zapłać</string>
<string name="note_to_receiver">Notatka dla odbiorcy</string>
<string name="thank_you_so_much">Dziękuję bardzo!</string>
<string name="amount_in_sats">Kwota w Satsach</string>
<string name="send_sats">Wyślij Satsy</string>
<string name="error_parsing_preview_for">"Błąd analizowania podglądu dla %1$s : %2$s"</string>
<string name="preview_card_image_for">"Podgląd obrazu karty dla %1$s"</string>
<string name="new_channel">Nowy kanał</string>
<string name="channel_name">Nazwa kanału</string>
<string name="my_awesome_group">Moja wspaniała Grupa</string>
<string name="picture_url">Adres URL zdjęcia</string>
<string name="description">Opis</string>
<string name="about_us">"O nas. "</string>
<string name="what_s_on_your_mind">Co masz na myśli?</string>
<string name="post">Wpis</string>
<string name="save">Zapisz</string>
<string name="create">Utwórz</string>
<string name="cancel">Anuluj</string>
<string name="relay_address">Adres retransmitera</string>
<string name="posts">Wpisy</string>
<string name="errors">Błędy</string>
<string name="add_a_relay">Dodaj Retransmiter</string>
<string name="username">Nazwa użytkownika</string>
<string name="about_me">O mnie</string>
<string name="avatar_url">URL awatara</string>
<string name="banner_url">URL banera</string>
<string name="website_url">Adres URL strony</string>
<string name="upload_image">Dodaj zdjęcie</string>
<string name="blocked_users">Zablokowani użytkownicy</string>
<string name="notes">Notatki</string>
<string name="replies">Odpowiedzi</string>
<string name="reports">"Zgłoszenia"</string>
<string name="more_options">Więcej opcji</string>
<string name="relays">" Retransmitery"</string>
<string name="website">Strona www</string>
<string name="send_a_direct_message">Wyślij bezpośrednią wiadomość</string>
<string name="edits_the_user_s_metadata">Edytowanie metadanych użytkowników</string>
<string name="follow">Obserwuj</string>
<string name="follow_back">Również obserwuj</string>
<string name="unblock">Odblokuj</string>
<string name="copy_user_id">Kopiuj ID użytkownika</string>
<string name="unblock_user">Odblokuj użytkownika</string>
@ -52,13 +100,16 @@
<string name="clear">Wyczyść</string>
<string name="app_logo">Logo aplikacji</string>
<string name="nsec_npub_hex_private_key">nsec. lub npub.</string>
<string name="ncryptsec_password">hasło, aby otworzyć klucz</string>
<string name="show_password">Pokaż hasło</string>
<string name="hide_password">Ukryj hasło</string>
<string name="invalid_key">Nieprawidłowy klucz</string>
<string name="invalid_key_with_message">Nieprawidłowy klucz: %1$s</string>
<string name="i_accept_the">"Akceptuję "</string>
<string name="terms_of_use">warunki użytkowania</string>
<string name="acceptance_of_terms_is_required">Wymagane jest zaakceptowanie warunków użytkowania</string>
<string name="password_is_required">Hasło jest wymagane</string>
<string name="key_is_required">Klucz jest wymagany</string>
<string name="login">Zaloguj się</string>
<string name="sign_up">Zarejestruj się</string>
<string name="create_account">Utwórz konto</string>
@ -75,18 +126,65 @@
<string name="unfollow">Przestań obserwować</string>
<string name="public_chat">Czat Publiczny</string>
<string name="remove">Usuń</string>
<string name="translations_to">do</string>
<string name="nip_05">Adres Nostr</string>
<string name="never">nigdy</string>
<string name="now">teraz</string>
<string name="h">godz.</string>
<string name="nudity">Nagość</string>
<string name="profanity_hateful_speech">Wulgaryzmy / Mowa nienawiści</string>
<string name="report_hateful_speech">Zgłoś nienawistną mowę</string>
<string name="report_nudity_porn">Zgłoś Nagość / Pornografię</string>
<string name="others">inne</string>
<string name="mark_all_as_read">Oznacz wszystkie jako przeczytane</string>
<string name="biometric_error">Błąd</string>
<string name="select_text_dialog_top">Zaznacz tekst</string>
<string name="account_switch_add_account_dialog_title">Dodaj nowe konto</string>
<string name="drawer_accounts">Konta</string>
<string name="account_switch_select_account">Wybierz Konto</string>
<string name="account_switch_add_account_btn">Dodaj konto</string>
<string name="account_switch_pubkey_only">Tylko do odczytu, brak klucza prywatnego</string>
<string name="back">Wstecz</string>
<string name="quick_action_select">Wybierz</string>
<string name="quick_action_share">Udostępnij</string>
<string name="quick_action_copy_user_id">ID autora</string>
<string name="quick_action_copy_note_id">ID notatki</string>
<string name="quick_action_copy_text">Skopiuj tekst</string>
<string name="quick_action_delete">Usuń</string>
<string name="quick_action_unfollow">Przestań obserwować</string>
<string name="quick_action_follow">Śledź</string>
<string name="quick_action_request_deletion_alert_title">Poproś o usunięcie</string>
<string name="quick_action_request_deletion_alert_body">Amethyst poprosi o usunięcie Twojej notatki z aktualnie podłączonych retransmitorów. Nie ma gwarancji, że Twoja notatka zostanie trwale usunięta z tych retransmitorów lub z innych retransmitorów, gdzie może być przechowywana.</string>
<string name="quick_action_block_dialog_btn">Zablokuj</string>
<string name="quick_action_delete_dialog_btn">Usuń</string>
<string name="quick_action_block">Zablokuj</string>
<string name="quick_action_report">Zgłoś</string>
<string name="quick_action_delete_button">Usuń</string>
<string name="quick_action_dont_show_again_button">Nie pokazuj ponownie</string>
<string name="report_dialog_spam">Spam lub oszustwa</string>
<string name="report_dialog_profanity">Wulgaryzmy lub nienawistne zachowanie</string>
<string name="report_dialog_impersonation">Złośliwe podszywanie się</string>
<string name="report_dialog_nudity">Nagość lub zawartość graficzna</string>
<string name="report_dialog_illegal">Nielegalne zachowanie</string>
<string name="report_dialog_blocking_a_user">Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje notatki są nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Zablokuj i ukryj użytkownika]]></string>
<string name="report_dialog_report_btn">Zgłoś nadużycie</string>
<string name="report_dialog_additional_reason_label">Dodatkowe informacje</string>
<string name="report_dialog_select_reason_label">Powód</string>
<string name="report_dialog_select_reason_placeholder">Wybierz powód…</string>
<string name="report_dialog_post_report_btn">Wyślij zgłoszenie</string>
<string name="report_dialog_title">Zablokuj i zgłoś</string>
<string name="block_only">Zablokuj</string>
<string name="bookmarks">Zakładki</string>
<string name="drafts">Projekty</string>
<string name="private_bookmarks">Prywatne Zakładki</string>
<string name="public_bookmarks">Publiczne zakładki</string>
<string name="add_to_private_bookmarks">Dodaj do prywatnych zakładek</string>
<string name="add_to_public_bookmarks">Dodaj do publicznych zakładek</string>
<string name="remove_from_private_bookmarks">Usuń z prywatnych zakładek</string>
<string name="remove_from_public_bookmarks">Usuń z publicznych zakładek</string>
<string name="wallet_connect_service_show_secret">Pokaż tajny klucz</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="custom_zaps_add_a_message">Dodaj wiadomość publiczną</string>
<string name="custom_zaps_add_a_message_private">Dodaj prywatną wiadomość</string>
<string name="custom_zaps_add_a_message_example">Dziękujemy za całą twoją pracę!</string>
@ -97,13 +195,103 @@
<string name="zap_type_private_explainer">Nadawca i odbiorca mogą zobaczyć się nawzajem i przeczytać wiadomość</string>
<string name="upload_server_relays_nip95">Twoje retransmitery (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Pliki są przechowywane przez Twoje retransmitery. Nowy NIP: sprawdź, czy jest obsługiwany</string>
<string name="do_you_really_want_to_disable_tor_text">Twoje dane zostaną natychmiast przekazane w ramach zwykłej sieci</string>
<string name="yes">Tak</string>
<string name="no">Nie</string>
<string name="follow_list_selection">Lista obserwowanych</string>
<string name="invalid_port_number">Nieprawidłowy numer portu</string>
<string name="app_notification_dms_channel_name">Prywatne Wiadomości</string>
<string name="app_notification_dms_channel_description">Powiadamia Cię, gdy nadejdzie prywatna wiadomość</string>
<string name="app_notification_zaps_channel_name">Otrzymano Zapy</string>
<string name="app_notification_zaps_channel_description">Powiadamia Cię, gdy ktoś prześle ci zapy</string>
<string name="channel_list_join_conversation">Dołącz do rozmowy</string>
<string name="channel_list_user_or_group_id">ID Użytkownika lub Grupy</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent lub hex</string>
<string name="channel_list_create_channel">Utwórz</string>
<string name="channel_list_join_channel">Dołącz</string>
<string name="today">Dzisiaj</string>
<string name="content_warning">Ostrzeżenie o zawartości</string>
<string name="content_warning_explanation">Ten post zawiera wrażliwe treści, które niektóre osoby mogą uznać za obraźliwe lub niepokojące</string>
<string name="content_warning_hide_all_sensitive_content">Zawsze ukrywaj wrażliwe treści</string>
<string name="content_warning_show_all_sensitive_content">Zawsze pokazuj wrażliwą zawartość</string>
<string name="content_warning_see_warnings">Zawsze pokazuj ostrzeżenia dotyczące zawartości</string>
<string name="filter_spam_from_strangers">Filtrowanie spamu od nieznajomych</string>
<string name="warn_when_posts_have_reports_from_your_follows">Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz</string>
<string name="read_from_relay">Odczytaj z Retransmitera</string>
<string name="write_to_relay">Zapisz do Retransmitera</string>
<string name="an_error_occurred_trying_to_get_relay_information">Wystąpił błąd podczas próby uzyskania informacji o retransmiterze z %1$s</string>
<string name="owner">Właściciel</string>
<string name="version">Wersja</string>
<string name="software">Oprogramowanie</string>
<string name="contact">Kontakt</string>
<string name="payments_url">Adres URL płatności</string>
<string name="limitations">Ograniczenia</string>
<string name="countries">Kraje</string>
<string name="languages">Języki</string>
<string name="posting_policy">Polityka publikowania</string>
<string name="message_length">Długość wiadomości</string>
<string name="subscriptions">Subskrybcje</string>
<string name="filters">Filtry</string>
<string name="relay_setup">Retransmitery</string>
<string name="discover_community">Społeczność</string>
<string name="discover_chat">Czaty</string>
<string name="community_approved_posts">Zatwierdzone posty</string>
<string name="groups_no_descriptor">Ta grupa nie ma opisu ani reguł. Porozmawiaj z właścicielem, aby je dodać</string>
<string name="community_no_descriptor">Ta społeczność nie ma opisu. Porozmawiaj z właścicielem, aby go dodać</string>
<string name="add_sensitive_content_label">Treść wrażliwa</string>
<string name="add_sensitive_content_description">Dodaje ostrzeżenie o wrażliwej treści przed wyświetleniem tej zawartości</string>
<string name="settings">Ustawienia</string>
<string name="connectivity_type_always">Zawsze</string>
<string name="connectivity_type_wifi_only">Tylko WiFi</string>
<string name="connectivity_type_never">Nigdy</string>
<string name="language">Język</string>
<string name="theme">Motyw</string>
<string name="automatically_load_images_gifs">Podgląd obrazu</string>
<string name="automatically_play_videos">Odtwarzanie wideo</string>
<string name="automatically_show_url_preview">Podgląd URL</string>
<string name="load_image">Załaduj obraz</string>
<string name="spamming_users">Spamerzy</string>
<string name="muted_button">Wyciszone. Kliknij, aby wyłączyć wyciszenie</string>
<string name="mute_button">Dźwięk włączony. Kliknij, aby wyciszyć</string>
<string name="nip05_verified">Adres Nostr został zweryfikowany</string>
<string name="nip05_failed">Nieudana weryfikacja adresu Nostr</string>
<string name="nip05_checking">Sprawdzanie adresu Nostr</string>
<string name="select_deselect_all">Zaznacz/Odznacz wszystko</string>
<string name="select_a_relay_to_continue">Wybierz retransmiter, aby kontynuować</string>
<string name="zap_forward_title">Przekaż zapy do:</string>
<string name="messages_new_message_to">Do</string>
<string name="messages_new_message_subject">Temat</string>
<string name="messages_new_message_subject_caption">Temat dyskusji</string>
<string name="messages_new_message_to_caption">"\@Użytkownik1, @Użytkownik2, @Użytkownik3"</string>
<string name="messages_group_descriptor">Członkowie tej grupy</string>
<string name="copy_to_clipboard">Kopiuj do schowka</string>
<string name="copy_npub_to_clipboard">Kopiuj npub do schowka</string>
<string name="copy_url_to_clipboard">Kopiuj adres URL do schowka</string>
<string name="copy_the_note_id_to_the_clipboard">Kopiuj ID notatki do schowka</string>
<string name="status_update">Zaktualizuj status</string>
<string name="lightning_wallets_not_found">Błąd podczas analizowania komunikatu o błędzie</string>
<string name="zap_split_weight_placeholder">25</string>
<string name="automatically_show_profile_picture">Zdjęcie profilowe</string>
<string name="automatically_show_profile_picture_description">Pokaż zdjęcia profilowe</string>
<string name="select_an_option">Wybierz opcję</string>
<string name="unable_to_download_relay_document">Nie można pobrać dokumentu retransmitera</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="classifieds_price">Cena (w Satach)</string>
<string name="classifieds_location_placeholder">Miasto, Województwo, Kraj</string>
<string name="classifieds_category_accessories">Akcesoria</string>
<string name="classifieds_category_electronics">Elektronika</string>
<string name="classifieds_category_furniture">Meble</string>
<string name="classifieds_category_books">Książki</string>
<string name="classifieds_category_pets">Zwierzęta domowe</string>
<string name="classifieds_category_sports">Sporty</string>
<string name="classifieds_category_fitness">Fitness</string>
<string name="classifieds_category_art">Sztuka</string>
<string name="classifieds_category_office">Biuro</string>
<string name="relay_info">Retransmiter %1$s</string>
<string name="expand_relay_list">Rozwiń listę retransmiterów</string>
<string name="relay_list_selector">Wybór listy retransmiterów</string>
<string name="invalid_nip19_uri">Nieprawidłowy adres</string>
<string name="dm_relays_not_found_create_now">Skonfiguruj teraz</string>
<string name="thank_you">Dziękuję!</string>
<string name="max_limit">Maksymalny Limit</string>
</resources>

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>
@ -845,4 +846,7 @@
<string name="draft_note">Draft Note</string>
<string name="load_from_text">From Msg</string>
<string name="dvm_waiting_status">Waiting for DVM to reply</string>
<string name="dvm_no_status">DVM seems not to reply</string>
</resources>

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}