refactor(nests): collapse 4 per-room subs into one NestRoomFilterAssembler
The room screen used to fan out four parallel relay subscriptions
through separate ComposeSubscriptionManagers — RoomChat, RoomPresence,
RoomReactions, RoomAdminCommands — each opening its own REQ per outbox
relay. They all keyed on the same dimensions (roomATag, account) and
ran in the same place (NestActivityContent), so the multi-sub
fan-out was wire waste.
Replace with a single NestRoomFilterAssembler that issues two Filters
inside one REQ per relay:
- {kinds: [1311, 10312, 7], "#a": [roomATag]}
→ chat, presence, reactions on the shared a-tag gate
- {kinds: [4312], "#a": [roomATag], "#p": [localPubkey]}
→ admin commands; #p restriction stays on a separate Filter so
it doesn't over-restrict the chat / presence / reaction kinds
Net wire change: 4 REQs per relay → 1 REQ per relay with 2 Filters,
same event coverage. Same kinds, same #a/#p semantics, same outbox
relay set.
Files removed:
RoomChatFilterAssembler.kt
RoomPresenceFilterAssembler.kt
RoomPresenceFilterAssemblerSubscription.kt
RoomReactionsFilterAssembler.kt
RoomAdminCommandsFilterAssembler.kt
Files updated:
NestActivityContent.kt — one NestRoomFilterAssemblerSubscription
call replaces four. The per-kind LocalCache.observeEvents
LaunchedEffects stay; only the wire side got collapsed.
RelaySubscriptionsCoordinator.kt — register `nestRoom` instead of
the four old per-room assemblers.
NestsFilterAssembler (the rooms-list level subscription) is unchanged
— different keying (per-user, per-follow-list), different lifecycle.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
+3
-8
@@ -45,11 +45,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagF
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomAdminCommandsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomChatFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomPresenceFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomReactionsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssembler
|
||||
@@ -111,10 +108,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val publicChats = PublicChatsFilterAssembler(client)
|
||||
val liveStreams = LiveStreamsFilterAssembler(client)
|
||||
val nests = NestsFilterAssembler(client)
|
||||
val roomPresence = RoomPresenceFilterAssembler(client)
|
||||
val roomChat = RoomChatFilterAssembler(client)
|
||||
val roomReactions = RoomReactionsFilterAssembler(client)
|
||||
val roomAdminCommands = RoomAdminCommandsFilterAssembler(client)
|
||||
val nestRoom = NestRoomFilterAssembler(client)
|
||||
val longs = LongsFilterAssembler(client)
|
||||
val articles = ArticlesFilterAssembler(client)
|
||||
val badges = BadgesFilterAssembler(client)
|
||||
@@ -140,6 +134,7 @@ class RelaySubscriptionsCoordinator(
|
||||
followPacksList,
|
||||
liveStreams,
|
||||
nests,
|
||||
nestRoom,
|
||||
longs,
|
||||
articles,
|
||||
badges,
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
|
||||
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.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
||||
|
||||
/**
|
||||
* Per-room state for every wire subscription scoped to a single
|
||||
* Nest. Combines what used to be four parallel `*QueryState`s
|
||||
* (chat, presence, reactions, admin commands) into one — the four
|
||||
* relay REQs collapsed into a single subscription with two
|
||||
* [Filter]s per relay (see [NestRoomFilterSubAssembler.updateFilter]).
|
||||
*/
|
||||
@Stable
|
||||
class NestRoomQueryState(
|
||||
val roomATag: String,
|
||||
val localPubkey: String,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
/**
|
||||
* Single per-room sub-assembler. Issues two [Filter]s per outbox
|
||||
* relay on every key update:
|
||||
*
|
||||
* 1. `kinds=[1311, 10312, 7]`, `#a=[roomATag]` — the chat, presence
|
||||
* and reaction streams the room consumes. Bundling them into
|
||||
* one Filter is safe: a single Filter requires (kind ∈ kinds)
|
||||
* AND (every tag dimension matches), and these three streams
|
||||
* share the same `#a` gate.
|
||||
*
|
||||
* 2. `kinds=[4312]`, `#a=[roomATag]`, `#p=[localPubkey]` — admin
|
||||
* commands. Stays separate from (1) because the additional
|
||||
* `#p` constraint must NOT apply to the chat/presence/reaction
|
||||
* streams (they don't tag the local user). A second Filter on
|
||||
* the same REQ keeps them as one wire subscription.
|
||||
*
|
||||
* Replaces the previous fan-out of four sibling assemblers — chat,
|
||||
* presence, reactions, admin commands — that each opened their own
|
||||
* REQ. Net wire change: 4 REQs per relay → 1 REQ per relay with 2
|
||||
* Filters, same event coverage.
|
||||
*/
|
||||
class NestRoomFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<NestRoomQueryState>,
|
||||
) : PerUniqueIdEoseManager<NestRoomQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: NestRoomQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays = key.account.outboxRelays.flow.value
|
||||
return relays.flatMap { relay ->
|
||||
listOf(
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
MeetingRoomPresenceEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to listOf(key.roomATag)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(AdminCommandEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.roomATag), "p" to listOf(key.localPubkey)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: NestRoomQueryState): String = "${key.roomATag}|${key.localPubkey}"
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NestRoomFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<NestRoomQueryState>() {
|
||||
private val sub = NestRoomFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle-aware subscription. Stays open while the room screen is
|
||||
* in the tree; closes on dispose. One call replaces the previous
|
||||
* four (`RoomChatFilterAssemblerSubscription`,
|
||||
* `RoomPresenceFilterAssemblerSubscription`,
|
||||
* `RoomReactionsFilterAssemblerSubscription`,
|
||||
* `RoomAdminCommandsFilterAssemblerSubscription`).
|
||||
*/
|
||||
@Composable
|
||||
fun NestRoomFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
localPubkey: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = NestRoomFilterAssemblerSubscription(
|
||||
roomATag,
|
||||
localPubkey,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().nestRoom,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NestRoomFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
localPubkey: String,
|
||||
account: Account,
|
||||
filterAssembler: NestRoomFilterAssembler,
|
||||
) {
|
||||
val state =
|
||||
remember(roomATag, localPubkey) {
|
||||
NestRoomQueryState(roomATag, localPubkey, account)
|
||||
}
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
|
||||
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
|
||||
|
||||
/**
|
||||
* Per-screen state for the per-room kind-4312 admin command sub.
|
||||
* Filtered by both `#a` (the room) and `#p` (the local pubkey) so
|
||||
* the relay only sends commands actually targeting the local user.
|
||||
* Authority enforcement happens client-side in the room screen.
|
||||
*/
|
||||
@Stable
|
||||
class RoomAdminCommandsQueryState(
|
||||
val roomATag: String,
|
||||
val localPubkey: String,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
class RoomAdminCommandsFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<RoomAdminCommandsQueryState>,
|
||||
) : PerUniqueIdEoseManager<RoomAdminCommandsQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: RoomAdminCommandsQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays = key.account.outboxRelays.flow.value
|
||||
return relays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(AdminCommandEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.roomATag), "p" to listOf(key.localPubkey)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: RoomAdminCommandsQueryState): String = "${key.roomATag}|${key.localPubkey}"
|
||||
}
|
||||
|
||||
@Stable
|
||||
class RoomAdminCommandsFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<RoomAdminCommandsQueryState>() {
|
||||
private val sub = RoomAdminCommandsFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomAdminCommandsFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
localPubkey: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = RoomAdminCommandsFilterAssemblerSubscription(
|
||||
roomATag,
|
||||
localPubkey,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().roomAdminCommands,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RoomAdminCommandsFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
localPubkey: String,
|
||||
account: Account,
|
||||
filterAssembler: RoomAdminCommandsFilterAssembler,
|
||||
) {
|
||||
val state = remember(roomATag, localPubkey) { RoomAdminCommandsQueryState(roomATag, localPubkey, account) }
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
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.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
|
||||
/**
|
||||
* Per-screen state for the per-room kind-1311 live-chat
|
||||
* subscription. Mirror of [RoomPresenceQueryState] — the room's
|
||||
* `a`-tag is the address-pointer string (`30312:<host>:<roomId>`).
|
||||
*/
|
||||
@Stable
|
||||
class RoomChatQueryState(
|
||||
val roomATag: String,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
/**
|
||||
* REQs `kinds=[1311], #a=[roomATag]` against the user's outbox
|
||||
* relays so the chat panel sees every message tagged for this
|
||||
* room. Events arrive in [com.vitorpamplona.amethyst.model.LocalCache];
|
||||
* the room screen pipes them into `NestViewModel.onChatEvent`.
|
||||
*/
|
||||
class RoomChatFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<RoomChatQueryState>,
|
||||
) : PerUniqueIdEoseManager<RoomChatQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: RoomChatQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays = key.account.outboxRelays.flow.value
|
||||
return relays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.roomATag)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: RoomChatQueryState): String = key.roomATag
|
||||
}
|
||||
|
||||
@Stable
|
||||
class RoomChatFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<RoomChatQueryState>() {
|
||||
private val sub = RoomChatFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle-aware Composable subscription. Sub stays open while the
|
||||
* Composable is in the tree, then closes on dispose.
|
||||
*/
|
||||
@Composable
|
||||
fun RoomChatFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = RoomChatFilterAssemblerSubscription(
|
||||
roomATag,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().roomChat,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RoomChatFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
account: Account,
|
||||
filterAssembler: RoomChatFilterAssembler,
|
||||
) {
|
||||
val state = remember(roomATag) { RoomChatQueryState(roomATag, account) }
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* 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.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.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
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.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
||||
|
||||
/**
|
||||
* Per-screen state for the room-presence subscription. The room
|
||||
* `a`-tag is the address-pointer string (e.g.
|
||||
* `30312:<host-pubkey>:<roomId>`); the relay subscription returns
|
||||
* every kind-10312 event tagged with that `#a`.
|
||||
*/
|
||||
@Stable
|
||||
class RoomPresenceQueryState(
|
||||
val roomATag: String,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
/**
|
||||
* REQs `kinds=[10312], #a=[roomATag]` against every relay the user
|
||||
* already talks to. The events arrive in [com.vitorpamplona.amethyst.model.LocalCache]
|
||||
* via the normal client pipeline; the room screen observes them
|
||||
* with `LocalCache.observeEvents(...)` and feeds each event into
|
||||
* `NestViewModel.onPresenceEvent`.
|
||||
*
|
||||
* One assembler instance services every audio-room screen — the
|
||||
* `key` (= room a-tag) keeps overlapping screens (in unlikely
|
||||
* scenarios such as PiP + opened-from-deep-link) cleanly separated.
|
||||
*/
|
||||
class RoomPresenceFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<RoomPresenceQueryState>,
|
||||
) : PerUniqueIdEoseManager<RoomPresenceQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: RoomPresenceQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
// Use the user's outbox relay set; that's the same fan-out the
|
||||
// global rooms-list subscription rides on, so the presence
|
||||
// events arrive on the same connections as the room itself.
|
||||
val relays = key.account.outboxRelays.flow.value
|
||||
return relays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(MeetingRoomPresenceEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.roomATag)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: RoomPresenceQueryState): String = key.roomATag
|
||||
}
|
||||
|
||||
@Stable
|
||||
class RoomPresenceFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<RoomPresenceQueryState>() {
|
||||
private val sub = RoomPresenceFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
* Lifecycle-aware subscription for the per-room kind-10312 presence
|
||||
* stream. Mirror of [ThreadFilterAssemblerSubscription] — call this
|
||||
* from the room screen and the relay sub stays open while the
|
||||
* Composable is in the tree, then closes on dispose.
|
||||
*/
|
||||
@Composable
|
||||
fun RoomPresenceFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = RoomPresenceFilterAssemblerSubscription(
|
||||
roomATag,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().roomPresence,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RoomPresenceFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
account: Account,
|
||||
filterAssembler: RoomPresenceFilterAssembler,
|
||||
) {
|
||||
val state = remember(roomATag) { RoomPresenceQueryState(roomATag, account) }
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.nests.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
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.nip25Reactions.ReactionEvent
|
||||
|
||||
/**
|
||||
* Per-screen state for the per-room kind-7 reaction subscription.
|
||||
* Mirror of [RoomChatQueryState] / [RoomPresenceQueryState].
|
||||
*/
|
||||
@Stable
|
||||
class RoomReactionsQueryState(
|
||||
val roomATag: String,
|
||||
val account: Account,
|
||||
)
|
||||
|
||||
/**
|
||||
* REQs `kinds=[7], #a=[roomATag]` — the room's reaction stream the
|
||||
* floating-overlay UI consumes. Kind 9735 (zap receipts) is not yet
|
||||
* in the filter; the v1 overlay only renders kind-7 reactions.
|
||||
* Adding zaps later is a one-line filter change.
|
||||
*/
|
||||
class RoomReactionsFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<RoomReactionsQueryState>,
|
||||
) : PerUniqueIdEoseManager<RoomReactionsQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: RoomReactionsQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays = key.account.outboxRelays.flow.value
|
||||
return relays.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(ReactionEvent.KIND),
|
||||
tags = mapOf("a" to listOf(key.roomATag)),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id(key: RoomReactionsQueryState): String = key.roomATag
|
||||
}
|
||||
|
||||
@Stable
|
||||
class RoomReactionsFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<RoomReactionsQueryState>() {
|
||||
private val sub = RoomReactionsFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = sub.invalidateFilters()
|
||||
|
||||
override fun destroy() = sub.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle-aware Composable subscription. Sub stays open while the
|
||||
* Composable is in the tree, then closes on dispose.
|
||||
*/
|
||||
@Composable
|
||||
fun RoomReactionsFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = RoomReactionsFilterAssemblerSubscription(
|
||||
roomATag,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().roomReactions,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RoomReactionsFilterAssemblerSubscription(
|
||||
roomATag: String,
|
||||
account: Account,
|
||||
filterAssembler: RoomReactionsFilterAssembler,
|
||||
) {
|
||||
val state = remember(roomATag) { RoomReactionsQueryState(roomATag, account) }
|
||||
KeyDataSourceSubscription(state, filterAssembler)
|
||||
}
|
||||
+18
-20
@@ -39,10 +39,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.nests.NestForegroundService
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomAdminCommandsFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomChatFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomPresenceFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomReactionsFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -145,12 +142,19 @@ private fun NestActivityBody(
|
||||
|
||||
LaunchedEffect(viewModel, onStageKeys) { viewModel.updateSpeakers(onStageKeys) }
|
||||
|
||||
// Per-room kind-10312 presence: open the wire sub for the duration
|
||||
// of this Composable, observe matching events from LocalCache, feed
|
||||
// them into the VM's aggregator. Drives the listener counter +
|
||||
// (Tier 2) participant grid + (T1 #5) hand-raise queue.
|
||||
// Single per-room wire subscription. NestRoomFilterAssembler bundles
|
||||
// every #a-tagged kind we need (chat 1311, presence 10312,
|
||||
// reactions 7, admin commands 4312) into one REQ per relay — see
|
||||
// its docstring. Replaces the four parallel subscriptions this
|
||||
// composable used to open. The LaunchedEffect collectors below
|
||||
// still fan-out per-kind because each one feeds a different VM
|
||||
// hook; the wire side is what got collapsed.
|
||||
val roomATag = remember(event) { event.address().toValue() }
|
||||
RoomPresenceFilterAssemblerSubscription(roomATag, accountViewModel)
|
||||
val localPubkey = accountViewModel.account.signer.pubKey
|
||||
NestRoomFilterAssemblerSubscription(roomATag, localPubkey, accountViewModel)
|
||||
|
||||
// Per-room kind-10312 presence — drives the listener counter,
|
||||
// participant grid, and hand-raise queue.
|
||||
LaunchedEffect(viewModel, roomATag) {
|
||||
val filter =
|
||||
Filter(
|
||||
@@ -171,10 +175,7 @@ private fun NestActivityBody(
|
||||
}
|
||||
}
|
||||
|
||||
// Per-room kind-1311 live chat: open the wire sub for the duration
|
||||
// of this Composable, observe matching events from LocalCache, feed
|
||||
// them into the VM's chat ledger.
|
||||
RoomChatFilterAssemblerSubscription(roomATag, accountViewModel)
|
||||
// Per-room kind-1311 live chat — feeds the VM's chat ledger.
|
||||
LaunchedEffect(viewModel, roomATag) {
|
||||
val filter =
|
||||
Filter(
|
||||
@@ -186,11 +187,10 @@ private fun NestActivityBody(
|
||||
}
|
||||
}
|
||||
|
||||
// Per-room kind-7 reactions: same shape as chat. The VM's
|
||||
// sliding-window aggregator drops entries older than 30 s; the
|
||||
// 1-s tick below drives the eviction so the floating-up overlay
|
||||
// animation timing is one-place rather than per-component.
|
||||
RoomReactionsFilterAssemblerSubscription(roomATag, accountViewModel)
|
||||
// Per-room kind-7 reactions. The VM's sliding-window aggregator
|
||||
// drops entries older than 30 s; the 1-s tick below drives the
|
||||
// eviction so the floating-up overlay animation timing is one-
|
||||
// place rather than per-component.
|
||||
LaunchedEffect(viewModel, roomATag) {
|
||||
val filter =
|
||||
Filter(
|
||||
@@ -214,8 +214,6 @@ private fun NestActivityBody(
|
||||
// (not in the relay) — only honour kicks where the signer's
|
||||
// ParticipantTag.canSpeak() returns true on the active
|
||||
// kind-30312. nostrnests' UI does the same gating.
|
||||
val localPubkey = accountViewModel.account.signer.pubKey
|
||||
RoomAdminCommandsFilterAssemblerSubscription(roomATag, localPubkey, accountViewModel)
|
||||
LaunchedEffect(viewModel, roomATag, localPubkey) {
|
||||
val filter =
|
||||
Filter(
|
||||
|
||||
Reference in New Issue
Block a user