mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-01 04:26:15 +00:00
fix(relays): attribute every subscription to the account that asked for it
Filters carry an accountPubKey so the relay screens can group by account, but attribution only happened for keys implementing AccountScopedQuery — and ~50 query states held an `account` without declaring it, so the cast failed silently and their filters showed as unattributed. Two of the gaps were real bugs rather than display issues: - CashuWalletFilterAssembler took `keys.first().pubkey` while flat-mapping every account's relays, so with two wallets logged in the second was never subscribed and its inbox relays were queried for the first account's nutzaps. Now built per account. - UserReportsSubAssembler unioned every account's follow list into one per-relay map, asking one account's follows of another's outbox relays. Now one pass per account. Where a subscription genuinely pools accounts (outbox discovery, on-screen event watching, profile metadata), it is attributed only when a single account is asking rather than inventing an owner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b6808959cd
commit
4d53bbea9e
@@ -3191,9 +3191,14 @@ class Account(
|
||||
val filters =
|
||||
entries.flatMap { entry ->
|
||||
val state = concordSessions.sessionFor(entry.id)?.state?.value ?: return@flatMap emptyList()
|
||||
ConcordSubscriptionPlanner.channelPreviewFilters(entry, state, lastReadFor = { channelIdHex ->
|
||||
loadLastRead(concordChannelLastReadRoute(entry.id, channelIdHex))
|
||||
})
|
||||
ConcordSubscriptionPlanner.channelPreviewFilters(
|
||||
entry,
|
||||
state,
|
||||
lastReadFor = { channelIdHex ->
|
||||
loadLastRead(concordChannelLastReadRoute(entry.id, channelIdHex))
|
||||
},
|
||||
accountPubKey = userProfile().pubkeyHex,
|
||||
)
|
||||
}
|
||||
if (filters.isEmpty()) return
|
||||
val byRelay = filters.groupBy { it.relay }.mapValues { (_, group) -> group.map { it.filter } }
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.service.relayClient
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
||||
/**
|
||||
* A subscription query state that belongs to one logged-in account.
|
||||
*
|
||||
* Around 66 query-state classes already carry an `account`, but nothing tied them together, so a
|
||||
* subscription manager could not ask "whose subscription is this?" without knowing the concrete
|
||||
* type. That is why the Active Relay Subscriptions screen filed the home feed under "not attributed"
|
||||
* despite it being built from one specific person's follow list: the base manager checked for
|
||||
* `AccountQueryState` and the home feed uses `HomeQueryState`.
|
||||
*
|
||||
* Implement this on any query state whose subscriptions belong to a single account, and the base
|
||||
* managers attribute them automatically.
|
||||
*/
|
||||
interface AccountScopedQuery {
|
||||
val account: Account
|
||||
}
|
||||
+18
-1
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.attributedTo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEByKey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -114,7 +116,13 @@ abstract class PerUniqueIdEoseManager<T, U : Any>(
|
||||
|
||||
uniqueSubscribedAccounts.forEach {
|
||||
val mainKey = id(it)
|
||||
val newFilters = updateFilter(it, since(it))?.ifEmpty { null }
|
||||
val newFilters =
|
||||
updateFilter(it, since(it))
|
||||
?.ifEmpty { null }
|
||||
// Attribute to the account that owns this subscription, once, here — rather than
|
||||
// threading a pubkey through every filter builder underneath. Builders that already
|
||||
// know their account keep what they set.
|
||||
?.let { f -> accountPubKeyOf(it)?.let { pk -> f.attributedTo(pk) } ?: f }
|
||||
findOrCreateSubFor(it).updateFilters(newFilters?.groupByRelay())
|
||||
|
||||
updated.add(mainKey)
|
||||
@@ -131,4 +139,13 @@ abstract class PerUniqueIdEoseManager<T, U : Any>(
|
||||
): List<RelayBasedFilter>?
|
||||
|
||||
abstract fun id(key: T): U
|
||||
|
||||
/**
|
||||
* The account behind [key], when the key is account-scoped. Null for keys about other users.
|
||||
*
|
||||
* Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses
|
||||
* HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the
|
||||
* other under "not attributed" despite both being built from a single account's data.
|
||||
*/
|
||||
private fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex
|
||||
}
|
||||
|
||||
+18
-1
@@ -21,7 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.attributedTo
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -127,7 +129,13 @@ abstract class PerUserAndFollowListEoseManager<T, U : Any>(
|
||||
uniqueSubscribedAccounts.forEach {
|
||||
val user = user(it)
|
||||
val sub = findOrCreateSubFor(it)
|
||||
val newFilters = updateFilter(it, since(it))?.ifEmpty { null }
|
||||
val newFilters =
|
||||
updateFilter(it, since(it))
|
||||
?.ifEmpty { null }
|
||||
// Attribute to the account that owns this subscription, once, here — rather than
|
||||
// threading a pubkey through every filter builder underneath. Builders that already
|
||||
// know their account keep what they set.
|
||||
?.let { f -> accountPubKeyOf(it)?.let { pk -> f.attributedTo(pk) } ?: f }
|
||||
sub.updateFilters(newFilters?.groupByRelay())
|
||||
updated.add(user)
|
||||
}
|
||||
@@ -145,4 +153,13 @@ abstract class PerUserAndFollowListEoseManager<T, U : Any>(
|
||||
abstract fun user(key: T): User
|
||||
|
||||
abstract fun list(key: T): U
|
||||
|
||||
/**
|
||||
* The account behind [key], when the key is account-scoped. Null for keys about other users.
|
||||
*
|
||||
* Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses
|
||||
* HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the
|
||||
* other under "not attributed" despite both being built from a single account's data.
|
||||
*/
|
||||
private fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex
|
||||
}
|
||||
|
||||
+18
-1
@@ -21,7 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.attributedTo
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -113,7 +115,13 @@ abstract class PerUserEoseManager<T>(
|
||||
|
||||
uniqueSubscribedAccounts.forEach {
|
||||
val user = user(it)
|
||||
val newFilters = updateFilter(it, since(it))?.ifEmpty { null }
|
||||
val newFilters =
|
||||
updateFilter(it, since(it))
|
||||
?.ifEmpty { null }
|
||||
// Attribute to the account that owns this subscription, once, here — rather than
|
||||
// threading a pubkey through every filter builder underneath. Builders that already
|
||||
// know their account keep what they set.
|
||||
?.let { f -> accountPubKeyOf(it)?.let { pk -> f.attributedTo(pk) } ?: f }
|
||||
|
||||
findOrCreateSubFor(it).updateFilters(newFilters?.groupByRelay())
|
||||
|
||||
@@ -131,4 +139,13 @@ abstract class PerUserEoseManager<T>(
|
||||
): List<RelayBasedFilter>?
|
||||
|
||||
abstract fun user(key: T): User
|
||||
|
||||
/**
|
||||
* The account behind [key], when the key is account-scoped. Null for keys about other users.
|
||||
*
|
||||
* Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses
|
||||
* HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the
|
||||
* other under "not attributed" despite both being built from a single account's data.
|
||||
*/
|
||||
private fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex
|
||||
}
|
||||
|
||||
+18
-1
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.attributedTo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -66,11 +68,26 @@ abstract class SingleSubNoEoseCacheEoseManager<T>(
|
||||
|
||||
override fun updateSubscriptions(keys: Set<T>) {
|
||||
val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) }
|
||||
val newFilters = updateFilter(uniqueSubscribedAccounts)?.ifEmpty { null }
|
||||
val newFilters =
|
||||
updateFilter(uniqueSubscribedAccounts)
|
||||
?.ifEmpty { null }
|
||||
// Attribute to the account that owns this subscription, once, here — rather than
|
||||
// threading a pubkey through every filter builder underneath. Builders that already
|
||||
// know their account keep what they set.
|
||||
?.let { f -> accountPubKeyOf(uniqueSubscribedAccounts.firstOrNull())?.let { pk -> f.attributedTo(pk) } ?: f }
|
||||
sub.updateFilters(newFilters?.groupByRelay())
|
||||
}
|
||||
|
||||
abstract fun updateFilter(keys: List<T>): List<RelayBasedFilter>?
|
||||
|
||||
abstract fun distinct(key: T): Any
|
||||
|
||||
/**
|
||||
* The account behind [key], when the key is account-scoped. Null for keys about other users.
|
||||
*
|
||||
* Keyed on [AccountScopedQuery] rather than a concrete query-state type: the home feed uses
|
||||
* HomeQueryState, notifications use AccountQueryState, and checking one concrete class filed the
|
||||
* other under "not attributed" despite both being built from a single account's data.
|
||||
*/
|
||||
private fun accountPubKeyOf(key: Any?): String? = (key as? AccountScopedQuery)?.account?.userProfile()?.pubkeyHex
|
||||
}
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts.AccountDraftsEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.marmot.MarmotGroupEventsEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager
|
||||
@@ -38,10 +39,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
// This allows multiple screen to be listening to logged-in accounts.
|
||||
@Stable
|
||||
class AccountQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedContentStates: AccountFeedContentStates,
|
||||
val otherAccounts: Set<HexKey>,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Always-on account loaders: metadata, gift wraps, drafts, inbox-relay
|
||||
|
||||
+16
-1
@@ -159,6 +159,15 @@ class AccountFollowsLoaderSubAssembler(
|
||||
|
||||
val connectedRelays = client.connectedRelaysFlow().value
|
||||
|
||||
// Attributed only when one account is asking. The follow lists above are unioned across every
|
||||
// logged-in account and the relays are then picked from that union, so with several accounts
|
||||
// active no single one owns a given filter. Deduped by pubkey because `Account` uses identity
|
||||
// equality.
|
||||
val soleAccountPubKey =
|
||||
accounts
|
||||
.mapTo(mutableSetOf()) { it.userProfile().pubkeyHex }
|
||||
.singleOrNull()
|
||||
|
||||
val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried)
|
||||
|
||||
hasTried.removeEveryoneBut(users)
|
||||
@@ -167,7 +176,13 @@ class AccountFollowsLoaderSubAssembler(
|
||||
if (users.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter = ExplainedFilter(purpose = SubPurpose.FOLLOW_LISTS, kinds = listOf(AdvertisedRelayListEvent.KIND), authors = users.sorted()),
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.RELAY_LISTS,
|
||||
kinds = listOf(AdvertisedRelayListEvent.KIND),
|
||||
authors = users.sorted(),
|
||||
accountPubKey = soleAccountPubKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ class MarmotGroupEventsEoseManager(
|
||||
"(metadataRelays=${groupRelays?.size ?: 0}, usingFallback=${groupRelays.isNullOrEmpty()}): ${relaysForGroup.map { it.url }}"
|
||||
}
|
||||
for (relay in relaysForGroup) {
|
||||
result.add(RelayBasedFilter(relay = relay, filter = ExplainedFilter.of(filter, SubPurpose.ENCRYPTED_GROUPS, "MLS group messages")))
|
||||
result.add(RelayBasedFilter(relay = relay, filter = ExplainedFilter.of(filter, SubPurpose.ENCRYPTED_GROUPS, "MLS group messages", entityIds = listOf(groupId))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ class NwcNotificationsEoseManager(
|
||||
relay = wallet.uri.relayUri,
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.WALLET,
|
||||
purpose = SubPurpose.NWC,
|
||||
kinds = listOf(NwcNotificationEvent.KIND, NwcNotificationEvent.LEGACY_KIND),
|
||||
authors = listOf(wallet.uri.pubKeyHex),
|
||||
tags = mapOf("p" to listOf(signer.pubKey)),
|
||||
|
||||
+3
-2
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -32,8 +33,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@Stable
|
||||
class ChannelFinderQueryState(
|
||||
val channel: Channel,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ChannelFinderFilterAssemblyGroup(
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.AddressableAuthorRelayLoaderSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler
|
||||
@@ -35,8 +36,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@Stable
|
||||
class EventFinderQueryState(
|
||||
val note: Note,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class EventFinderFilterAssembler(
|
||||
|
||||
+12
-2
@@ -60,6 +60,16 @@ class EventWatcherSubAssembler(
|
||||
|
||||
lastNotesOnFilter = keys.map { it.note }
|
||||
|
||||
// Attributed only when one account is watching. The notes here are whatever is rendered, not
|
||||
// anything an account owns, so with several accounts active no single one is the honest owner —
|
||||
// and splitting would re-request the same replies/zaps once per account.
|
||||
// Deduped by pubkey, not by `Account`: that class uses identity equality, so two objects for
|
||||
// the same logged-in user would look like two accounts and suppress attribution entirely.
|
||||
val soleAccountPubKey =
|
||||
keys
|
||||
.mapTo(mutableSetOf()) { it.account.userProfile().pubkeyHex }
|
||||
.singleOrNull()
|
||||
|
||||
return groupByRelayPresence(lastNotesOnFilter, latestEOSEs)
|
||||
.map { group ->
|
||||
if (group.isNotEmpty()) {
|
||||
@@ -67,8 +77,8 @@ class EventWatcherSubAssembler(
|
||||
val events = group.mapNotNull { if (it !is AddressableNote) it else null }
|
||||
|
||||
listOfNotNull(
|
||||
filterRepliesAndReactionsToNotes(events, findMinimumEOSEs(events, latestEOSEs)),
|
||||
filterRepliesAndReactionsToAddresses(addressables, findMinimumEOSEs(addressables, latestEOSEs)),
|
||||
filterRepliesAndReactionsToNotes(events, findMinimumEOSEs(events, latestEOSEs), soleAccountPubKey),
|
||||
filterRepliesAndReactionsToAddresses(addressables, findMinimumEOSEs(addressables, latestEOSEs), soleAccountPubKey),
|
||||
).flatten()
|
||||
} else {
|
||||
emptyList()
|
||||
|
||||
+6
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
@@ -72,6 +73,7 @@ val TextNoteKindList = listOf(TextNoteEvent.KIND)
|
||||
fun filterRepliesAndReactionsToAddresses(
|
||||
keys: List<AddressableNote>,
|
||||
since: SincePerRelayMap?,
|
||||
accountPubKey: HexKey? = null,
|
||||
): List<RelayBasedFilter>? {
|
||||
if (keys.isEmpty()) return null
|
||||
|
||||
@@ -94,6 +96,7 @@ fun filterRepliesAndReactionsToAddresses(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = RepliesAndReactionsToAddressesKinds1,
|
||||
tags = mapOf("a" to sortedList),
|
||||
since = since,
|
||||
@@ -106,6 +109,7 @@ fun filterRepliesAndReactionsToAddresses(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = PostsAndChatMessagesToAddresses,
|
||||
tags = mapOf("a" to sortedList),
|
||||
since = since,
|
||||
@@ -118,6 +122,7 @@ fun filterRepliesAndReactionsToAddresses(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = DeletionKindList,
|
||||
tags = mapOf("a" to sortedList),
|
||||
since = since,
|
||||
@@ -130,6 +135,7 @@ fun filterRepliesAndReactionsToAddresses(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = TextNoteKindList,
|
||||
tags = mapOf("q" to sortedList),
|
||||
since = since,
|
||||
|
||||
+5
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
@@ -78,6 +79,7 @@ val RepliesAndReactionsKinds2 =
|
||||
fun filterRepliesAndReactionsToNotes(
|
||||
events: List<Note>,
|
||||
since: SincePerRelayMap?,
|
||||
accountPubKey: HexKey? = null,
|
||||
): List<RelayBasedFilter>? {
|
||||
if (events.isEmpty()) return null
|
||||
|
||||
@@ -101,6 +103,7 @@ fun filterRepliesAndReactionsToNotes(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = RepliesAndReactionsKinds,
|
||||
tags = mapOf("e" to sortedList),
|
||||
since = since,
|
||||
@@ -113,6 +116,7 @@ fun filterRepliesAndReactionsToNotes(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = RepliesAndReactionsKinds2,
|
||||
tags = mapOf("e" to sortedList),
|
||||
since = since,
|
||||
@@ -124,6 +128,7 @@ fun filterRepliesAndReactionsToNotes(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ENGAGEMENT,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = listOf(TextNoteEvent.KIND, CommentEvent.KIND),
|
||||
tags = mapOf("q" to sortedList),
|
||||
since = since,
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserOutboxFinderSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserCardsSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler
|
||||
@@ -36,8 +37,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineT
|
||||
@Stable
|
||||
class UserFinderQueryState(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class UserFinderFilterAssembler(
|
||||
|
||||
+27
-2
@@ -103,6 +103,18 @@ class UserOutboxFinderSubAssembler(
|
||||
val accounts = keys.mapTo(mutableSetOf()) { it.account }
|
||||
val connectedRelays = client.connectedRelaysFlow().value
|
||||
|
||||
// Attributed only when one account is asking. Unlike the reports sweep, the relay choice here
|
||||
// is made from the *union* of every logged-in account's tiers (`pickRelaysToLoadUsers` below),
|
||||
// and the users being resolved are whoever is on screen rather than anyone's follow list — so
|
||||
// with several accounts active there is no single honest owner for a given filter, and
|
||||
// splitting the sweep per account would re-issue the same lookups once per account.
|
||||
// Deduped by pubkey, not by `Account`: that class uses identity equality, so two objects for
|
||||
// the same logged-in user would look like two accounts and suppress attribution entirely.
|
||||
val soleAccountPubKey =
|
||||
accounts
|
||||
.mapTo(mutableSetOf()) { it.userProfile().pubkeyHex }
|
||||
.singleOrNull()
|
||||
|
||||
val perRelayKeysBoth =
|
||||
pickRelaysToLoadUsers(
|
||||
noOutboxList,
|
||||
@@ -118,7 +130,13 @@ class UserOutboxFinderSubAssembler(
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = ExplainedFilter(kinds = relayListKinds, authors = sortedUsers, purpose = SubPurpose.RELAY_LISTS),
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
kinds = relayListKinds,
|
||||
authors = sortedUsers,
|
||||
purpose = SubPurpose.RELAY_LISTS,
|
||||
accountPubKey = soleAccountPubKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -148,7 +166,14 @@ class UserOutboxFinderSubAssembler(
|
||||
fallbackRelays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter = ExplainedFilter(kinds = relayListKinds, authors = sortedAbandoned, purpose = SubPurpose.RELAY_LISTS, purposeDetail = "outbox discovery for users whose relay list we lost"),
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
kinds = relayListKinds,
|
||||
authors = sortedAbandoned,
|
||||
purpose = SubPurpose.RELAY_LISTS,
|
||||
purposeDetail = "outbox discovery for users whose relay list we lost",
|
||||
accountPubKey = soleAccountPubKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ fun filterReportsToKeysFromTrusted(
|
||||
trustedAccounts: List<HexKey>,
|
||||
relay: NormalizedRelayUrl,
|
||||
since: Long?,
|
||||
accountPubKey: HexKey? = null,
|
||||
): RelayBasedFilter? {
|
||||
if (targets.isEmpty() || trustedAccounts.isEmpty()) return null
|
||||
return RelayBasedFilter(
|
||||
@@ -43,6 +44,7 @@ fun filterReportsToKeysFromTrusted(
|
||||
purpose = SubPurpose.MODERATION,
|
||||
kinds = ReportKindList,
|
||||
authors = trustedAccounts,
|
||||
accountPubKey = accountPubKey,
|
||||
tags = mapOf("p" to targets.sorted()),
|
||||
since = since,
|
||||
),
|
||||
|
||||
+3
@@ -63,6 +63,7 @@ fun filterUserMetadataForKey(
|
||||
indexRelays: Set<NormalizedRelayUrl>,
|
||||
cannotConnectRelays: Set<NormalizedRelayUrl>,
|
||||
since: EOSEAccountFast<User>,
|
||||
accountPubKey: HexKey? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
val perRelayUsers =
|
||||
mapOfSet {
|
||||
@@ -114,6 +115,7 @@ fun filterUserMetadataForKey(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = UserMetadataForKeyKinds,
|
||||
authors = firstTimers.sorted(),
|
||||
),
|
||||
@@ -127,6 +129,7 @@ fun filterUserMetadataForKey(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = UserMetadataForKeyKinds,
|
||||
authors = updates.sorted(),
|
||||
since = minimumTime,
|
||||
|
||||
+9
@@ -70,6 +70,13 @@ class UserCardsSubAssembler(
|
||||
|
||||
val accounts = keys.mapTo(mutableSetOf()) { it.account }
|
||||
|
||||
// Attributed only when one account is asking: the trusted-author sets below are pooled across
|
||||
// accounts, so with several active none of them owns a given filter.
|
||||
val soleAccountPubKey =
|
||||
accounts
|
||||
.mapTo(mutableSetOf()) { it.userProfile().pubkeyHex }
|
||||
.singleOrNull()
|
||||
|
||||
val trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>> =
|
||||
mapOfSet {
|
||||
accounts.forEach { account ->
|
||||
@@ -92,12 +99,14 @@ class UserCardsSubAssembler(
|
||||
val trustedAccounts = trustedUsersInThisRelay.sorted()
|
||||
listOfNotNull(
|
||||
filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||
accountPubKey = soleAccountPubKey,
|
||||
targets = groups.usersWithoutEose.toHexSet(),
|
||||
trustedAccounts = trustedAccounts,
|
||||
relay = relay,
|
||||
since = null,
|
||||
),
|
||||
filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||
accountPubKey = soleAccountPubKey,
|
||||
targets = groups.usersWithEose.toHexSet(),
|
||||
trustedAccounts = trustedAccounts,
|
||||
relay = relay,
|
||||
|
||||
+15
-8
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.toHexSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
|
||||
@@ -31,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
|
||||
class UserReportsSubAssembler(
|
||||
client: INostrClient,
|
||||
@@ -65,14 +65,19 @@ class UserReportsSubAssembler(
|
||||
|
||||
if (lastUsersOnFilter.isEmpty()) return null
|
||||
|
||||
val trustedAccountsPerRelay =
|
||||
mapOfSet {
|
||||
accounts.map { it.declaredFollowsPerOutboxRelay.value }.forEach {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
// One pass per account, never a union. "Whose follows wrote this report" is the whole point of
|
||||
// the filter, so merging every logged-in account's follow list into one per-relay map both made
|
||||
// the result unattributable and asked one account's follows of another account's outbox relays.
|
||||
return accounts.flatMap { account -> filtersFor(account, lastUsersOnFilter) }.ifEmpty { null }
|
||||
}
|
||||
|
||||
return trustedAccountsPerRelay
|
||||
private fun filtersFor(
|
||||
account: Account,
|
||||
lastUsersOnFilter: Set<User>,
|
||||
): List<RelayBasedFilter> {
|
||||
val accountPubKey = account.userProfile().pubkeyHex
|
||||
|
||||
return account.declaredFollowsPerOutboxRelay.value
|
||||
.flatMap { (relay, trustedUsersInThisRelay) ->
|
||||
// this relay + accounts are where we could find reports.
|
||||
// we might have already loaded them, so let's separate new targets that were checked before from the others
|
||||
@@ -84,12 +89,14 @@ class UserReportsSubAssembler(
|
||||
trustedAccounts = trustedAccounts,
|
||||
relay = relay,
|
||||
since = null,
|
||||
accountPubKey = accountPubKey,
|
||||
),
|
||||
filterReportsToKeysFromTrusted(
|
||||
targets = groups.usersWithEose.toHexSet(),
|
||||
trustedAccounts = trustedAccounts,
|
||||
relay = relay,
|
||||
since = findMinimumEOSEsForUsers(groups.usersWithEose, relay),
|
||||
accountPubKey = accountPubKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+8
@@ -96,6 +96,13 @@ class UserWatcherSubAssembler(
|
||||
}
|
||||
|
||||
// assembles all index relays from all accounts
|
||||
// Attributed only when one account is looking: the index relays below are pooled across every
|
||||
// account and the users are whoever is on screen, so with several askers none of them owns it.
|
||||
val soleAccountPubKey =
|
||||
keys
|
||||
.mapTo(mutableSetOf()) { it.account.userProfile().pubkeyHex }
|
||||
.singleOrNull()
|
||||
|
||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
keys.mapTo(mutableSetOf()) { it.account }.forEach {
|
||||
indexRelays.addAll(
|
||||
@@ -110,6 +117,7 @@ class UserWatcherSubAssembler(
|
||||
indexRelays,
|
||||
failureTracker.cannotConnectRelays,
|
||||
latestEOSEs,
|
||||
soleAccountPubKey,
|
||||
).ifEmpty { null }
|
||||
|
||||
sub.updateFilters(newFilters?.groupByRelay())
|
||||
|
||||
+4
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchPostWatcherSubAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchUserWatcherSubAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -35,8 +36,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Stable
|
||||
class SearchQueryState(
|
||||
val searchQuery: MutableStateFlow<String>,
|
||||
val account: Account,
|
||||
) : MutableQueryState {
|
||||
override val account: Account,
|
||||
) : MutableQueryState,
|
||||
AccountScopedQuery {
|
||||
override fun flow(): Flow<String> = searchQuery
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -23,11 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.apps.recommendations.datas
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
class ProfileAppRecommendationsQueryState(
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ProfileAppRecommendationsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class ArticlesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ArticlesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class BadgesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class BadgesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,11 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
class ProfileBadgesQueryState(
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ProfileBadgesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CalendarsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class CalendarsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -22,14 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class ChatroomQueryState(
|
||||
val room: ChatroomKey,
|
||||
val account: Account,
|
||||
) {
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery {
|
||||
val listId = room.hashCode().toString()
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.actions.ConcordSubscriptionPlanner
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.launchChatFeedToggleObserver
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
@@ -34,8 +35,8 @@ import kotlinx.coroutines.Job
|
||||
|
||||
/** One screen's request to keep the user's joined Concord Channels live. */
|
||||
class ConcordChannelQueryState(
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Keeps every joined Concord community's planes live while a Concord-bearing
|
||||
@@ -81,7 +82,11 @@ class ConcordChannelSubAssembler(
|
||||
// per-filter result cap (the "Soapbox shows 1 of 12 channels" bug). See
|
||||
// [ConcordSubscriptionPlanner.controlIsolatedFilters].
|
||||
val all =
|
||||
ConcordSubscriptionPlanner.controlIsolatedFilters(entries, since = since) { entry ->
|
||||
ConcordSubscriptionPlanner.controlIsolatedFilters(
|
||||
entries,
|
||||
since = since,
|
||||
accountPubKey = account.userProfile().pubkeyHex,
|
||||
) { entry ->
|
||||
account.concordSessions
|
||||
.sessionFor(entry.id)
|
||||
?.state
|
||||
|
||||
+5
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
|
||||
@@ -44,10 +45,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One open Concord Channel whose older history the screen wants paged in. */
|
||||
class ConcordChannelHistoryQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val communityId: String,
|
||||
val channelId: String,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Mounts the on-demand **history** pager for whichever Concord Channel screen is open. The live
|
||||
@@ -133,6 +134,8 @@ class ConcordChannelHistorySubAssembler(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.COMMUNITY_CHATS,
|
||||
purposeDetail = "concord channel history",
|
||||
entityIds = listOf(key.communityId),
|
||||
kinds = listOf(ConcordStreamEnvelope.KIND_WRAP),
|
||||
// All epoch planes at once: the relay serves them interleaved by created_at, so
|
||||
// one backward cursor walks across the Refounding boundaries; "exhausted" then
|
||||
|
||||
+3
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.trackingListener
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.filterGroupNotificationsToPubkey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
@@ -40,9 +41,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One Buzz DM channel whose recent chat is kept live. */
|
||||
class BuzzDmJoinedChatTailQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val groupId: GroupId,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Always-on **live tail** for the recent chat of every Buzz DM channel the viewer belongs to — the DM
|
||||
|
||||
+3
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.trackingListener
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.filterGroupNotificationsToPubkey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
@@ -41,9 +42,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One joined group whose recent chat is kept live for the Messages-list preview. */
|
||||
class RelayGroupJoinedChatTailQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val groupId: GroupId,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Always-on **live tail** for the recent chat of every NIP-29 group the user has joined — the group
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.launchChatFeedToggleObserver
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
@@ -33,8 +34,8 @@ import kotlinx.coroutines.Job
|
||||
|
||||
/** One request to keep the relay-signed state of the user's joined groups live. */
|
||||
class RelayGroupJoinedStateQueryState(
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Keeps the relay-signed **state** of every group the user has joined live and current — name, picture,
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -40,9 +41,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One open NIP-29 group whose older history the chat screen wants paged in. */
|
||||
class RelayGroupOpenChatHistoryQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val groupId: GroupId,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Mounts the on-demand **history** pager for whichever NIP-29 group chat screen is open. The live
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.trackingListener
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -37,9 +38,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One open NIP-29 group whose recent chat the screen wants live. */
|
||||
class RelayGroupOpenChatTailQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val groupId: GroupId,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Per-open-group **live tail**: the recent chat window of the *currently open* group, `#h`-scoped on
|
||||
|
||||
+3
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -40,9 +41,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** One open NIP-29 group whose older forum threads the Threads tab wants paged in. */
|
||||
class RelayGroupOpenThreadsHistoryQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val groupId: GroupId,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Mounts the on-demand **history** pager for whichever NIP-29 group's Threads tab is open. The Threads
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class RelayGroupsDiscoveryQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class RelayGroupsDiscoveryFilterAssembler(
|
||||
|
||||
+3
-2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -31,8 +32,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
/** One screen's request for the full channel directory of a single relay. */
|
||||
class RelayGroupsOnRelayQueryState(
|
||||
val relay: NormalizedRelayUrl,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Subscribes to the relay-signed directory (kinds 39000-39003) of a single relay,
|
||||
|
||||
+3
-2
@@ -23,13 +23,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
@Stable
|
||||
class ChatroomListState(
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ChatroomListFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasourc
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CommunitiesListQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class CommunitiesListFilterAssembler(
|
||||
|
||||
+3
-2
@@ -22,16 +22,17 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class DiscoveryQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
class DiscoveryFilterAssembler(
|
||||
client: INostrClient,
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasour
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class BrowseEmojiSetsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class BrowseEmojiSetsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasourc
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class FollowPacksQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class FollowPacksFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class GitRepositoriesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class GitRepositoriesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@@ -29,8 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
class HashtagQueryState(
|
||||
val hashtag: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) {
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery {
|
||||
val lowercaseHashtag = hashtag.lowercase()
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class HighlightsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class HighlightsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -32,10 +33,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Stable
|
||||
class HomeQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedState: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class HomeFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class LiveStreamsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class LiveStreamsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class LongsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class LongsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class MusicPlaylistsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class MusicPlaylistsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class MusicTracksQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class MusicTracksFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
@@ -32,9 +33,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
* been granted permissions in the user's ledger.
|
||||
*/
|
||||
class ConnectedAppsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val authors: Set<HexKey>,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Live subscription for NIP-5D napplet manifests (kinds 15129/35129) while
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -33,9 +34,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||
* selection) and a scope.
|
||||
*/
|
||||
class NappletsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Subscribes to NIP-5D napplet manifests (kinds 15129/35129) while a napplet screen is open,
|
||||
|
||||
+3
-2
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwa
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -53,8 +54,8 @@ import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
|
||||
@Stable
|
||||
class NestRoomQueryState(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Single per-room sub-assembler. Issues two [Filter]s per outbox
|
||||
|
||||
+3
-2
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwa
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -51,8 +52,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresence
|
||||
@Stable
|
||||
class NestRoomLivenessQueryState(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Single per-room sub-assembler for the feed thumbnail liveness
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class NestsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class NestsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -33,9 +34,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||
* scope.
|
||||
*/
|
||||
class NsitesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
/**
|
||||
* Subscribes to NIP-5A static-site manifests (kinds 15128/35128) while an nSite screen is open,
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PicturesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class PicturesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PodcastEpisodesQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class PodcastEpisodesFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PodcastsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class PodcastsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PollsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class PollsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class ProductsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ProductsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PublicChatsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class PublicChatsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class ShortsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ShortsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class SoftwareAppsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class SoftwareAppsFilterAssembler(
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
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.quartz.nip01Core.core.HexKey
|
||||
@@ -32,8 +33,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@Stable
|
||||
class ThreadQueryState(
|
||||
val eventId: HexKey,
|
||||
val account: Account,
|
||||
)
|
||||
override val account: Account,
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class ThreadFilterAssembler(
|
||||
|
||||
+3
-2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -29,10 +30,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class VideoQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedState: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
class VideoFilterAssembler(
|
||||
client: INostrClient,
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class WorkoutsQueryState(
|
||||
val account: Account,
|
||||
override val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
) : AccountScopedQuery
|
||||
|
||||
@Stable
|
||||
class WorkoutsFilterAssembler(
|
||||
|
||||
+20
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntr
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityState
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId
|
||||
import com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -165,6 +166,7 @@ object ConcordSubscriptionPlanner {
|
||||
lastReadFor: (channelIdHex: String) -> Long,
|
||||
catchUpLimit: Int = 50,
|
||||
previewLimit: Int = 10,
|
||||
accountPubKey: HexKey? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
val authorsByChannel = LinkedHashMap<ConcordChannelId, MutableSet<String>>()
|
||||
val relaysByChannel = HashMap<ConcordChannelId, Set<NormalizedRelayUrl>>()
|
||||
@@ -180,6 +182,8 @@ object ConcordSubscriptionPlanner {
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.COMMUNITY_CHATS,
|
||||
purposeDetail = "concord community planes",
|
||||
entityIds = listOf(entry.id),
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = listOf(ConcordStreamEnvelope.KIND_WRAP),
|
||||
authors = authors.toList(),
|
||||
// -1 so the last-read message (created_at == lastRead) is itself returned:
|
||||
@@ -191,6 +195,8 @@ object ConcordSubscriptionPlanner {
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.COMMUNITY_CHATS,
|
||||
purposeDetail = "concord community planes",
|
||||
entityIds = listOf(entry.id),
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = listOf(ConcordStreamEnvelope.KIND_WRAP),
|
||||
authors = authors.toList(),
|
||||
limit = previewLimit,
|
||||
@@ -232,6 +238,7 @@ object ConcordSubscriptionPlanner {
|
||||
fun controlIsolatedFilters(
|
||||
entries: List<ConcordCommunityListEntry>,
|
||||
since: SincePerRelayMap?,
|
||||
accountPubKey: HexKey? = null,
|
||||
stateOf: (ConcordCommunityListEntry) -> ConcordCommunityState?,
|
||||
): List<RelayBasedFilter> {
|
||||
val controlSubs = controlPlaneSubs(entries)
|
||||
@@ -243,7 +250,7 @@ object ConcordSubscriptionPlanner {
|
||||
otherSubs += channelPlaneSubs(entry, state)
|
||||
}
|
||||
|
||||
return relayBasedFilters(controlSubs, since).orEmpty() + relayBasedFilters(otherSubs, since).orEmpty()
|
||||
return relayBasedFilters(controlSubs, since, accountPubKey).orEmpty() + relayBasedFilters(otherSubs, since, accountPubKey).orEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,10 +265,18 @@ object ConcordSubscriptionPlanner {
|
||||
fun relayBasedFilters(
|
||||
subs: List<ConcordPlaneSub>,
|
||||
since: SincePerRelayMap?,
|
||||
accountPubKey: HexKey? = null,
|
||||
): List<RelayBasedFilter>? {
|
||||
val authorsByRelay = HashMap<NormalizedRelayUrl, MutableSet<String>>()
|
||||
// Which communities each relay is being asked about. Collapsing planes into one filter per
|
||||
// relay is what makes the subscription screen unable to name them otherwise — the plane
|
||||
// pubkeys are stream keys, not something a user can recognise.
|
||||
val communitiesByRelay = HashMap<NormalizedRelayUrl, MutableSet<String>>()
|
||||
for (sub in subs) {
|
||||
for (relay in sub.relays) authorsByRelay.getOrPut(relay) { HashSet() }.add(sub.pubKeyHex)
|
||||
for (relay in sub.relays) {
|
||||
authorsByRelay.getOrPut(relay) { HashSet() }.add(sub.pubKeyHex)
|
||||
sub.channelId?.let { communitiesByRelay.getOrPut(relay) { HashSet() }.add(it.communityId) }
|
||||
}
|
||||
}
|
||||
if (authorsByRelay.isEmpty()) return null
|
||||
|
||||
@@ -271,6 +286,9 @@ object ConcordSubscriptionPlanner {
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.COMMUNITY_CHATS,
|
||||
purposeDetail = "concord live planes",
|
||||
entityIds = communitiesByRelay[relay]?.sorted(),
|
||||
accountPubKey = accountPubKey,
|
||||
// Stored plane wraps (1059) plus ephemeral ones (21059) — the latter carry the
|
||||
// live-only typing heartbeats a relay broadcasts but never stores.
|
||||
kinds = listOf(ConcordStreamEnvelope.KIND_WRAP, ConcordStreamEnvelope.KIND_WRAP_EPHEMERAL),
|
||||
|
||||
+2
-2
@@ -85,7 +85,7 @@ private class CashuMintDirectorySubAssembler(
|
||||
|
||||
val mintAnnouncements =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.WALLET,
|
||||
purpose = SubPurpose.MINT_DIRECTORY,
|
||||
purposeDetail = "mint directory",
|
||||
kinds = listOf(CashuMintEvent.KIND),
|
||||
)
|
||||
@@ -95,7 +95,7 @@ private class CashuMintDirectorySubAssembler(
|
||||
// recommendations here.
|
||||
val cashuRecommendations =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.WALLET,
|
||||
purpose = SubPurpose.MINT_DIRECTORY,
|
||||
purposeDetail = "mint directory",
|
||||
kinds = listOf(MintRecommendationEvent.KIND),
|
||||
tags = mapOf("k" to listOf(CashuMintEvent.KIND.toString())),
|
||||
|
||||
+22
-5
@@ -95,16 +95,31 @@ private class CashuWalletSubAssembler(
|
||||
) : SingleSubEoseManager<CashuWalletQueryState>(client, allKeys, invalidateAfterEose = true) {
|
||||
override fun distinct(key: CashuWalletQueryState): Any = key.pubkey
|
||||
|
||||
/**
|
||||
* One set of filters **per account**, never a merged one.
|
||||
*
|
||||
* [SingleSubEoseManager] hands over every distinct key, so with two wallets logged in this used
|
||||
* to take `keys.first().pubkey` while pooling *both* accounts' relays — the second account's
|
||||
* wallet was never subscribed, and its inbox relays were queried for the first account's
|
||||
* nutzaps. Keeping each account's pubkey with its own relay sets is also what lets the
|
||||
* subscription screen attribute these filters instead of piling them under "not attributed".
|
||||
*/
|
||||
override fun updateFilter(
|
||||
keys: List<CashuWalletQueryState>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? {
|
||||
if (keys.isEmpty()) return null
|
||||
return keys.flatMap { filtersFor(it, since) }.ifEmpty { null }
|
||||
}
|
||||
|
||||
val pubkey = keys.first().pubkey
|
||||
val ownEventRelays = keys.flatMap { it.ownEventRelays }.toSet()
|
||||
val inboxRelays = keys.flatMap { it.inboxRelays }.toSet()
|
||||
if (ownEventRelays.isEmpty() && inboxRelays.isEmpty()) return null
|
||||
private fun filtersFor(
|
||||
key: CashuWalletQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val pubkey = key.pubkey
|
||||
val ownEventRelays = key.ownEventRelays
|
||||
val inboxRelays = key.inboxRelays
|
||||
if (ownEventRelays.isEmpty() && inboxRelays.isEmpty()) return emptyList()
|
||||
|
||||
val ownedFilter =
|
||||
ExplainedFilter(
|
||||
@@ -123,13 +138,15 @@ private class CashuWalletSubAssembler(
|
||||
MintRecommendationEvent.KIND,
|
||||
),
|
||||
authors = listOf(pubkey),
|
||||
accountPubKey = pubkey,
|
||||
)
|
||||
|
||||
val inboundNutzapsFilter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.WALLET,
|
||||
purpose = SubPurpose.NUTZAP_INBOX,
|
||||
kinds = listOf(NutzapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
accountPubKey = pubkey,
|
||||
)
|
||||
|
||||
// Own NIP-60 events are read from the user's outbox; inbound nutzaps
|
||||
|
||||
+4
@@ -40,6 +40,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||
trustedAccounts: List<HexKey>,
|
||||
relay: NormalizedRelayUrl,
|
||||
since: Long?,
|
||||
accountPubKey: HexKey? = null,
|
||||
): RelayBasedFilter? {
|
||||
if (targets.isEmpty() || trustedAccounts.isEmpty()) return null
|
||||
return RelayBasedFilter(
|
||||
@@ -47,6 +48,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
accountPubKey = accountPubKey,
|
||||
kinds = ContactCardKindList,
|
||||
authors = trustedAccounts,
|
||||
// kind:30382 addresses the target user in the d-tag
|
||||
@@ -72,6 +74,8 @@ fun filterContactCardsByAuthorInTheRelay(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
// This variant fetches an account's OWN contact card, so the author is the owner.
|
||||
accountPubKey = author,
|
||||
kinds = ContactCardKindList,
|
||||
authors = listOf(author),
|
||||
limit = limit,
|
||||
|
||||
+8
@@ -37,6 +37,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
data class MetadataQueryState(
|
||||
val pubkeys: Set<HexKey>,
|
||||
val indexRelays: Set<NormalizedRelayUrl>,
|
||||
/** Which account is looking. Null when the caller has no account context (kept out of attribution). */
|
||||
val accountPubKey: HexKey? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -69,6 +71,11 @@ class MetadataFilterAssembler(
|
||||
|
||||
if (allPubkeys.isEmpty() || allRelays.isEmpty()) return null
|
||||
|
||||
// Attributed only when one account is looking. These pubkeys are whoever is rendered rather
|
||||
// than anything an account owns, and several query states are batched into one filter, so with
|
||||
// more than one asker there is no single honest owner.
|
||||
val soleAccountPubKey = keys.mapNotNullTo(mutableSetOf()) { it.accountPubKey }.singleOrNull()
|
||||
|
||||
val pubkeyList = allPubkeys.toList()
|
||||
|
||||
// Create filter for metadata (Kind 0)
|
||||
@@ -78,6 +85,7 @@ class MetadataFilterAssembler(
|
||||
authors = pubkeyList,
|
||||
limit = pubkeyList.size,
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
accountPubKey = soleAccountPubKey,
|
||||
)
|
||||
|
||||
// Apply since times per relay
|
||||
|
||||
+43
-10
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.relayClient.subscriptions
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
|
||||
/**
|
||||
@@ -66,12 +67,18 @@ class ExplainedFilter(
|
||||
/** Free-form extra context for [SubPurpose.OTHER] or for narrowing a bucket while debugging. */
|
||||
val purposeDetail: String? = null,
|
||||
/**
|
||||
* The thing this filter serves — a community, group, channel or mint id. Deliberately an **id,
|
||||
* not a name**: names change, are not always loaded when the filter is built, and would pin a
|
||||
* stale copy into a long-lived subscription. The UI resolves it against `LocalCache` at render
|
||||
* time, so it always shows the current name and shows nothing gracefully when unknown.
|
||||
* The things this filter serves — community, group, channel or mint ids.
|
||||
*
|
||||
* A **list**, because filters are routinely batched: relay-group state is fetched with one `#d`
|
||||
* filter per host relay carrying every joined group on it, so a single filter can legitimately
|
||||
* serve a dozen chats. Modelling one id would have forced either a wrong answer ("All") or a
|
||||
* filter-per-chat, which is far more REQs than the relays want.
|
||||
*
|
||||
* Ids, not names: names change, are often not loaded when the filter is built, and would pin a
|
||||
* stale copy into a long-lived subscription. The UI resolves them against `LocalCache` at render
|
||||
* time, so it shows the current name and degrades to a short id when unknown.
|
||||
*/
|
||||
val entityId: HexKey? = null,
|
||||
val entityIds: List<HexKey>? = null,
|
||||
/**
|
||||
* Which logged-in account asked for this. Several accounts are commonly active at once and they
|
||||
* do not share relay sets, so "why is this relay connected" is only answerable per account —
|
||||
@@ -93,7 +100,7 @@ class ExplainedFilter(
|
||||
until: Long?,
|
||||
limit: Int?,
|
||||
search: String?,
|
||||
) = ExplainedFilter(ids, authors, kinds, tags, tagsAll, since, until, limit, search, purpose, purposeDetail, entityId, accountPubKey)
|
||||
) = ExplainedFilter(ids, authors, kinds, tags, tagsAll, since, until, limit, search, purpose, purposeDetail, entityIds, accountPubKey)
|
||||
|
||||
companion object {
|
||||
/** Tags [filter] with a [purpose], preserving every protocol field. */
|
||||
@@ -101,7 +108,7 @@ class ExplainedFilter(
|
||||
filter: Filter,
|
||||
purpose: SubPurpose,
|
||||
detail: String? = null,
|
||||
entityId: HexKey? = null,
|
||||
entityIds: List<HexKey>? = null,
|
||||
accountPubKey: HexKey? = null,
|
||||
) = ExplainedFilter(
|
||||
filter.ids,
|
||||
@@ -115,7 +122,7 @@ class ExplainedFilter(
|
||||
filter.search,
|
||||
purpose,
|
||||
detail,
|
||||
entityId,
|
||||
entityIds,
|
||||
accountPubKey,
|
||||
)
|
||||
}
|
||||
@@ -135,8 +142,14 @@ fun Collection<Filter>.purposes(): Set<SubPurpose> = mapNotNullTo(mutableSetOf()
|
||||
* whose. Entries with no entity collapse to a single row for that purpose.
|
||||
*/
|
||||
fun Collection<Filter>.purposeEntities(): Set<PurposeEntity> =
|
||||
mapNotNullTo(mutableSetOf()) { filter ->
|
||||
(filter as? ExplainedFilter)?.let { PurposeEntity(it.purpose, it.entityId, it.accountPubKey, it.purposeDetail) }
|
||||
flatMapTo(mutableSetOf()) { filter ->
|
||||
val explained = filter as? ExplainedFilter ?: return@flatMapTo emptyList()
|
||||
val ids = explained.entityIds
|
||||
if (ids.isNullOrEmpty()) {
|
||||
listOf(PurposeEntity(explained.purpose, null, explained.accountPubKey, explained.purposeDetail))
|
||||
} else {
|
||||
ids.map { PurposeEntity(explained.purpose, it, explained.accountPubKey, explained.purposeDetail) }
|
||||
}
|
||||
}
|
||||
|
||||
/** A single "this relay is doing X, for Y, on behalf of account Z" fact. Ids only; names resolve in the UI. */
|
||||
@@ -146,3 +159,23 @@ data class PurposeEntity(
|
||||
val accountPubKey: HexKey? = null,
|
||||
val detail: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Stamps [accountPubKey] onto every tagged filter that does not already name one.
|
||||
*
|
||||
* Applied once where a subscription manager knows its account, rather than threading a pubkey
|
||||
* parameter through the ~200 filter builders below it. Filters that already name an account are left
|
||||
* alone, so a builder with better knowledge always wins.
|
||||
*/
|
||||
fun List<RelayBasedFilter>.attributedTo(accountPubKey: HexKey): List<RelayBasedFilter> =
|
||||
map { relayFilter ->
|
||||
val filter = relayFilter.filter
|
||||
if (filter is ExplainedFilter && filter.accountPubKey == null) {
|
||||
RelayBasedFilter(
|
||||
relay = relayFilter.relay,
|
||||
filter = ExplainedFilter.of(filter, filter.purpose, filter.purposeDetail, filter.entityIds, accountPubKey),
|
||||
)
|
||||
} else {
|
||||
relayFilter
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user