Brute forces the finding of events.

This commit is contained in:
Vitor Pamplona
2025-07-09 16:57:26 -04:00
parent 267d4c505a
commit accc8d4439
17 changed files with 345 additions and 145 deletions
@@ -95,6 +95,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
@@ -2928,7 +2929,7 @@ object LocalCache : ILocalCache {
} }
getOrCreateUser(nip19.hex) getOrCreateUser(nip19.hex)
} }
is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { is NNote -> {
getOrCreateNote(nip19.hex) getOrCreateNote(nip19.hex)
} }
is NEvent -> { is NEvent -> {
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class EventFinderQueryState( class EventFinderQueryState(
val note: Note, val note: Note,
val account: Account,
) )
class EventFinderFilterAssembler( class EventFinderFilterAssembler(
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -30,18 +31,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
fun EventFinderFilterAssemblerSubscription( fun EventFinderFilterAssemblerSubscription(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) = EventFinderFilterAssemblerSubscription(note, accountViewModel.dataSources().eventFinder) ) = EventFinderFilterAssemblerSubscription(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
@Composable @Composable
fun EventFinderFilterAssemblerSubscription( fun EventFinderFilterAssemblerSubscription(
note: Note, note: Note,
account: Account,
dataSource: EventFinderFilterAssembler, dataSource: EventFinderFilterAssembler,
) { ) {
// different screens get different states // different screens get different states
// even if they are tracking the same tag. // even if they are tracking the same tag.
val state = val state =
remember(note) { remember(note, account) {
EventFinderQueryState(note) EventFinderQueryState(note, account)
} }
KeyDataSourceSubscription(state, dataSource) KeyDataSourceSubscription(state, dataSource)
@@ -22,64 +22,113 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.relayHints
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet
fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? { fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl> {
val missingAddressables = mutableSetOf<Address>() val set = mutableSetOf<NormalizedRelayUrl>()
keys.forEach { set.addAll(LocalCache.relayHints.hintsForAddress(note.idHex))
if (it.note is AddressableNote && it.note.event == null) {
missingAddressables.add(it.note.address())
}
// loads threading that is event-based val isInChannel = note.channelHex()
it.note.replyTo?.forEach { if (isInChannel != null) {
if (it is AddressableNote && it.event == null) { LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach {
missingAddressables.add(it.address()) set.add(it)
}
} }
} }
return filterMissingAddressables(missingAddressables) note.replyTo?.map { parentNote ->
set.addAll(parentNote.relays)
parentNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
set.addAll(childNote.relays)
childNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
note.reactions.map { reactionType ->
reactionType.value.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
}
note.boosts.map { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
return set
} }
fun filterMissingAddressables(missingAddressables: Set<Address>): List<RelayBasedFilter> { fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
val addressesPerRelay =
mapOfSet {
keys.forEach { key ->
val default = key.account.followPlusAllMine.flow.value
if (key.note is AddressableNote && key.note.event == null) {
potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, key.note.address)
}
}
// loads threading that is event-based
key.note.replyTo?.forEach { note ->
if (note is AddressableNote && note.event == null) {
potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.address)
}
}
}
}
}
return filterMissingAddressables(addressesPerRelay)
}
fun filterMissingAddressables(missingAddressables: Map<NormalizedRelayUrl, Set<Address>>): List<RelayBasedFilter> {
if (missingAddressables.isEmpty()) return emptyList() if (missingAddressables.isEmpty()) return emptyList()
return missingAddressables.flatMap { aTag -> return missingAddressables.flatMap { relayEntry ->
val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(aTag.pubKeyHex) relayEntry.value.map { address ->
val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) if (address.kind < 25000 && address.dTag.isBlank()) {
val authorHomeRelays =
authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForKey(aTag.pubKeyHex).ifEmpty { null }
?: listOfNotNull(LocalCache.getUserIfExists(aTag.pubKeyHex)?.latestMetadataRelay)
val relayHints = LocalCache.relayHints.hintsForAddress(aTag.toValue())
(authorHomeRelays + relayHints).toSet().map {
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
RelayBasedFilter( RelayBasedFilter(
relay = it, relay = relayEntry.key,
filter = filter =
Filter( Filter(
kinds = listOf(aTag.kind), kinds = listOf(address.kind),
authors = listOf(aTag.pubKeyHex), authors = listOf(address.pubKeyHex),
limit = 1, limit = 1,
), ),
) )
} else { } else {
RelayBasedFilter( RelayBasedFilter(
relay = it, relay = relayEntry.key,
filter = filter =
Filter( Filter(
kinds = listOf(aTag.kind), kinds = listOf(address.kind),
tags = mapOf("d" to listOf(aTag.dTag)), tags = mapOf("d" to listOf(address.dTag)),
authors = listOf(aTag.pubKeyHex), authors = listOf(address.pubKeyHex),
limit = 1, limit = 1,
), ),
) )
@@ -22,48 +22,101 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.relayHints import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet import com.vitorpamplona.quartz.utils.mapOfSet
fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? { fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
val missingEvents = mutableSetOf<String>() val set = mutableSetOf<NormalizedRelayUrl>()
keys.forEach { set.addAll(LocalCache.relayHints.hintsForEvent(note.idHex))
if (it.note !is AddressableNote && it.note.event == null) {
missingEvents.add(it.note.idHex)
}
// loads threading that is event-based val isInChannel = note.channelHex()
it.note.replyTo?.forEach { if (isInChannel != null) {
if (it !is AddressableNote && it.event == null) { LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach {
missingEvents.add(it.idHex) set.add(it)
}
} }
} }
return filterMissingEvents(missingEvents) note.replyTo?.map { parentNote ->
set.addAll(parentNote.relays)
parentNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
set.addAll(childNote.relays)
childNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
note.reactions.map { reactionType ->
reactionType.value.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
}
note.boosts.map { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
return set
} }
fun filterMissingEvents(missingEventIds: Set<HexKey>): List<RelayBasedFilter> { fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
if (missingEventIds.isEmpty()) return emptyList() val eventsPerRelay =
val relayHints =
mapOfSet { mapOfSet {
missingEventIds.forEach { eventId -> keys.forEach { key ->
LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl -> val default = key.account.followPlusAllMine.flow.value
add(relayUrl, eventId)
if (key.note !is AddressableNote && key.note.event == null) {
potentialRelaysToFindEvent(key.note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, key.note.idHex)
}
}
// loads threading that is event-based
key.note.replyTo?.forEach { note ->
if (note !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
} }
} }
} }
return relayHints.map { return filterMissingEvents(eventsPerRelay)
RelayBasedFilter( }
relay = it.key,
filter = Filter(ids = it.value.sorted()), fun filterMissingEvents(missingEventIds: Map<NormalizedRelayUrl, Set<String>>): List<RelayBasedFilter> {
) if (missingEventIds.isEmpty()) return emptyList()
return missingEventIds.mapNotNull {
if (it.value.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(ids = it.value.sorted()),
)
} else {
null
}
} }
} }
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach import kotlin.collections.forEach
@@ -36,35 +37,44 @@ val MetadataAndRelayListKinds =
AdvertisedRelayListEvent.KIND, AdvertisedRelayListEvent.KIND,
) )
fun filterFindUserMetadataForKey(author: HexKey): List<RelayBasedFilter> = fun filterFindUserMetadataForKey(
author: HexKey,
defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> =
LocalCache.checkGetOrCreateUser(author)?.let { LocalCache.checkGetOrCreateUser(author)?.let {
filterFindUserMetadataForKey(setOf(it)) filterFindUserMetadataForKey(setOf(it), defaultRelays)
} ?: emptyList() } ?: emptyList()
fun filterFindUserMetadataForKey(authors: Set<User>): List<RelayBasedFilter> { fun filterFindUserMetadataForKey(
authors: Set<User>,
defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val perRelayKeys = val perRelayKeys =
mapOfSet { mapOfSet {
authors.forEach { key -> authors.forEach { key ->
val relays = val relays =
key.authorRelayList()?.writeRelaysNorm() key.authorRelayList()?.writeRelaysNorm()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null } ?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null }
?: key.relaysBeingUsed.keys ?: (key.relaysBeingUsed.keys + defaultRelays).toList()
relays.forEach { relays.forEach {
add(it, key.pubkeyHex) add(it, key.pubkeyHex)
} }
// TODO, DESPERATION: TRY ALL THE CONNECTED RELAYS.
} }
} }
return perRelayKeys.map { return perRelayKeys.mapNotNull {
RelayBasedFilter( if (it.value.isNotEmpty()) {
relay = it.key, RelayBasedFilter(
filter = relay = it.key,
Filter( filter =
kinds = MetadataAndRelayListKinds, Filter(
authors = it.value.sorted(), kinds = MetadataAndRelayListKinds,
), authors = it.value.sorted(),
) ),
)
} else {
null
}
} }
} }
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
class UserLoaderSubAssembler( class UserLoaderSubAssembler(
client: NostrClient, client: NostrClient,
@@ -42,7 +43,11 @@ class UserLoaderSubAssembler(
} }
} }
val defaultRelays = mutableSetOf<NormalizedRelayUrl>()
keys.mapTo(mutableSetOf()) { it.account }.forEach { keys.mapTo(mutableSetOf()) { it.account }.forEach {
defaultRelays.addAll(it.followPlusAllMine.flow.value)
it.kind3FollowList.flow.value.authors.forEach { it.kind3FollowList.flow.value.authors.forEach {
val user = LocalCache.getOrCreateUser(it) val user = LocalCache.getOrCreateUser(it)
if (user.latestMetadata == null) { if (user.latestMetadata == null) {
@@ -55,7 +60,7 @@ class UserLoaderSubAssembler(
if (firstTimers.isEmpty()) return null if (firstTimers.isEmpty()) return null
return filterFindUserMetadataForKey(firstTimers) return filterFindUserMetadataForKey(firstTimers, defaultRelays)
} }
override fun distinct(key: UserFinderQueryState) = key.user override fun distinct(key: UserFinderQueryState) = key.user
@@ -20,17 +20,30 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterByAddress(address: NAddress) = fun filterByAddress(
filterMissingAddressables( address: NAddress,
setOf( default: Set<NormalizedRelayUrl>,
Address( ): List<RelayBasedFilter> {
address.kind, val note = LocalCache.getOrCreateAddressableNote(address.address())
address.author,
address.dTag, val list =
), mapOfSet {
), if (note.event == null) {
) potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.address)
}
}
}
return filterMissingAddressables(list)
}
@@ -22,5 +22,9 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassembli
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.filterFindUserMetadataForKey import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.filterFindUserMetadataForKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
fun filterByAuthor(pubKey: HexKey) = filterFindUserMetadataForKey(pubKey) fun filterByAuthor(
pubKey: HexKey,
defaultRelays: Set<NormalizedRelayUrl>,
) = filterFindUserMetadataForKey(pubKey, defaultRelays)
@@ -20,7 +20,40 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterByEvent(eventId: HexKey) = filterMissingEvents(setOf(eventId)) fun filterByEvent(
eventId: HexKey,
default: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val note = LocalCache.checkGetOrCreateNote(eventId) ?: return emptyList()
val list =
mapOfSet {
if (note !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
// loads threading that is event-based
note.replyTo?.forEach { parentNote ->
if (parentNote !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
}
}
return filterMissingEvents(list)
}
@@ -55,25 +55,27 @@ class SearchWatcherSubAssembler(
if (mySearchString.isBlank()) return null if (mySearchString.isBlank()) return null
val defaultRelays = key.account.followPlusAllMine.flow.value
val directFilters = val directFilters =
runCatching { runCatching {
if (Hex.isHex(mySearchString)) { if (Hex.isHex(mySearchString)) {
val key = Hex.decode(mySearchString).toHexKey() val hexKey = Hex.decode(mySearchString).toHexKey()
filterByAuthor(key) + filterByEvent(key) filterByAuthor(hexKey, defaultRelays) + filterByEvent(hexKey, defaultRelays)
} else { } else {
val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity
if (parsed != null) { if (parsed != null) {
cache.consume(parsed) cache.consume(parsed)
when (parsed) { when (parsed) {
is NSec -> filterByAuthor(parsed.toPubKeyHex()) is NSec -> filterByAuthor(parsed.toPubKeyHex(), defaultRelays)
is NPub -> filterByAuthor(parsed.hex) is NPub -> filterByAuthor(parsed.hex, defaultRelays)
is NProfile -> filterByAuthor(parsed.hex) is NProfile -> filterByAuthor(parsed.hex, defaultRelays)
is NNote -> filterByEvent(parsed.hex) is NNote -> filterByEvent(parsed.hex, defaultRelays)
is NEvent -> filterByEvent(parsed.hex) is NEvent -> filterByEvent(parsed.hex, defaultRelays)
is NEmbed -> emptyList() is NEmbed -> emptyList()
is NRelay -> emptyList() is NRelay -> emptyList()
is NAddress -> filterByAddress(parsed) is NAddress -> filterByAddress(parsed, defaultRelays)
else -> emptyList() else -> emptyList()
} }
} else { } else {
@@ -53,7 +53,7 @@ fun ThreadScreen(
) )
WatchLifecycleAndUpdateModel(feedViewModel) WatchLifecycleAndUpdateModel(feedViewModel)
ThreadFilterAssemblerSubscription(noteId, accountViewModel.dataSources().thread) ThreadFilterAssemblerSubscription(noteId, accountViewModel)
LoadNote(noteId, accountViewModel) { LoadNote(noteId, accountViewModel) {
if (it != null) { if (it != null) {
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class ThreadQueryState( class ThreadQueryState(
val eventId: HexKey, val eventId: HexKey,
val account: Account,
) )
class ThreadFilterAssembler( class ThreadFilterAssembler(
@@ -22,19 +22,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@Composable @Composable
fun ThreadFilterAssemblerSubscription( fun ThreadFilterAssemblerSubscription(
eventId: HexKey, eventId: HexKey,
accountViewModel: AccountViewModel,
) = ThreadFilterAssemblerSubscription(
eventId,
accountViewModel.account,
accountViewModel.dataSources().thread,
)
@Composable
fun ThreadFilterAssemblerSubscription(
eventId: HexKey,
account: Account,
filterAssembler: ThreadFilterAssembler, filterAssembler: ThreadFilterAssembler,
) { ) {
// different screens get different states // different screens get different states
// even if they are tracking the same tag. // even if they are tracking the same tag.
val state = val state =
remember(eventId) { remember(eventId) {
ThreadQueryState(eventId) ThreadQueryState(eventId, account)
} }
KeyDataSourceSubscription(state, filterAssembler) KeyDataSourceSubscription(state, filterAssembler)
@@ -24,34 +24,54 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterMissingEventsForThread(threadInfo: ThreadAssembler.ThreadInfo): List<RelayBasedFilter> { fun filterMissingEventsForThread(
val missingEvents = mutableSetOf<HexKey>() threadInfo: ThreadAssembler.ThreadInfo,
val missingAddresses = mutableSetOf<Address>() defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val missingEvents =
mapOfSet {
if (threadInfo.root.event == null && threadInfo.root !is AddressableNote) {
potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, threadInfo.root.idHex)
}
}
if (threadInfo.root.event == null) { threadInfo.allNotes.forEach {
if (threadInfo.root is AddressableNote) { if (it !is AddressableNote && it.event == null) {
missingAddresses.add(threadInfo.root.address) potentialRelaysToFindEvent(it).ifEmpty { defaultRelays }.forEach { relayUrl ->
} else { add(relayUrl, it.idHex)
missingEvents.add(threadInfo.root.idHex) }
} }
}
threadInfo.allNotes.forEach {
if (it.event == null) {
if (it is AddressableNote) {
missingAddresses.add(it.address)
} else {
missingEvents.add(it.idHex)
} }
} }
}
val missingEventsFilter = filterMissingEvents(missingEvents) ?: emptyList() val missingAddresses =
val missingAddressFilter = filterMissingAddressables(missingAddresses) ?: emptyList() mapOfSet {
if (threadInfo.root.event == null && threadInfo.root is AddressableNote) {
potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, threadInfo.root.address)
}
}
threadInfo.allNotes.forEach {
if (it is AddressableNote && it.event == null) {
potentialRelaysToFindAddress(it).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, it.address)
}
}
}
}
val missingEventsFilter = filterMissingEvents(missingEvents)
val missingAddressFilter = filterMissingAddressables(missingAddresses)
return missingEventsFilter + missingAddressFilter return missingEventsFilter + missingAddressFilter
} }
@@ -45,7 +45,8 @@ class ThreadEventLoaderSubAssembler(
since: SincePerRelayMap?, since: SincePerRelayMap?,
): List<RelayBasedFilter>? { ): List<RelayBasedFilter>? {
val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null
return filterMissingEventsForThread(branches) val defaultRelays = key.account.followPlusAllMine.flow.value
return filterMissingEventsForThread(branches, defaultRelays)
} }
override fun id(key: ThreadQueryState) = key.eventId override fun id(key: ThreadQueryState) = key.eventId
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.nip01Core.relay.filters package com.vitorpamplona.quartz.nip01Core.relay.filters
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
class Filter( class Filter(
val ids: List<String>? = null, val ids: List<String>? = null,
@@ -48,22 +47,13 @@ class Filter(
search: String? = this.search, search: String? = this.search,
) = Filter(ids, authors, kinds, tags, since, until, limit, search) ) = Filter(ids, authors, kinds, tags, since, until, limit, search)
init { fun isFilledFilter() =
// TODO: Remove this in production (ids != null && ids.isNotEmpty()) ||
ids?.forEach { (authors != null && authors.isNotEmpty()) ||
if (it.length != 64) throw IllegalArgumentException("Invalid id length $it on ${toJson()}") (kinds != null && kinds.isNotEmpty()) ||
} (tags != null && tags.isNotEmpty()) ||
authors?.forEach { (since != null) ||
if (it.length != 64) throw IllegalArgumentException("Invalid author length $it on ${toJson()}") (until != null) ||
} (limit != null) ||
tags?.get("#p")?.forEach { (search != null && search.isNotEmpty())
if (it.length != 64) throw IllegalArgumentException("Invalid p-tag length $it on ${toJson()}")
}
tags?.get("#e")?.forEach {
if (it.length != 64) throw IllegalArgumentException("Invalid e-tag length $it on ${toJson()}")
}
tags?.get("#a")?.forEach {
if (Address.parse(it) != null) throw IllegalArgumentException("Invalid a-tag length $it on ${toJson()}")
}
}
} }