From 8155cfee9d4a1ba7617089a448ffb3694ba53c65 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 20 Jan 2026 15:18:11 +0200 Subject: [PATCH] update metadata loading --- .../eoseManagers/PerUniqueIdEoseManager.kt | 1 + .../PerUserAndFollowListEoseManager.kt | 1 + .../eoseManagers/PerUserEoseManager.kt | 1 + .../SingleSubNoEoseCacheEoseManager.kt | 1 + .../RelaySubscriptionsCoordinator.kt | 2 +- .../account/AccountFilterAssembler.kt | 2 +- .../AccountFilterAssemblerSubscription.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../ChannelFinderFilterAssemblyGroup.kt | 2 +- ...ChannelFinderFilterAssemblySubscription.kt | 2 +- ...adataAndLiveActivityWatcherSubAssembler.kt | 2 +- .../event/EventFinderFilterAssembler.kt | 2 +- .../EventFinderFilterAssemblerSubscription.kt | 2 +- .../watchers/EventWatcherSubAssembler.kt | 2 +- .../NWCFinderFilterAssemblerSubscription.kt | 2 +- .../nwc/NWCPaymentFilterAssembler.kt | 2 +- .../user/UserFinderFilterAssembler.kt | 2 +- .../UserFinderFilterAssemblerSubscription.kt | 2 +- .../loaders/UserOutboxFinderSubAssembler.kt | 2 +- .../user/watchers/UserCardsSubAssembler.kt | 2 +- .../user/watchers/UserReportsSubAssembler.kt | 2 +- .../user/watchers/UserWatcherSubAssembler.kt | 2 +- .../searchCommand/SearchFilterAssembler.kt | 4 +- .../TextSearchDataSourceSubscription.kt | 2 +- .../UserSearchDataSourceSubscription.kt | 2 +- .../datasource/ChatroomFilterAssembler.kt | 2 +- .../ChatroomFilterAssemblerSubscription.kt | 2 +- .../datasource/ChannelFilterAssembler.kt | 2 +- .../ChannelFilterAssemblerSubscription.kt | 2 +- .../datasource/ChatroomListFilterAssembler.kt | 2 +- ...ChatroomListFilterAssemblerSubscription.kt | 2 +- .../CommunityFeedFilterSubAssembler.kt | 2 +- .../datasource/CommunityFilterAssembler.kt | 2 +- .../CommunityFilterAssemblerSubscription.kt | 2 +- .../datasource/DiscoveryFilterAssembler.kt | 2 +- .../DiscoveryFilterAssemblerSubscription.kt | 2 +- .../FollowPackFeedFilterAssembler.kt | 2 +- ...llowPackFeedFilterAssemblerSubscription.kt | 2 +- .../FollowPackFeedFilterSubAssembler.kt | 2 +- .../datasource/GeoHashFilterAssembler.kt | 2 +- .../GeoHashFilterAssemblerSubscription.kt | 2 +- .../datasource/HashtagFilterAssembler.kt | 2 +- .../HashtagFilterAssemblerSubscription.kt | 2 +- .../home/datasource/HomeFilterAssembler.kt | 2 +- .../HomeFilterAssemblerSubscription.kt | 2 +- .../datasource/UserProfileFilterAssembler.kt | 2 +- .../UserProfileFilterAssemblerSubscription.kt | 2 +- .../UserProfileMetadataFilterSubAssembler.kt | 2 +- .../datasources/ThreadFilterAssembler.kt | 2 +- .../ThreadFilterAssemblerSubscription.kt | 2 +- .../video/datasource/VideoFilterAssembler.kt | 2 +- .../VideoFilterAssemblerSubscription.kt | 2 +- .../assemblers/FeedMetadataCoordinator.kt | 221 ++++++++++++++++++ .../assemblers/MetadataFilterAssembler.kt | 93 ++++++++ .../assemblers/ReactionsFilterAssembler.kt | 92 ++++++++ .../ComposeSubscriptionManager.kt | 2 +- .../ComposeSubscriptionManagerControls.kt | 2 +- .../MutableComposeSubscriptionManager.kt | 2 +- .../eoseManagers/BaseEoseManager.kt | 14 +- .../relayClient/eoseManagers/IEoseManager.kt | 27 +++ .../eoseManagers/PerKeyEoseManager.kt | 162 +++++++++++++ .../eoseManagers/SingleSubEoseManager.kt | 6 +- .../relayClient/preload/MetadataPreloader.kt | 109 +++++++++ .../preload/MetadataRateLimiter.kt | 109 +++++++++ .../KeyDataSourceSubscription.kt | 8 +- .../PrioritizedSubscriptionQueue.kt | 129 ++++++++++ .../subscriptions/SubscriptionPriority.kt | 55 +++++ .../amethyst/commons/relays/EOSECache.kt | 144 ++++++++++++ .../commons/network/RelayConnectionManager.kt | 27 ++- .../vitorpamplona/amethyst/desktop/Main.kt | 27 ++- .../DesktopRelaySubscriptionsCoordinator.kt | 141 +++++++++++ .../amethyst/desktop/ui/BookmarksScreen.kt | 12 + .../amethyst/desktop/ui/FeedScreen.kt | 51 ++-- .../desktop/ui/NotificationsScreen.kt | 11 + .../amethyst/desktop/ui/SearchScreen.kt | 2 + .../amethyst/desktop/ui/ThreadScreen.kt | 47 +++- .../amethyst/desktop/ui/UserProfileScreen.kt | 2 + 77 files changed, 1490 insertions(+), 107 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/MetadataFilterAssembler.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ReactionsFilterAssembler.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/eoseManagers/BaseEoseManager.kt (88%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/IEoseManager.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/eoseManagers/SingleSubEoseManager.kt (95%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataPreloader.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataRateLimiter.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions}/KeyDataSourceSubscription.kt (87%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/PrioritizedSubscriptionQueue.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/SubscriptionPriority.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSECache.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 4c14a918d..eb9b1122b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relays.EOSEByKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index ff2b8411a..9152c9220 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 3b5583f38..5eff68dd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index 2a93ccfd9..a1136b4dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter 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 0265f6603..5df26f53b 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 @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManagerControls import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManagerControls import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index b62529dcc..30d303ce8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts.AccountDraftsEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt index 2745f17e7..b7b092c0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 4693c9e74..6067d32b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 5600979a5..c06c81764 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt index eebcea166..209fd7d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index 56781d1f7..c6cced14e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities.filterLiveStreamUpdatesByAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index db3d6bb25..a3bf87923 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt index d0ffe095e..a9c131539 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event 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.model.Note -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index ff3f946a6..efa768df6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.MutableTime diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index fbb130e33..caf176734 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt index b71822d98..9fd6d793a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index dc4240828..712a84916 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.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.composeSubscriptionManagers.ComposeSubscriptionManager 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt index 7e1450331..2bd9789fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint 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.model.User -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @SuppressLint("StateFlowValueCalledInComposition") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index 76108f540..a7dcadb85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index a8bdc42cd..030afe5d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -21,9 +21,9 @@ 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.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index 2b4843562..3b102fc38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -21,9 +21,9 @@ 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.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index d3dd7e145..414e831ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index 811cd7338..a29198da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager +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.composeSubscriptionManagers.MutableComposeSubscriptionManager -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableQueryState import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchWatcherSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt index 534621242..a38617087 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/UserSearchDataSourceSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/UserSearchDataSourceSubscription.kt index c6a337bee..a5744c98d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/UserSearchDataSourceSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/UserSearchDataSourceSubscription.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt index 96ef67020..41fd483a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt @@ -20,8 +20,8 @@ */ 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.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt index bb98b4d51..31249fafd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index 8beefc621..f90e7592a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt index 0dc9e50f8..6eca5a187 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datas import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt index 79174b783..a8be570de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssemblerSubscription.kt index 9c041e289..6104739fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt index 2007746cc..5ddb60842 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index 531a781bd..2a49e068a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt index ed4f4f3f1..c3d2f1fdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable fun CommunityFilterAssemblerSubscription( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index 9d9f83089..d5048b017 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -20,8 +20,8 @@ */ 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.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt index 5a2a10b0c..d34fdd0f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt index ef3f1a91a..f11ea2a23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt index c30204f15..7b18453a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasourc import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt index 80681a5f2..c7b018856 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.filterHomePostsByAuthors import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt index 8b80a73b5..ef29b850b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt index 49bbf8ef1..3e3cf3307 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index 09b7c405c..a64e66fc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 1bf1a8387..e1f882f2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index 4b749ce55..99e2ef0cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt index 34c15b570..21eda3d9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index 5a217020b..3f7b17701 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt index 94af9e02b..deecd3e27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable fun UserProfileFilterAssemblerSubscription( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt index 07c905301..9d3545f79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index e389d4501..cce418cd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt index 7954bda88..5360d95c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources 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.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index 435189d58..8f4c6b9e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -20,8 +20,8 @@ */ 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.composeSubscriptionManagers.ComposeSubscriptionManager 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt index 691cb8dbb..a0dffc8d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt new file mode 100644 index 000000000..87ab2a074 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt @@ -0,0 +1,221 @@ +/** + * 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.commons.relayClient.assemblers + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.relayClient.preload.MetadataPreloader +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.PrioritizedSubscriptionQueue +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubscriptionPriority +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import kotlinx.coroutines.CoroutineScope + +/** + * Coordinates metadata and reactions loading for feed items. + * Ensures metadata (display names, avatars) loads before reactions. + * + * Priority order: + * 1. METADATA - Display names, avatars (highest priority) + * 2. REACTIONS - Likes, zaps, reposts (second priority) + * + * Usage: + * ``` + * val coordinator = FeedMetadataCoordinator(client, scope, indexRelays, preloader) + * coordinator.start() + * + * // When feed loads new notes + * LaunchedEffect(notes) { + * coordinator.loadMetadataForNotes(notes) + * } + * ``` + */ +class FeedMetadataCoordinator( + private val client: INostrClient, + private val scope: CoroutineScope, + private val indexRelays: Set, + private val preloader: MetadataPreloader? = null, + private val onEvent: ((Event, NormalizedRelayUrl) -> Unit)? = null, +) { + private val priorityQueue = PrioritizedSubscriptionQueue(scope) + + // Track what we've already queued to avoid duplicates + private val queuedPubkeys = mutableSetOf() + private val queuedNoteIds = mutableSetOf() + + /** + * Start processing the subscription queue. + * Call once when coordinator is created. + */ + fun start() { + priorityQueue.start { filter -> + // Convert filter to relay-based map for all index relays + val filterMap = indexRelays.associateWith { listOf(filter) } + + // Create listener to pass events to the callback + val listener = + if (onEvent != null) { + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + onEvent.invoke(event, relay) + } + } + } else { + null + } + + client.openReqSubscription( + subId = newSubId(), + filters = filterMap, + listener = listener, + ) + } + } + + /** + * Load metadata and reactions for a list of notes. + * Metadata loads first (priority 1), then reactions (priority 2). + * + * @param notes The notes to load metadata/reactions for + */ + fun loadMetadataForNotes(notes: List) { + if (notes.isEmpty()) return + + // Extract unique authors that we haven't already queued + val authors = + notes + .mapNotNull { it.author?.pubkeyHex } + .filter { it !in queuedPubkeys } + .distinct() + + // Extract unique note IDs that we haven't already queued + val noteIds = + notes + .map { it.idHex } + .filter { it !in queuedNoteIds } + .distinct() + + // Queue metadata first (highest priority) + if (authors.isNotEmpty()) { + queuedPubkeys.addAll(authors) + + // Use preloader if available for rate-limited loading + if (preloader != null) { + notes.mapNotNull { it.author }.forEach { user -> + preloader.preloadForUser(user) + } + } else { + // Direct queue without rate limiting + val metadataFilter = + Filter( + kinds = listOf(MetadataEvent.KIND), + authors = authors, + limit = authors.size, + ) + priorityQueue.enqueue( + SubscriptionPriority.METADATA, + metadataFilter, + tag = "feed-metadata", + ) + } + } + + // Queue reactions second (lower priority) + if (noteIds.isNotEmpty()) { + queuedNoteIds.addAll(noteIds) + + val reactionsFilter = + Filter( + kinds = listOf(ReactionEvent.KIND), + tags = mapOf("e" to noteIds), + ) + priorityQueue.enqueue( + SubscriptionPriority.REACTIONS, + reactionsFilter, + tag = "feed-reactions", + ) + } + } + + /** + * Load metadata for specific pubkeys. + * Useful for loading follower/following metadata. + */ + fun loadMetadataForPubkeys(pubkeys: List) { + val newPubkeys = pubkeys.filter { it !in queuedPubkeys } + if (newPubkeys.isEmpty()) return + + queuedPubkeys.addAll(newPubkeys) + + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND), + authors = newPubkeys, + limit = newPubkeys.size, + ) + priorityQueue.enqueue( + SubscriptionPriority.METADATA, + filter, + tag = "pubkey-metadata", + ) + } + + /** + * Load reactions for specific note IDs. + */ + fun loadReactionsForNotes(noteIds: List) { + val newNoteIds = noteIds.filter { it !in queuedNoteIds } + if (newNoteIds.isEmpty()) return + + queuedNoteIds.addAll(newNoteIds) + + val filter = + Filter( + kinds = listOf(ReactionEvent.KIND), + tags = mapOf("e" to newNoteIds), + ) + priorityQueue.enqueue( + SubscriptionPriority.REACTIONS, + filter, + tag = "note-reactions", + ) + } + + /** + * Clear queued items. Call when switching feeds. + */ + fun clear() { + priorityQueue.clear() + queuedPubkeys.clear() + queuedNoteIds.clear() + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/MetadataFilterAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/MetadataFilterAssembler.kt new file mode 100644 index 000000000..fa148168a --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/MetadataFilterAssembler.kt @@ -0,0 +1,93 @@ +/** + * 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.commons.relayClient.assemblers + +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +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 + +/** + * Query state for metadata subscriptions. + * Groups pubkeys with their preferred index relays. + */ +data class MetadataQueryState( + val pubkeys: Set, + val indexRelays: Set, +) + +/** + * Subscribes to Kind 0 (user metadata) for a set of pubkeys. + * Used to load display names, avatars, and other profile information. + * + * This assembler: + * - Batches multiple pubkey requests into single subscription + * - Sends requests to index relays for efficient discovery + * - Caches EOSE to avoid re-fetching known metadata + */ +class MetadataFilterAssembler( + client: INostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun distinct(key: MetadataQueryState): Any = key.pubkeys.hashCode() + + override fun updateFilter( + keys: List, + since: SincePerRelayMap?, + ): List? { + // Collect all pubkeys and relays from all query states + val allPubkeys = mutableSetOf() + val allRelays = mutableSetOf() + + keys.forEach { state -> + allPubkeys.addAll(state.pubkeys) + allRelays.addAll(state.indexRelays) + } + + if (allPubkeys.isEmpty() || allRelays.isEmpty()) return null + + val pubkeyList = allPubkeys.toList() + + // Create filter for metadata (Kind 0) + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND), + authors = pubkeyList, + limit = pubkeyList.size, + ) + + // Apply since times per relay + return allRelays.map { relay -> + val sinceTime = since?.get(relay)?.time + val filterWithSince = + if (sinceTime != null) { + filter.copy(since = sinceTime) + } else { + filter + } + RelayBasedFilter(relay, filterWithSince) + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ReactionsFilterAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ReactionsFilterAssembler.kt new file mode 100644 index 000000000..213b4e34d --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ReactionsFilterAssembler.kt @@ -0,0 +1,92 @@ +/** + * 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.commons.relayClient.assemblers + +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +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.nip25Reactions.ReactionEvent + +/** + * Query state for reactions subscriptions. + * Groups note IDs with their preferred relays. + */ +data class ReactionsQueryState( + val noteIds: Set, + val relays: Set, +) + +/** + * Subscribes to Kind 7 (reactions) for a set of note IDs. + * Used to load like counts, zap counts, and other reactions. + * + * This assembler: + * - Batches multiple note ID requests into single subscription + * - Uses e-tags to filter reactions for specific notes + * - Caches EOSE to avoid re-fetching known reactions + */ +class ReactionsFilterAssembler( + client: INostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun distinct(key: ReactionsQueryState): Any = key.noteIds.hashCode() + + override fun updateFilter( + keys: List, + since: SincePerRelayMap?, + ): List? { + // Collect all note IDs and relays from all query states + val allNoteIds = mutableSetOf() + val allRelays = mutableSetOf() + + keys.forEach { state -> + allNoteIds.addAll(state.noteIds) + allRelays.addAll(state.relays) + } + + if (allNoteIds.isEmpty() || allRelays.isEmpty()) return null + + val noteIdList = allNoteIds.toList() + + // Create filter for reactions (Kind 7) targeting these notes via e-tags + val filter = + Filter( + kinds = listOf(ReactionEvent.KIND), + tags = mapOf("e" to noteIdList), + ) + + // Apply since times per relay + return allRelays.map { relay -> + val sinceTime = since?.get(relay)?.time + val filterWithSince = + if (sinceTime != null) { + filter.copy(since = sinceTime) + } else { + filter + } + RelayBasedFilter(relay, filterWithSince) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt index 4c9bb3f66..e6c6c8202 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt @@ -18,7 +18,7 @@ * 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.composeSubscriptionManagers +package com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers import java.util.concurrent.ConcurrentHashMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt index f26b1752d..7ee190b8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt @@ -18,7 +18,7 @@ * 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.composeSubscriptionManagers +package com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers interface ComposeSubscriptionManagerControls { fun invalidateKeys() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt index 9966f901d..aa890e82b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt @@ -18,7 +18,7 @@ * 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.composeSubscriptionManagers +package com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/BaseEoseManager.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/BaseEoseManager.kt index 970e79779..7e5322fef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/BaseEoseManager.kt @@ -18,10 +18,10 @@ * 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.eoseManagers +package com.vitorpamplona.amethyst.commons.relayClient.eoseManagers -import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.amethyst.service.BundledUpdate +import com.vitorpamplona.amethyst.commons.service.BundledUpdate +import com.vitorpamplona.amethyst.commons.utils.isDebug import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId @@ -29,18 +29,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscriptio import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers -interface IEoseManager { - fun invalidateFilters(ignoreIfDoing: Boolean = false) - - fun destroy() -} - abstract class BaseEoseManager( val client: INostrClient, val allKeys: () -> Set, val sampleTime: Long = 500, ) : IEoseManager { - protected val logTag: String = this.javaClass.simpleName + protected val logTag: String = this::class.simpleName ?: "BaseEoseManager" private val orchestrator = SubscriptionController(client) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/IEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/IEoseManager.kt new file mode 100644 index 000000000..787e9a64c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/IEoseManager.kt @@ -0,0 +1,27 @@ +/** + * 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.commons.relayClient.eoseManagers + +interface IEoseManager { + fun invalidateFilters(ignoreIfDoing: Boolean = false) + + fun destroy() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt new file mode 100644 index 000000000..4768f18c8 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt @@ -0,0 +1,162 @@ +/** + * 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.commons.relayClient.eoseManagers + +import com.vitorpamplona.amethyst.commons.relays.EOSECache +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Generic per-key EOSE manager that creates a subscription for each unique key. + * + * This query type creates a new relay subscription for every distinct key K + * extracted from query state T. It is ideal for screens that need separate + * subscriptions per entity (user, thread, etc.). + * + * @param T The query state type (e.g., ThreadQueryState) + * @param K The key type used to deduplicate subscriptions (e.g., String noteId) + * + * This class keeps EOSEs for each key for as long as possible and can be + * shared among multiple query states that map to the same key. + */ +abstract class PerKeyEoseManager( + client: INostrClient, + allKeys: () -> Set, + val invalidateAfterEose: Boolean = false, + cacheSize: Int = 200, +) : BaseEoseManager(client, allKeys) { + // EOSE cache keyed by K + private val latestEOSEs = EOSECache(cacheSize) + + // Map from key K to subscription ID + private val keySubscriptionMap = mutableMapOf() + + /** + * Get the since map for a query state's key. + */ + fun since(queryState: T): SincePerRelayMap? = latestEOSEs.since(extractKey(queryState)) + + /** + * Record a new EOSE for a query state. + */ + open fun newEose( + queryState: T, + relayUrl: NormalizedRelayUrl, + time: Long, + filters: List? = null, + ) { + latestEOSEs.newEose(extractKey(queryState), relayUrl, time) + if (invalidateAfterEose) { + invalidateFilters() + } + } + + /** + * Create a new subscription for a query state. + */ + open fun newSub(queryState: T): Subscription = + requestNewSubscription( + object : IRequestListener { + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + newEose(queryState, relay, TimeUtils.now(), forFilters) + } + + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (isLive) { + newEose(queryState, relay, TimeUtils.now(), forFilters) + } + } + }, + ) + + /** + * End a subscription for a key. + */ + open fun endSub( + key: K, + subId: String, + ) { + dismissSubscription(subId) + keySubscriptionMap.remove(key) + } + + /** + * Find or create a subscription for a query state. + */ + fun findOrCreateSubFor(queryState: T): Subscription { + val key = extractKey(queryState) + val subId = keySubscriptionMap[key] + return if (subId == null) { + newSub(queryState).also { keySubscriptionMap[key] = it.id } + } else { + getSubscription(subId) ?: newSub(queryState).also { keySubscriptionMap[key] = it.id } + } + } + + override fun updateSubscriptions(keys: Set) { + val uniqueByKey = keys.distinctBy { extractKey(it) } + + val updatedKeys = mutableSetOf() + + uniqueByKey.forEach { queryState -> + val key = extractKey(queryState) + val newFilters = updateFilter(queryState, since(queryState))?.ifEmpty { null } + findOrCreateSubFor(queryState).updateFilters(newFilters?.groupByRelay()) + updatedKeys.add(key) + } + + // Clean up subscriptions for keys no longer active + keySubscriptionMap.filter { it.key !in updatedKeys }.forEach { + endSub(it.key, it.value) + } + } + + /** + * Build filters for a query state. + * @return List of relay-based filters, or null to clear the subscription + */ + abstract fun updateFilter( + queryState: T, + since: SincePerRelayMap?, + ): List? + + /** + * Extract the deduplication key from a query state. + * Subscriptions are shared among query states with the same key. + */ + abstract fun extractKey(queryState: T): K +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt index 795af8564..04ab7376c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -18,10 +18,10 @@ * 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.eoseManagers +package com.vitorpamplona.amethyst.commons.relayClient.eoseManagers -import com.vitorpamplona.amethyst.service.relays.EOSERelayList -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relays.EOSERelayList +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataPreloader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataPreloader.kt new file mode 100644 index 000000000..14771a607 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataPreloader.kt @@ -0,0 +1,109 @@ +/** + * 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.commons.relayClient.preload + +import com.vitorpamplona.amethyst.commons.model.User + +/** + * Preloads user metadata and avatar images for feed items. + * Coordinates between metadata subscription (rate-limited) and image preloading. + * + * Priority order: + * 1. Metadata (display names, avatar URLs) - FIRST + * 2. Avatar images (prefetch when metadata arrives) - SECOND + */ +class MetadataPreloader( + private val rateLimiter: MetadataRateLimiter, + private val imagePrefetcher: ImagePrefetcher? = null, +) { + /** + * Queue users for metadata preloading. + * If user already has metadata, prefetch their avatar image. + * Otherwise, queue for metadata subscription. + */ + fun preloadForUsers(users: Collection) { + users.forEach { user -> + val metadata = user.info + if (metadata != null) { + // Already have metadata, prefetch avatar + metadata.picture?.let { avatarUrl -> + imagePrefetcher?.prefetch(avatarUrl) + } + } else { + // Need to fetch metadata first + rateLimiter.enqueue(user.pubkeyHex) + } + } + } + + /** + * Queue a single user for metadata preloading. + */ + fun preloadForUser(user: User) { + val metadata = user.info + if (metadata != null) { + metadata.picture?.let { avatarUrl -> + imagePrefetcher?.prefetch(avatarUrl) + } + } else { + rateLimiter.enqueue(user.pubkeyHex) + } + } + + /** + * Called when metadata arrives for a user. + * Triggers avatar image prefetch. + */ + fun onMetadataReceived(user: User) { + user.info?.picture?.let { avatarUrl -> + imagePrefetcher?.prefetch(avatarUrl) + } + } + + /** + * Prefetch avatar images for users that already have metadata. + */ + fun prefetchAvatars(users: Collection) { + users.forEach { user -> + user.info?.picture?.let { avatarUrl -> + imagePrefetcher?.prefetch(avatarUrl) + } + } + } +} + +/** + * Interface for image prefetching. + * Platform-specific implementations use Coil (Android) or similar (Desktop). + */ +interface ImagePrefetcher { + /** + * Prefetch an image URL into the cache. + */ + fun prefetch(url: String) + + /** + * Prefetch multiple image URLs. + */ + fun prefetchAll(urls: Collection) { + urls.forEach { prefetch(it) } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataRateLimiter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataRateLimiter.kt new file mode 100644 index 000000000..f359f0012 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/preload/MetadataRateLimiter.kt @@ -0,0 +1,109 @@ +/** + * 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.commons.relayClient.preload + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch + +/** + * Global rate limiter for metadata requests to prevent thundering herd on fast scroll. + * Batches pubkeys and processes at a controlled rate. + * + * @param maxRequestsPerSecond Maximum requests to process per second (default 20) + * @param scope CoroutineScope for processing + */ +class MetadataRateLimiter( + private val maxRequestsPerSecond: Int = 20, + private val scope: CoroutineScope, +) { + private val queue = Channel(Channel.BUFFERED) + private val processed = mutableSetOf() + + /** + * Enqueue a pubkey for metadata fetching. + * Duplicates within the same batch are automatically filtered. + */ + fun enqueue(pubkey: String) { + if (pubkey !in processed) { + queue.trySend(pubkey) + } + } + + /** + * Enqueue multiple pubkeys for metadata fetching. + */ + fun enqueueAll(pubkeys: Collection) { + pubkeys.forEach { enqueue(it) } + } + + /** + * Start processing the queue with rate limiting. + * @param onRequest Callback invoked for each pubkey to process + */ + fun start(onRequest: suspend (String) -> Unit) { + scope.launch { + val batch = mutableListOf() + queue.consumeAsFlow().collect { pubkey -> + if (pubkey !in processed) { + batch.add(pubkey) + processed.add(pubkey) + } + + // Process batch when we hit the limit or queue is empty + if (batch.size >= maxRequestsPerSecond) { + processBatch(batch, onRequest) + batch.clear() + delay(1000) // Wait 1 second before next batch + } + } + + // Process remaining + if (batch.isNotEmpty()) { + processBatch(batch, onRequest) + } + } + } + + private suspend fun processBatch( + batch: List, + onRequest: suspend (String) -> Unit, + ) { + batch.forEach { pubkey -> + onRequest(pubkey) + } + } + + /** + * Clear the processed set to allow re-fetching. + * Call this when switching accounts or clearing cache. + */ + fun reset() { + processed.clear() + } + + /** + * Check if a pubkey has already been processed. + */ + fun isProcessed(pubkey: String): Boolean = pubkey in processed +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/KeyDataSourceSubscription.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/KeyDataSourceSubscription.kt index 039b715fe..60cb75256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/KeyDataSourceSubscription.kt @@ -18,13 +18,13 @@ * 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 +package com.vitorpamplona.amethyst.commons.relayClient.subscriptions import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager -import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableQueryState +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState @Composable fun KeyDataSourceSubscription( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/PrioritizedSubscriptionQueue.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/PrioritizedSubscriptionQueue.kt new file mode 100644 index 000000000..8eac02945 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/PrioritizedSubscriptionQueue.kt @@ -0,0 +1,129 @@ +/** + * 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.commons.relayClient.subscriptions + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch + +/** + * A subscription request with its priority. + */ +data class PrioritizedFilter( + val priority: SubscriptionPriority, + val filter: Filter, + val tag: String? = null, +) + +/** + * Queue that processes subscription requests in priority order. + * Ensures metadata loads before reactions, reactions before replies, etc. + * + * Usage: + * ``` + * val queue = PrioritizedSubscriptionQueue(scope) + * queue.start { filter -> relayClient.subscribe(filter) } + * + * // Add subscriptions - they'll be processed in priority order + * queue.enqueue(SubscriptionPriority.METADATA, metadataFilter) + * queue.enqueue(SubscriptionPriority.REACTIONS, reactionsFilter) + * ``` + */ +class PrioritizedSubscriptionQueue( + private val scope: CoroutineScope, +) { + // Separate channels per priority for efficient processing + private val queues = + SubscriptionPriority.entries.associateWith { + Channel(Channel.BUFFERED) + } + + private var isRunning = false + + /** + * Enqueue a filter with the given priority. + */ + fun enqueue( + priority: SubscriptionPriority, + filter: Filter, + tag: String? = null, + ) { + queues[priority]?.trySend(PrioritizedFilter(priority, filter, tag)) + } + + /** + * Enqueue multiple filters with the same priority. + */ + fun enqueueAll( + priority: SubscriptionPriority, + filters: List, + tag: String? = null, + ) { + filters.forEach { enqueue(priority, it, tag) } + } + + /** + * Start processing the queue. + * Processes higher priority items first within each batch. + * + * @param onSubscribe Callback to execute the subscription + */ + fun start(onSubscribe: suspend (Filter) -> Unit) { + if (isRunning) return + isRunning = true + + // Process each priority queue in order + SubscriptionPriority.sortedByPriority().forEach { priority -> + scope.launch { + queues[priority]?.consumeAsFlow()?.collect { item -> + onSubscribe(item.filter) + } + } + } + } + + /** + * Process all pending items immediately in priority order. + * Useful for batch operations. + */ + suspend fun flush(onSubscribe: suspend (Filter) -> Unit) { + SubscriptionPriority.sortedByPriority().forEach { priority -> + val queue = queues[priority] ?: return@forEach + while (true) { + val item = queue.tryReceive().getOrNull() ?: break + onSubscribe(item.filter) + } + } + } + + /** + * Clear all pending items. + */ + fun clear() { + queues.values.forEach { channel -> + while (channel.tryReceive().isSuccess) { + // Drain the channel + } + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/SubscriptionPriority.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/SubscriptionPriority.kt new file mode 100644 index 000000000..b752ca47a --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/SubscriptionPriority.kt @@ -0,0 +1,55 @@ +/** + * 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.commons.relayClient.subscriptions + +/** + * Priority levels for relay subscriptions. + * Lower order = higher priority = processed first. + * + * Priority order ensures progressive rendering: + * 1. METADATA - Display names, avatars load first + * 2. REACTIONS - Like/zap counts load second + * 3. REPLIES - Reply counts + * 4. CONTENT - Additional content + */ +enum class SubscriptionPriority( + val order: Int, +) { + /** User metadata (Kind 0) - display names, avatars. Highest priority. */ + METADATA(1), + + /** Reactions (Kind 7) - likes, zaps, reposts */ + REACTIONS(2), + + /** Replies - reply counts and threads */ + REPLIES(3), + + /** Additional content - lower priority items */ + CONTENT(4), + ; + + companion object { + /** + * Get priorities sorted by order (highest priority first). + */ + fun sortedByPriority(): List = entries.sortedBy { it.order } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSECache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSECache.kt new file mode 100644 index 000000000..a4f5d4eaa --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSECache.kt @@ -0,0 +1,144 @@ +/** + * 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.commons.relays + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Generic EOSE cache keyed by any type K. + * KMP-compatible version (no LruCache dependency). + * + * For memory-constrained environments, consider using platform-specific + * LRU implementations in androidMain/jvmMain. + */ +open class EOSECache( + private val maxSize: Int = 200, +) { + private val cache = linkedMapOf() + private val lock = Any() + + fun addOrUpdate( + key: K, + relayUrl: NormalizedRelayUrl, + time: Long, + ) { + synchronized(lock) { + val relayList = cache[key] + if (relayList == null) { + // Evict oldest if at capacity + if (cache.size >= maxSize) { + cache.remove(cache.keys.first()) + } + val newList = EOSERelayList() + newList.addOrUpdate(relayUrl, time) + cache[key] = newList + } else { + relayList.addOrUpdate(relayUrl, time) + } + } + } + + fun since(key: K): SincePerRelayMap? = + synchronized(lock) { + cache[key]?.relayList?.toMutableMap() + } + + fun newEose( + key: K, + relayUrl: NormalizedRelayUrl, + time: Long, + ) = addOrUpdate(key, relayUrl, time) + + fun remove(key: K) { + synchronized(lock) { + cache.remove(key) + } + } + + fun clear() { + synchronized(lock) { + cache.clear() + } + } + + fun size(): Int = synchronized(lock) { cache.size } +} + +/** + * Two-level EOSE cache: outer key -> inner key -> relay list. + * Useful for user + list combinations. + */ +open class EOSETwoLevelCache( + private val outerMaxSize: Int = 20, + private val innerMaxSize: Int = 200, +) { + private val cache = linkedMapOf>() + private val lock = Any() + + fun addOrUpdate( + outerKey: K1, + innerKey: K2, + relayUrl: NormalizedRelayUrl, + time: Long, + ) { + synchronized(lock) { + val innerCache = cache[outerKey] + if (innerCache == null) { + // Evict oldest if at capacity + if (cache.size >= outerMaxSize) { + cache.remove(cache.keys.first()) + } + val newCache = EOSECache(innerMaxSize) + newCache.addOrUpdate(innerKey, relayUrl, time) + cache[outerKey] = newCache + } else { + innerCache.addOrUpdate(innerKey, relayUrl, time) + } + } + } + + fun since( + outerKey: K1, + innerKey: K2, + ): SincePerRelayMap? = + synchronized(lock) { + cache[outerKey]?.since(innerKey) + } + + fun newEose( + outerKey: K1, + innerKey: K2, + relayUrl: NormalizedRelayUrl, + time: Long, + ) = addOrUpdate(outerKey, innerKey, relayUrl, time) + + fun removeOuter(key: K1) { + synchronized(lock) { + cache.remove(key) + } + } + + fun clear() { + synchronized(lock) { + cache.clear() + } + } +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt index 8669b66e8..ff25e8f47 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt @@ -44,24 +44,27 @@ import kotlinx.coroutines.flow.asStateFlow open class RelayConnectionManager( websocketBuilder: WebsocketBuilder, ) : IRelayClientListener { - private val client = NostrClient(websocketBuilder) + private val _client = NostrClient(websocketBuilder) + + /** Exposes the underlying INostrClient for subscription coordinators */ + val client: com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient get() = _client private val _relayStatuses = MutableStateFlow>(emptyMap()) val relayStatuses: StateFlow> = _relayStatuses.asStateFlow() - val connectedRelays: StateFlow> = client.connectedRelaysFlow() - val availableRelays: StateFlow> = client.availableRelaysFlow() + val connectedRelays: StateFlow> = _client.connectedRelaysFlow() + val availableRelays: StateFlow> = _client.availableRelaysFlow() init { - client.subscribe(this) + _client.subscribe(this) } fun connect() { - client.connect() + _client.connect() } fun disconnect() { - client.disconnect() + _client.disconnect() } fun addRelay(url: String): NormalizedRelayUrl? { @@ -85,18 +88,18 @@ open class RelayConnectionManager( listener: IRequestListener? = null, ) { val filterMap = relays.associateWith { filters } - client.openReqSubscription(subId, filterMap, listener) + _client.openReqSubscription(subId, filterMap, listener) } fun unsubscribe(subId: String) { - client.close(subId) + _client.close(subId) } fun send( event: Event, relays: Set = connectedRelays.value, ) { - client.send(event, relays) + _client.send(event, relays) } /** @@ -118,7 +121,7 @@ open class RelayConnectionManager( if (relay !in availableRelays.value) { updateRelayStatus(relay) { it.copy(connected = false, error = null) } } - client.send(event, setOf(relay)) + _client.send(event, setOf(relay)) } /** @@ -135,7 +138,7 @@ open class RelayConnectionManager( updateRelayStatus(relay) { it.copy(connected = false, error = null) } } val filterMap = mapOf(relay to filters) - client.openReqSubscription( + _client.openReqSubscription( subId = subId, filters = filterMap, listener = @@ -159,7 +162,7 @@ open class RelayConnectionManager( relay: NormalizedRelayUrl, subId: String, ) { - client.close(subId) + _client.close(subId) } private fun updateRelayStatus( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index e5d06bd4d..a97db089c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.ui.relay.RelayStatusCard import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.desktop.ui.BookmarksScreen import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog import com.vitorpamplona.amethyst.desktop.ui.FeedScreen @@ -251,6 +252,17 @@ fun App( val accountState by accountManager.accountState.collectAsState() val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) } + // Subscriptions coordinator for metadata/reactions loading + val subscriptionsCoordinator = + remember(relayManager, localCache) { + DesktopRelaySubscriptionsCoordinator( + client = relayManager.client, + scope = scope, + indexRelays = relayManager.availableRelays.value, + localCache = localCache, + ) + } + // Try to load saved account on startup DisposableEffect(Unit) { scope.launch(Dispatchers.IO) { @@ -260,7 +272,12 @@ fun App( relayManager.addDefaultRelays() relayManager.connect() + + // Start subscriptions coordinator + subscriptionsCoordinator.start() + onDispose { + subscriptionsCoordinator.clear() relayManager.disconnect() } } @@ -296,6 +313,7 @@ fun App( accountManager = accountManager, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onShowComposeDialog = onShowComposeDialog, onShowReplyDialog = onShowReplyDialog, ) @@ -324,6 +342,7 @@ fun MainContent( accountManager: AccountManager, account: AccountState.LoggedIn, nwcConnection: Nip47WalletConnect.Nip47URINorm?, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator, onShowComposeDialog: () -> Unit, onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit, ) { @@ -429,6 +448,7 @@ fun MainContent( localCache = localCache, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onCompose = onShowComposeDialog, onNavigateToProfile = { pubKeyHex -> onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) @@ -454,6 +474,7 @@ fun MainContent( SearchScreen( localCache = localCache, relayManager = relayManager, + subscriptionsCoordinator = subscriptionsCoordinator, onNavigateToProfile = { pubKeyHex -> onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) }, @@ -467,6 +488,7 @@ fun MainContent( localCache = localCache, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onNavigateToProfile = { pubKeyHex -> onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) }, @@ -476,7 +498,7 @@ fun MainContent( onZapFeedback = onZapFeedback, ) DesktopScreen.Messages -> MessagesPlaceholder() - DesktopScreen.Notifications -> NotificationsScreen(relayManager, account) + DesktopScreen.Notifications -> NotificationsScreen(relayManager, account, subscriptionsCoordinator) DesktopScreen.MyProfile -> UserProfileScreen( pubKeyHex = account.pubKeyHex, @@ -484,6 +506,7 @@ fun MainContent( localCache = localCache, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onBack = { onScreenChange(DesktopScreen.Feed) }, onCompose = onShowComposeDialog, onNavigateToProfile = { pubKeyHex -> @@ -498,6 +521,7 @@ fun MainContent( localCache = localCache, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onBack = { onScreenChange(DesktopScreen.Feed) }, onCompose = onShowComposeDialog, onNavigateToProfile = { pubKeyHex -> @@ -512,6 +536,7 @@ fun MainContent( localCache = localCache, account = account, nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, onBack = { onScreenChange(DesktopScreen.Feed) }, onNavigateToProfile = { pubKeyHex -> onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt new file mode 100644 index 000000000..5076561bd --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -0,0 +1,141 @@ +/** + * 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.desktop.subscriptions + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.FeedMetadataCoordinator +import com.vitorpamplona.amethyst.commons.relayClient.preload.MetadataPreloader +import com.vitorpamplona.amethyst.commons.relayClient.preload.MetadataRateLimiter +import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope + +/** + * Desktop-specific relay subscriptions coordinator. + * Manages metadata and reactions loading with rate limiting and prioritization. + * + * This coordinator ensures: + * - Display names and avatars load before reactions + * - Metadata requests are rate-limited (20/sec) to avoid relay flooding + * - Subscriptions are batched efficiently + * + * Usage: + * ``` + * val coordinator = DesktopRelaySubscriptionsCoordinator( + * client = relayManager.client, + * scope = viewModelScope, + * indexRelays = relayManager.availableRelays.value, + * ) + * coordinator.start() + * + * // In screens: + * LaunchedEffect(notes) { + * coordinator.loadMetadataForNotes(notes) + * } + * ``` + */ +class DesktopRelaySubscriptionsCoordinator( + private val client: INostrClient, + private val scope: CoroutineScope, + private val indexRelays: Set, + private val localCache: DesktopLocalCache, +) { + // Rate limiter: 20 requests per second to avoid flooding relays + private val rateLimiter = MetadataRateLimiter(maxRequestsPerSecond = 20, scope = scope) + + // Preloader handles metadata + avatar prefetching + private val preloader = MetadataPreloader(rateLimiter, imagePrefetcher = null) + + // Feed metadata coordinator with priority queue + val feedMetadata = + FeedMetadataCoordinator( + client = client, + scope = scope, + indexRelays = indexRelays, + preloader = preloader, + onEvent = { event, _ -> + // Consume metadata events into local cache + if (event is MetadataEvent) { + localCache.consumeMetadata(event) + } + }, + ) + + /** + * Start the coordinator. + * Call once when app starts or user logs in. + */ + fun start() { + // Start rate limiter to process queued metadata requests + rateLimiter.start { pubkey -> + // When rate limiter dequeues a pubkey, subscribe to its metadata + client.openReqSubscription( + filters = + indexRelays.associateWith { + listOf( + com.vitorpamplona.quartz.nip01Core.relay.filters.Filter( + kinds = listOf(com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent.KIND), + authors = listOf(pubkey), + limit = 1, + ), + ) + }, + ) + } + + // Start feed metadata coordinator + feedMetadata.start() + } + + /** + * Load metadata and reactions for notes. + * Delegates to FeedMetadataCoordinator. + */ + fun loadMetadataForNotes(notes: List) { + feedMetadata.loadMetadataForNotes(notes) + } + + /** + * Load metadata for specific pubkeys. + */ + fun loadMetadataForPubkeys(pubkeys: List) { + feedMetadata.loadMetadataForPubkeys(pubkeys) + } + + /** + * Load reactions for specific notes. + */ + fun loadReactionsForNotes(noteIds: List) { + feedMetadata.loadReactionsForNotes(noteIds) + } + + /** + * Clear all queued requests. + * Call when switching accounts or during cleanup. + */ + fun clear() { + feedMetadata.clear() + rateLimiter.reset() + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt index fbc40ff75..c4222545e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark @@ -71,6 +72,7 @@ fun BookmarksScreen( localCache: DesktopLocalCache, account: AccountState.LoggedIn, nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, onNavigateToProfile: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {}, @@ -109,6 +111,16 @@ fun BookmarksScreen( } val privateEvents by privateEventState.items.collectAsState() + // Load metadata for bookmark authors via coordinator + LaunchedEffect(publicEvents, privateEvents, subscriptionsCoordinator) { + if (subscriptionsCoordinator != null) { + val pubkeys = (publicEvents + privateEvents).map { it.pubKey }.distinct() + if (pubkeys.isNotEmpty()) { + subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys) + } + } + } + // Subscribe to user's bookmark list (kind 30001) rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) { val configuredRelays = relayStatuses.keys diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index f6676ae14..2167b492e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -72,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.desktop.DesktopPreferences import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent @@ -146,6 +148,7 @@ fun FeedScreen( localCache: DesktopLocalCache, account: AccountState.LoggedIn? = null, nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, onCompose: () -> Unit = {}, onNavigateToProfile: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {}, @@ -168,9 +171,14 @@ fun FeedScreen( var feedMode by remember { mutableStateOf(DesktopPreferences.feedMode) } var followedUsers by remember { mutableStateOf>(emptySet()) } var zapsByEvent by remember { mutableStateOf>>(emptyMap()) } - var reactionsByEvent by remember { mutableStateOf>(emptyMap()) } - var repliesByEvent by remember { mutableStateOf>(emptyMap()) } - var repostsByEvent by remember { mutableStateOf>(emptyMap()) } + // Track reaction event IDs per target event to deduplicate + var reactionIdsByEvent by remember { mutableStateOf>>(emptyMap()) } + val reactionsByEvent = reactionIdsByEvent.mapValues { it.value.size } + // Track reply/repost event IDs per target event to deduplicate + var replyIdsByEvent by remember { mutableStateOf>>(emptyMap()) } + val repliesByEvent = replyIdsByEvent.mapValues { it.value.size } + var repostIdsByEvent by remember { mutableStateOf>>(emptyMap()) } + val repostsByEvent = repostIdsByEvent.mapValues { it.value.size } var bookmarkList by remember { mutableStateOf(null) } var bookmarkedEventIds by remember { mutableStateOf>(emptySet()) } @@ -355,9 +363,10 @@ fun FeedScreen( onEvent = { event, _, _, _ -> if (event is ReactionEvent) { val targetEventId = event.originalPost().firstOrNull() ?: return@createReactionsSubscription - reactionsByEvent = - reactionsByEvent.toMutableMap().apply { - this[targetEventId] = (this[targetEventId] ?: 0) + 1 + reactionIdsByEvent = + reactionIdsByEvent.toMutableMap().apply { + val existing = this[targetEventId] ?: emptySet() + this[targetEventId] = existing + event.id } } }, @@ -382,9 +391,10 @@ fun FeedScreen( .lastOrNull() ?.get(1) ?: return@createRepliesSubscription if (replyToId in eventIds) { - repliesByEvent = - repliesByEvent.toMutableMap().apply { - this[replyToId] = (this[replyToId] ?: 0) + 1 + replyIdsByEvent = + replyIdsByEvent.toMutableMap().apply { + val existing = this[replyToId] ?: emptySet() + this[replyToId] = existing + event.id } } }, @@ -404,9 +414,10 @@ fun FeedScreen( onEvent = { event, _, _, _ -> if (event is RepostEvent) { val targetEventId = event.boostedEventId() ?: return@createRepostsSubscription - repostsByEvent = - repostsByEvent.toMutableMap().apply { - this[targetEventId] = (this[targetEventId] ?: 0) + 1 + repostIdsByEvent = + repostIdsByEvent.toMutableMap().apply { + val existing = this[targetEventId] ?: emptySet() + this[targetEventId] = existing + event.id } } }, @@ -415,7 +426,21 @@ fun FeedScreen( // Subscribe to metadata for note authors (to enable zaps and populate search cache) val authorPubkeys = events.map { it.pubKey }.distinct() - rememberSubscription(relayStatuses, authorPubkeys, relayManager = relayManager) { + + // Use coordinator for rate-limited metadata loading (preferred) + LaunchedEffect(authorPubkeys, subscriptionsCoordinator) { + if (subscriptionsCoordinator != null && authorPubkeys.isNotEmpty()) { + subscriptionsCoordinator.loadMetadataForPubkeys(authorPubkeys) + } + } + + // Fallback subscription if coordinator not available + rememberSubscription(relayStatuses, authorPubkeys, subscriptionsCoordinator, relayManager = relayManager) { + // Skip if using coordinator + if (subscriptionsCoordinator != null) { + return@rememberSubscription null + } + val configuredRelays = relayStatuses.keys if (configuredRelays.isEmpty() || authorPubkeys.isEmpty()) { return@rememberSubscription null diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 77edace1c..8e5bd5ed4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -108,6 +110,7 @@ sealed class NotificationItem( fun NotificationsScreen( relayManager: DesktopRelayConnectionManager, account: AccountState.LoggedIn, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, ) { val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState() @@ -123,6 +126,14 @@ fun NotificationsScreen( } val notifications by notificationState.items.collectAsState() + // Load metadata for notification authors via coordinator + LaunchedEffect(notifications, subscriptionsCoordinator) { + if (subscriptionsCoordinator != null && notifications.isNotEmpty()) { + val pubkeys = notifications.map { it.event.pubKey }.distinct() + subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys) + } + } + // Track EOSE to know when initial load is complete var eoseReceivedCount by remember { mutableStateOf(0) } val initialLoadComplete = eoseReceivedCount > 0 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 0257bf852..9ccb0cb4e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard import com.vitorpamplona.amethyst.commons.viewmodels.SearchBarState import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull @@ -76,6 +77,7 @@ import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull fun SearchScreen( localCache: DesktopLocalCache, relayManager: DesktopRelayConnectionManager, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, onNavigateToProfile: (String) -> Unit, onNavigateToThread: (String) -> Unit, onNavigateToHashtag: (String) -> Unit = {}, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index b73def3f0..f9c839456 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -68,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent @@ -87,6 +89,7 @@ fun ThreadScreen( localCache: DesktopLocalCache, account: AccountState.LoggedIn?, nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, onBack: () -> Unit, onNavigateToProfile: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {}, @@ -121,14 +124,31 @@ fun ThreadScreen( // Track zaps per event var zapsByEvent by remember(noteId) { mutableStateOf>>(emptyMap()) } - var reactionsByEvent by remember(noteId) { mutableStateOf>(emptyMap()) } - var repliesByEvent by remember(noteId) { mutableStateOf>(emptyMap()) } - var repostsByEvent by remember(noteId) { mutableStateOf>(emptyMap()) } + // Track reaction event IDs per target event to deduplicate + var reactionIdsByEvent by remember(noteId) { mutableStateOf>>(emptyMap()) } + val reactionsByEvent = reactionIdsByEvent.mapValues { it.value.size } + // Track reply/repost event IDs per target event to deduplicate + var replyIdsByEvent by remember(noteId) { mutableStateOf>>(emptyMap()) } + val repliesByEvent = replyIdsByEvent.mapValues { it.value.size } + var repostIdsByEvent by remember(noteId) { mutableStateOf>>(emptyMap()) } + val repostsByEvent = repostIdsByEvent.mapValues { it.value.size } // Bookmark state var bookmarkList by remember { mutableStateOf(null) } var bookmarkedEventIds by remember { mutableStateOf>(emptySet()) } + // Load metadata for thread authors via coordinator + LaunchedEffect(rootNote, replyEvents, subscriptionsCoordinator) { + if (subscriptionsCoordinator != null) { + val pubkeys = mutableListOf() + rootNote?.let { pubkeys.add(it.pubKey) } + pubkeys.addAll(replyEvents.map { it.pubKey }) + if (pubkeys.isNotEmpty()) { + subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys.distinct()) + } + } + } + // Subscribe to user's bookmark list rememberSubscription(relayStatuses, account, relayManager = relayManager) { val configuredRelays = relayStatuses.keys @@ -244,9 +264,10 @@ fun ThreadScreen( onEvent = { event, _, _, _ -> if (event is ReactionEvent) { val targetEventId = event.originalPost().firstOrNull() ?: return@createReactionsSubscription - reactionsByEvent = - reactionsByEvent.toMutableMap().apply { - this[targetEventId] = (this[targetEventId] ?: 0) + 1 + reactionIdsByEvent = + reactionIdsByEvent.toMutableMap().apply { + val existing = this[targetEventId] ?: emptySet() + this[targetEventId] = existing + event.id } } }, @@ -270,9 +291,10 @@ fun ThreadScreen( .lastOrNull() ?.get(1) ?: return@createRepliesSubscription if (replyToId in allEventIds) { - repliesByEvent = - repliesByEvent.toMutableMap().apply { - this[replyToId] = (this[replyToId] ?: 0) + 1 + replyIdsByEvent = + replyIdsByEvent.toMutableMap().apply { + val existing = this[replyToId] ?: emptySet() + this[replyToId] = existing + event.id } } }, @@ -292,9 +314,10 @@ fun ThreadScreen( onEvent = { event, _, _, _ -> if (event is RepostEvent) { val targetEventId = event.boostedEventId() ?: return@createRepostsSubscription - repostsByEvent = - repostsByEvent.toMutableMap().apply { - this[targetEventId] = (this[targetEventId] ?: 0) + 1 + repostIdsByEvent = + repostIdsByEvent.toMutableMap().apply { + val existing = this[targetEventId] ?: emptySet() + this[targetEventId] = existing + event.id } } }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 7b745ee98..54600dcfb 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent @@ -94,6 +95,7 @@ fun UserProfileScreen( localCache: DesktopLocalCache, account: AccountState.LoggedIn?, nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null, + subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, onBack: () -> Unit, onCompose: () -> Unit = {}, onNavigateToProfile: (String) -> Unit = {},