diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index c96d80a77e..d610d409e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt new file mode 100644 index 0000000000..b0b2d8da44 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt @@ -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, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: NestRoomQueryState, + since: SincePerRelayMap?, + ): List { + 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() { + 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) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomAdminCommandsFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomAdminCommandsFilterAssembler.kt deleted file mode 100644 index 23976e70ae..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomAdminCommandsFilterAssembler.kt +++ /dev/null @@ -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, -) : PerUniqueIdEoseManager(client, allKeys) { - override fun updateFilter( - key: RoomAdminCommandsQueryState, - since: SincePerRelayMap?, - ): List { - 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() { - 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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomChatFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomChatFilterAssembler.kt deleted file mode 100644 index 54ab650731..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomChatFilterAssembler.kt +++ /dev/null @@ -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::`). - */ -@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, -) : PerUniqueIdEoseManager(client, allKeys) { - override fun updateFilter( - key: RoomChatQueryState, - since: SincePerRelayMap?, - ): List { - 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() { - 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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssembler.kt deleted file mode 100644 index dcdc8b5ad6..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssembler.kt +++ /dev/null @@ -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::`); 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, -) : PerUniqueIdEoseManager(client, allKeys) { - override fun updateFilter( - key: RoomPresenceQueryState, - since: SincePerRelayMap?, - ): List { - // 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() { - private val sub = RoomPresenceFilterSubAssembler(client, ::allKeys) - - override fun invalidateKeys() = invalidateFilters() - - override fun invalidateFilters() = sub.invalidateFilters() - - override fun destroy() = sub.destroy() -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssemblerSubscription.kt deleted file mode 100644 index 64c9a75ccf..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomPresenceFilterAssemblerSubscription.kt +++ /dev/null @@ -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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomReactionsFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomReactionsFilterAssembler.kt deleted file mode 100644 index ff19c5e99e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/RoomReactionsFilterAssembler.kt +++ /dev/null @@ -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, -) : PerUniqueIdEoseManager(client, allKeys) { - override fun updateFilter( - key: RoomReactionsQueryState, - since: SincePerRelayMap?, - ): List { - 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() { - 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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestActivityContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestActivityContent.kt index a2174b444b..e65e371fcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestActivityContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestActivityContent.kt @@ -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(