diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 6f9bd7b57..ca2657137 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -878,15 +878,17 @@ class Account( } val liveHomeListAuthorsPerRelay: StateFlow>?> by lazy { - liveHomeListAuthorsPerRelayFlow.flowOn(Dispatchers.Default).stateIn( - scope, - SharingStarted.Eagerly, - authorsPerRelay( - liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(), - connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, - settings.torSettings.torType.value, - ).ifEmpty { null }, - ) + liveHomeListAuthorsPerRelayFlow + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + authorsPerRelay( + liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(), + connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, + settings.torSettings.torType.value, + ).ifEmpty { null }, + ) } val liveNotificationFollowLists: StateFlow by lazy { @@ -1677,6 +1679,7 @@ class Account( fun broadcast(note: Note) { note.event?.let { if (it is WrappedEvent && it.host != null) { + // download the event and send it. it.host?.let { Amethyst.instance.client.sendFilterAndStopOnFirstResponse( filters = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index a385b32ac..890e39350 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji +import com.vitorpamplona.amethyst.service.relays.EOSERelayList import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.filters.EOSETime import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -162,7 +162,7 @@ open class Note( var relays = listOf() private set - var lastReactionsDownloadTime: Map = emptyMap() + var lastReactionsDownloadTime: EOSERelayList = EOSERelayList() fun id() = Hex.decode(idHex) @@ -298,7 +298,7 @@ open class Note( zapPayments = mapOf() zapsAmount = BigDecimal.ZERO relays = listOf() - lastReactionsDownloadTime = emptyMap() + lastReactionsDownloadTime.clear() if (repliesChanged) flowSet?.replies?.invalidateData() if (reactionsChanged) flowSet?.reactions?.invalidateData() @@ -831,7 +831,7 @@ open class Note( } fun clearEOSE() { - lastReactionsDownloadTime = emptyMap() + lastReactionsDownloadTime.clear() } fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 0a7b9e5b0..147072dac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -57,14 +57,13 @@ class ThreadAssembler { // recursive val roots = - note.replyTo - ?.map { - if (it !in testedNotes) { - searchRoot(it, testedNotes) - } else { - null - } - }?.filterNotNull() + note.replyTo?.mapNotNull { + if (it !in testedNotes) { + searchRoot(it, testedNotes) + } else { + null + } + } if (roots != null && roots.isNotEmpty()) { return roots[0] @@ -79,6 +78,18 @@ class ThreadAssembler { val allNotes: ImmutableSet, ) + fun findRoot(noteId: String): Note? { + val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null + + return if (note.event != null) { + val thread = OnlyLatestVersionSet() + + searchRoot(note, thread) ?: note + } else { + note + } + } + fun findThreadFor(noteId: String): ThreadInfo? { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index 8eca307e1..24b30a1c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -50,9 +50,5 @@ class CacheClientConnector( private fun markAsSeen( eventId: HexKey, relay: RelayBriefInfoCache.RelayBriefInfo, - ) { - val note = LocalCache.getNoteIfExists(eventId) - - note?.addRelay(relay) - } + ) = LocalCache.getNoteIfExists(eventId)?.addRelay(relay) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt index d42614c46..1a3b41c82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt @@ -22,14 +22,14 @@ package com.vitorpamplona.amethyst.service.relayClient import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQueryState +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableQueryState @Composable fun KeyDataSourceSubscription( state: T, - dataSource: QueryBasedSubscriptionOrchestrator, + dataSource: ComposeSubscriptionManager, ) = DisposableEffect(state) { dataSource.subscribe(state) onDispose { @@ -40,7 +40,7 @@ fun KeyDataSourceSubscription( @Composable fun KeyDataSourceSubscription( state: T, - dataSource: MutableQueryBasedSubscriptionOrchestrator, + dataSource: MutableComposeSubscriptionManager, ) = DisposableEffect(state) { dataSource.subscribe(state) onDispose { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt index 1e1b7e566..628003dff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt @@ -21,10 +21,63 @@ package com.vitorpamplona.amethyst.service.relayClient import android.util.Log +import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger.Companion.TAG import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.LargeCache +import java.util.concurrent.atomic.AtomicInteger +import kotlin.collections.forEach +import kotlin.collections.get +import kotlin.concurrent.timer +import kotlin.text.get +import kotlin.text.set + +class FrameStat { + var eventCount = AtomicInteger(0) + var kinds = LargeCache() + + fun increment(kind: Int) { + eventCount.incrementAndGet() + + val kindCount = kinds.get(kind) + if (kindCount != null) { + kindCount.incrementAndGet() + } else { + kinds.put(kind, AtomicInteger(1)) + } + } + + fun hasAnything() = eventCount.get() > 0 + + fun reset() { + eventCount.set(0) + kinds.forEach { key, value -> + value.set(0) + } + } + + fun log() { + Log.d(TAG, "Events Per Second: ${eventCount.get()}") + kinds.forEach { key, value -> + if (value.get() > 0) { + Log.d(TAG, "-- Events Per Second Debug: $key $value") + } + } + } + + // Initialize the timer in the constructor. This ensures it starts when the + // ResettableCounter object is created. + init { + // Use a timer to reset the counter every second. + timer(name = "EventsPerSecondCounter", period = 1000, daemon = true) { + if (hasAnything()) { + log() + reset() + } + } + } +} /** * Listens to NostrClient's onNotify messages from the relay @@ -36,8 +89,7 @@ class RelaySpeedLogger( val TAG = RelaySpeedLogger::class.java.simpleName } - var time = TimeUtils.now() / 10 - var onEventCount: Long = 0L + var current = FrameStat() private val clientListener = object : NostrClient.Listener { @@ -49,14 +101,7 @@ class RelaySpeedLogger( arrivalTime: Long, afterEOSE: Boolean, ) { - val now = TimeUtils.now() / 10 - if (time == now) { - onEventCount++ - } else { - Log.d(TAG, "EPS: ${onEventCount / 10} ") - time = now - onEventCount = 0 - } + current.increment(event.kind) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt similarity index 64% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt index d06b01261..15f75b99f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt @@ -18,56 +18,49 @@ * 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.reqCommand +package com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator import java.util.concurrent.ConcurrentHashMap /** - * Creates a data source where the filters are derived from - * data (keys) that is being watched in the UI. + * This allows composables to directly register their queries + * to relays. There may be multiple duplications in these + * subscriptions since we do not control when screens are removed. */ -abstract class QueryBasedSubscriptionOrchestrator( - client: NostrClient, -) : SubscriptionOrchestrator(client) { - private var queries: ConcurrentHashMap = ConcurrentHashMap() +abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControls { + private var composeSubscriptions: ConcurrentHashMap = ConcurrentHashMap() // This is called by main. Keep it really fast. fun subscribe(query: T?) { if (query == null) return - val wasEmpty = queries.isEmpty() + val wasEmpty = composeSubscriptions.isEmpty() - queries.put(query, query) + composeSubscriptions.put(query, query) if (wasEmpty) { start() } - invalidateFilters() + invalidateKeys() } // This is called by main. Keep it really fast. fun unsubscribe(query: T?) { if (query == null) return - queries.remove(query) + composeSubscriptions.remove(query) - invalidateFilters() + invalidateKeys() - if (queries.isEmpty()) { + if (composeSubscriptions.isEmpty()) { stop() } } + fun allKeys() = composeSubscriptions.keys + fun forEachSubscriber(action: (T) -> Unit) { - queries.keys.forEach(action) + composeSubscriptions.keys.forEach(action) } - - final override fun updateSubscriptions() { - updateSubscriptions(queries.keys) - } - - abstract fun updateSubscriptions(keys: Set) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt new file mode 100644 index 000000000..1143bd325 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers + +interface ComposeSubscriptionManagerControls { + fun start() + + fun stop() + + fun invalidateKeys() + + fun invalidateFilters() + + fun destroy() + + fun printStats() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt similarity index 63% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt index d87e36811..f593abd17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt @@ -18,10 +18,8 @@ * 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.searchCommand +package com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -29,31 +27,31 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -interface MutableQueryState { - fun flow(): Flow<*> -} - /** - * Creates a data source where the filters are derived from - * data (keys) that is being watched in the UI. + * This allows composables to directly register their queries + * to relays. There may be multiple duplications in these + * subscriptions since we do not control when screens are removed. + * + * This is similar to QueryBasedSubscriptionOrchestrator, but it + * also allows the subscription itself to change over time as a + * flow, which trigger an update on the relay subscriptions */ -abstract class MutableQueryBasedSubscriptionOrchestrator( - client: NostrClient, +abstract class MutableComposeSubscriptionManager( val scope: CoroutineScope, -) : SubscriptionOrchestrator(client) { - private var queries: ConcurrentHashMap = ConcurrentHashMap() +) : ComposeSubscriptionManagerControls { + private var composeSubscriptions: ConcurrentHashMap = ConcurrentHashMap() // This is called by main. Keep it really fast. fun subscribe(query: T?) { if (query == null) return - val wasEmpty = queries.isEmpty() + val wasEmpty = composeSubscriptions.isEmpty() - queries[query]?.cancel() - queries[query] = + composeSubscriptions[query]?.cancel() + composeSubscriptions[query] = scope.launch { query.flow().collectLatest { - invalidateFilters() + invalidateKeys() } } @@ -61,30 +59,30 @@ abstract class MutableQueryBasedSubscriptionOrchestrator( start() } - invalidateFilters() + invalidateKeys() } // This is called by main. Keep it really fast. fun unsubscribe(query: T?) { if (query == null) return - queries[query]?.cancel() - queries.remove(query) + composeSubscriptions[query]?.cancel() + composeSubscriptions.remove(query) - invalidateFilters() + invalidateKeys() - if (queries.isEmpty()) { + if (composeSubscriptions.isEmpty()) { stop() } } + fun allKeys() = composeSubscriptions.keys + fun forEachSubscriber(action: (T) -> Unit) { - queries.keys.forEach(action) + composeSubscriptions.keys.forEach(action) } - - final override fun updateSubscriptions() { - updateSubscriptions(queries.keys) - } - - abstract fun updateSubscriptions(keys: Set) +} + +interface MutableQueryState { + fun flow(): Flow<*> } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt new file mode 100644 index 000000000..b33c18c2f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.eoseManagers + +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.datasources.SubscriptionController + +abstract class BaseEoseManager( + val client: NostrClient, + val allKeys: () -> Set, +) { + val orchestrator = + SubscriptionController(client) { + updateSubscriptions(allKeys()) + } + + abstract fun updateSubscriptions(keys: Set) + + fun printStats() = orchestrator.printStats(this.javaClass.simpleName) + + fun invalidateFilters() = orchestrator.invalidateFilters() + + fun start() = orchestrator.start() + + fun stop() = orchestrator.stop() + + fun destroy() = orchestrator.destroy() +} 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 new file mode 100644 index 000000000..20b2ed412 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.eoseManagers + +import com.vitorpamplona.amethyst.service.relays.EOSEFollowList +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.distinctBy + +/** + * This query type creates a new relay subscription for every SubID.id() + * that is subscribed into the keys. It is ideal for screen loading that + * can be shared among multiple logged-in users. + * + * This class keeps EOSEs for each SubID.id() for as long as possible and + * shares all EOSEs among all users. + */ +abstract class PerUniqueIdEoseManager( + client: NostrClient, + allKeys: () -> Set, + val invalidateAfterEose: Boolean = false, +) : BaseEoseManager(client, allKeys) { + // long term EOSE cache + private val latestEOSEs = EOSEFollowList() + + // map between each query Id and each subscription id + private val userSubscriptionMap = mutableMapOf() + + fun since(key: T) = latestEOSEs.since(id(key)) + + fun newEose( + key: T, + relayUrl: String, + time: Long, + ) { + latestEOSEs.newEose(id(key), relayUrl, time) + if (invalidateAfterEose) { + invalidateFilters() + } + } + + open fun newSub(key: T): Subscription = + orchestrator.requestNewSubscription { time, relayUrl -> + newEose(key, relayUrl, time) + } + + open fun endSub( + key: String, + subId: String, + ) { + orchestrator.dismissSubscription(subId) + } + + fun findOrCreateSubFor(key: T): Subscription { + val id = id(key) + var subId = userSubscriptionMap[id] + return if (subId == null) { + newSub(key).also { userSubscriptionMap[id] = it.id } + } else { + orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[id] = it.id } + } + } + + override fun updateSubscriptions(keys: Set) { + val uniqueSubscribedAccounts = keys.distinctBy { id(it) } + + val updated = mutableSetOf() + + uniqueSubscribedAccounts.forEach { + val mainKey = id(it) + findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + + updated.add(mainKey) + } + + userSubscriptionMap.forEach { + if (it.key !in updated) { + endSub(it.key, it.value) + } + } + } + + abstract fun updateFilter( + key: T, + since: Map?, + ): List? + + abstract fun id(key: T): String +} 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 new file mode 100644 index 000000000..b06f386d3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.eoseManagers + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.EOSEAccount +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * This query type creates a new relay subscription for every logged-in + * user AND each follow list they select in the top of the screen. + * + * It is ideal for the home, video, discovery and notification screens + * since they all have lists to choose from. + * + * This class keeps EOSEs for each user & list for as long as possible and + * does NOT share EOSEs with other users. Changing the list will not make the + * app reuse the EOSE because it assumes the filter is going to be different + */ +abstract class PerUserAndFollowListEoseManager( + client: NostrClient, + allKeys: () -> Set, + val invalidateAfterEose: Boolean = false, +) : BaseEoseManager(client, allKeys) { + private val latestEOSEs = EOSEAccount() + private val userSubscriptionMap = mutableMapOf() + + fun since(key: T) = latestEOSEs.since(user(key), list(key)) + + fun newEose( + key: T, + relayUrl: String, + time: Long, + ) = latestEOSEs.newEose(user(key), list(key), relayUrl, time) + + open fun newSub(key: T): Subscription = + orchestrator.requestNewSubscription { time, relayUrl -> + newEose(key, relayUrl, time) + if (invalidateAfterEose) { + invalidateFilters() + } + } + + open fun endSub( + key: User, + subId: String, + ) { + orchestrator.dismissSubscription(subId) + } + + fun findOrCreateSubFor(key: T): Subscription { + val user = user(key) + var subId = userSubscriptionMap[user] + return if (subId == null) { + newSub(key).also { userSubscriptionMap[user] = it.id } + } else { + orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } + } + } + + override fun updateSubscriptions(keys: Set) { + val uniqueSubscribedAccounts = keys.distinctBy { user(it) } + + val updated = mutableSetOf() + + uniqueSubscribedAccounts.forEach { + val user = user(it) + findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + + updated.add(user) + } + + userSubscriptionMap.forEach { + if (it.key !in updated) { + endSub(it.key, it.value) + } + } + } + + abstract fun updateFilter( + key: T, + since: Map?, + ): List? + + abstract fun user(key: T): User + + abstract fun list(key: T): String +} 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 new file mode 100644 index 000000000..47335edff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.eoseManagers + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.distinctBy + +/** + * This query type creates a new relay subscription for every distinct + * user that is subscribed into the keys. It is ideal for screens that + * CANNOT be shared among multiple logged-in users. + * + * This class keeps EOSEs for each user for as long as possible and + * does NOT share EOSEs with other users. The EOSEs are kept even when + * the subscription disappears and comes back later. + */ +abstract class PerUserEoseManager( + client: NostrClient, + allKeys: () -> Set, + val invalidateAfterEose: Boolean = false, +) : BaseEoseManager(client, allKeys) { + private val latestEOSEs = EOSEAccountFast() + private val userSubscriptionMap = mutableMapOf() + + fun since(key: T) = latestEOSEs.since(user(key)) + + fun newEose( + key: T, + relayUrl: String, + time: Long, + ) = latestEOSEs.newEose(user(key), relayUrl, time) + + open fun newSub(key: T): Subscription = + orchestrator.requestNewSubscription { time, relayUrl -> + newEose(key, relayUrl, time) + if (invalidateAfterEose) { + invalidateFilters() + } + } + + open fun endSub( + key: User, + subId: String, + ) { + orchestrator.dismissSubscription(subId) + } + + fun findOrCreateSubFor(key: T): Subscription { + val user = user(key) + var subId = userSubscriptionMap[user] + return if (subId == null) { + newSub(key).also { userSubscriptionMap[user] = it.id } + } else { + orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } + } + } + + override fun updateSubscriptions(keys: Set) { + val uniqueSubscribedAccounts = keys.distinctBy { user(it) } + + val updated = mutableSetOf() + + uniqueSubscribedAccounts.forEach { + val user = user(it) + findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + + updated.add(user) + } + + userSubscriptionMap.forEach { + if (it.key !in updated) { + endSub(it.key, it.value) + } + } + } + + abstract fun updateFilter( + key: T, + since: Map?, + ): List? + + abstract fun user(key: T): User +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt new file mode 100644 index 000000000..0ff2f01d4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.eoseManagers + +import com.vitorpamplona.amethyst.service.relays.EOSERelayList +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.distinctBy + +/** + * This query type creates only ONE relay subscription. It filters duplicates + * by DistinctById.uniqueId() but it doesn't create a new subscription for each. + * + * All filters are passed as a single sub. + * + * It is ideal for temporary filters, including event, user finding that must be + * disabled after the user is found. + * + * This class keeps EOSEs for as long as possible and + * shares all EOSEs among all users. + */ +abstract class SingleSubEoseManager( + client: NostrClient, + allKeys: () -> Set, + val invalidateAfterEose: Boolean = false, +) : BaseEoseManager(client, allKeys) { + // long term EOSE cache + private val latestEOSEs = EOSERelayList() + + fun since() = latestEOSEs.since() + + open fun newEose( + relayUrl: String, + time: Long, + ) = latestEOSEs.newEose(relayUrl, time) + + val sub = + orchestrator.requestNewSubscription { time, relayUrl -> + newEose(relayUrl, time) + if (invalidateAfterEose) { + invalidateFilters() + } + } + + override fun updateSubscriptions(keys: Set) { + val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) } + + sub.typedFilters = updateFilter(uniqueSubscribedAccounts, since())?.ifEmpty { null } + } + + abstract fun updateFilter( + key: List, + since: Map?, + ): List? + + abstract fun distinct(key: T): Any +} 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 3ab0a736e..55a22e45d 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 @@ -21,8 +21,9 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand 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.ChannelFinderFilterAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler @@ -57,7 +58,7 @@ class RelaySubscriptionsCoordinator( // loaders of content that is not yet in the device. // they are active when looking at events, users, channels. - val channelFinder = ChannelFinderFilterAssembler(client) + val channelFinder = ChannelFinderFilterAssemblyGroup(client) val eventFinder = EventFinderFilterAssembler(client) val userFinder = UserFinderFilterAssembler(client) @@ -77,7 +78,7 @@ class RelaySubscriptionsCoordinator( val nwc = NWCPaymentFilterAssembler(client) val all = - listOf( + listOf( account, home, chatroomList, @@ -103,5 +104,5 @@ class RelaySubscriptionsCoordinator( fun destroy() = all.forEach { it.destroy() } - fun printCounters() = all.forEach { it.stats.printCounter("${it.javaClass.simpleName}") } + fun printCounters() = all.forEach { it.printStats() } } 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 9361cd434..d1584d30b 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 @@ -21,302 +21,41 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSERelayList -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.blossom.BlossomServersEvent -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent -import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent -import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent -import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent -import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent -import com.vitorpamplona.quartz.utils.TimeUtils -// This allows multiple screen to be listening to tags, even the same tag +// This allows multiple screen to be listening to logged-in accounts. class AccountQueryState( val account: Account, val otherAccounts: Set, ) +/** + * This assembler loads everything eech account needs. + */ class AccountFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val latestEOSE = EOSERelayList() - var hasLoadedTheBasics: Boolean = false - - fun createAccountMetadataFilter(authorsHexes: List): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - ContactListEvent.KIND, - StatusEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - SearchRelayListEvent.KIND, - FileServersEvent.KIND, - BlossomServersEvent.KIND, - PrivateOutboxRelayListEvent.KIND, - ), - authors = authorsHexes, - limit = 20 * authorsHexes.size, - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + AccountMetadataEoseManager(client, ::allKeys), + AccountGiftWrapsEoseManager(client, ::allKeys), + AccountNotificationsEoseManager(client, ::allKeys), ) - fun createOtherAccountsBaseFilter(otherAccounts: List): TypedFilter? { - if (otherAccounts.isEmpty()) return null - return TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - ContactListEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - SearchRelayListEvent.KIND, - FileServersEvent.KIND, - BlossomServersEvent.KIND, - MuteListEvent.KIND, - PeopleListEvent.KIND, - ), - authors = otherAccounts, - limit = otherAccounts.size * 20, - ), - ) - } + override fun start() = group.forEach { it.start() } - fun createAccountSettingsFilter(authorsHexes: List): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - PeopleListEvent.KIND, - FollowListEvent.KIND, - MuteListEvent.KIND, - BadgeProfilesEvent.KIND, - EmojiPackSelectionEvent.KIND, - EphemeralChatListEvent.KIND, - ChannelListEvent.KIND, - ), - authors = authorsHexes, - limit = 100 * authorsHexes.size, - ), - ) + override fun stop() = group.forEach { it.stop() } - fun createAccountSettings2Filter(authorsHexes: List): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - AppSpecificDataEvent.KIND, - ), - authors = authorsHexes, - tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)), - limit = 2 * authorsHexes.size, - ), - ) + override fun invalidateKeys() = invalidateFilters() - fun createAccountLastPostsListFilter(authorsHexes: List): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - authors = authorsHexes, - limit = 500, - ), - ) + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - fun createAccountReportsAndDraftsFilter(authorsHexes: List): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - DraftEvent.KIND, - ReportEvent.KIND, - BookmarkListEvent.KIND, - ), - authors = authorsHexes, - since = latestEOSE.relayList, - ), - ) + override fun destroy() = group.forEach { it.start() } - fun createGiftWrapsToMeFilter(authorsHexes: List) = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(GiftWrapEvent.KIND), - tags = mapOf("p" to authorsHexes), - since = - latestEOSE.relayList.mapValues { - EOSETime(it.value.time - TimeUtils.twoDays()) - }, - ), - ) - - fun createNotificationFilter(authorsHexes: List) = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - LnZapPaymentResponseEvent.KIND, - ChannelMessageEvent.KIND, - EphemeralChatEvent.KIND, - BadgeAwardEvent.KIND, - ), - tags = mapOf("p" to authorsHexes), - limit = 4000, - since = latestEOSE.relayList, - ), - ) - - fun createNotificationFilter2(authorsHexes: List) = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - GitReplyEvent.KIND, - GitIssueEvent.KIND, - GitPatchEvent.KIND, - HighlightEvent.KIND, - CommentEvent.KIND, - CalendarDateSlotEvent.KIND, - CalendarTimeSlotEvent.KIND, - CalendarRSVPEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - InteractiveStorySceneEvent.KIND, - ), - tags = mapOf("p" to authorsHexes), - limit = 400, - since = latestEOSE.relayList, - ), - ) - - fun createNotificationFilter3(authorsHexes: List) = - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - PollNoteEvent.KIND, - ), - tags = mapOf("p" to authorsHexes), - limit = 100, - since = latestEOSE.relayList, - ), - ) - - fun mergeAllFilters( - mainAccounts: List, - otherAccounts: List, - ): List? = - if (hasLoadedTheBasics) { - // gets everything about the user logged in - listOfNotNull( - createAccountMetadataFilter(mainAccounts), - createAccountSettings2Filter(mainAccounts), - createNotificationFilter(mainAccounts), - createNotificationFilter2(mainAccounts), - createNotificationFilter3(mainAccounts), - createGiftWrapsToMeFilter(mainAccounts), - createAccountReportsAndDraftsFilter(mainAccounts), - createAccountSettingsFilter(mainAccounts), - createAccountLastPostsListFilter(mainAccounts), - createOtherAccountsBaseFilter(otherAccounts), - ).ifEmpty { null } - } else { - // just the basics. - listOf( - createAccountMetadataFilter(mainAccounts), - createAccountSettingsFilter(mainAccounts), - createAccountSettings2Filter(mainAccounts), - ).ifEmpty { null } - } - - val accountChannel = - requestNewSubscription { time, relayUrl -> - if (hasLoadedTheBasics) { - latestEOSE.addOrUpdate(relayUrl, time) - } else { - hasLoadedTheBasics = true - - invalidateFilters() - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val mainAccounts = mutableSetOf() - val otherAccounts = mutableSetOf() - - keys.forEach { - mainAccounts.add(it.account.userProfile().pubkeyHex) - otherAccounts.addAll(it.otherAccounts) - } - - accountChannel.typedFilters = mergeAllFilters(mainAccounts.toList(), (otherAccounts - mainAccounts).toList()) - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt new file mode 100644 index 000000000..fbffa31f7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class AccountMetadataEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun user(query: AccountQueryState) = query.account.userProfile() + + override fun updateFilter( + key: AccountQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterAccountInfoAndListsFromKey(user(key).pubkeyHex, since), + filterFollowsAndMutesFromKey(user(key).pubkeyHex, since), + filterDraftsAndReportsFromKey(user(key).pubkeyHex, since), + filterLastPostsFromKey(user(key).pubkeyHex, since), + filterBasicAccountInfoFromKeys(key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since), + ).flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt new file mode 100644 index 000000000..0e66330fa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.blossom.BlossomServersEvent +import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent + +fun filterAccountInfoAndListsFromKey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + ContactListEvent.KIND, + StatusEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + SearchRelayListEvent.KIND, + FileServersEvent.KIND, + BlossomServersEvent.KIND, + PrivateOutboxRelayListEvent.KIND, + ), + authors = listOf(pubkey), + limit = 20, + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(AppSpecificDataEvent.KIND), + authors = listOf(pubkey), + tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)), + limit = 1, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt new file mode 100644 index 000000000..90ebf3f2e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.blossom.BlossomServersEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent + +fun filterBasicAccountInfoFromKeys( + otherAccounts: List?, + since: Map?, +): List? { + if (otherAccounts == null || otherAccounts.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + ContactListEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + SearchRelayListEvent.KIND, + FileServersEvent.KIND, + BlossomServersEvent.KIND, + ), + authors = otherAccounts.toList(), + limit = otherAccounts.size * 8, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt new file mode 100644 index 000000000..d5ea07720 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent + +fun filterDraftsAndReportsFromKey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + DraftEvent.KIND, + ReportEvent.KIND, + BookmarkListEvent.KIND, + ), + authors = listOf(pubkey), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt new file mode 100644 index 000000000..4fd29f6f0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent + +fun filterFollowsAndMutesFromKey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + PeopleListEvent.KIND, + FollowListEvent.KIND, + MuteListEvent.KIND, + BadgeProfilesEvent.KIND, + EmojiPackSelectionEvent.KIND, + EphemeralChatListEvent.KIND, + ChannelListEvent.KIND, + ), + authors = listOf(pubkey), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt new file mode 100644 index 000000000..7bbe9252b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +fun filterLastPostsFromKey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + authors = listOf(pubkey), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt new file mode 100644 index 000000000..f72b36a69 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class AccountNotificationsEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun user(query: AccountQueryState) = query.account.userProfile() + + override fun updateFilter( + key: AccountQueryState, + since: Map?, + ): List? = filterNotificationsToPubkey(user(key).pubkeyHex, since) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt new file mode 100644 index 000000000..78d65c4f5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent +import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent +import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent + +fun filterNotificationsToPubkey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + LnZapPaymentResponseEvent.KIND, + ChannelMessageEvent.KIND, + EphemeralChatEvent.KIND, + BadgeAwardEvent.KIND, + ), + tags = mapOf("p" to listOf(pubkey)), + limit = 2000, + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + GitReplyEvent.KIND, + GitIssueEvent.KIND, + GitPatchEvent.KIND, + HighlightEvent.KIND, + CommentEvent.KIND, + CalendarDateSlotEvent.KIND, + CalendarTimeSlotEvent.KIND, + CalendarRSVPEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + InteractiveStorySceneEvent.KIND, + ), + tags = mapOf("p" to listOf(pubkey)), + limit = 400, + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(PollNoteEvent.KIND), + tags = mapOf("p" to listOf(pubkey)), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt new file mode 100644 index 000000000..03a6ffb86 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class AccountGiftWrapsEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun user(query: AccountQueryState) = query.account.userProfile() + + override fun updateFilter( + key: AccountQueryState, + since: Map?, + ): List? = filterGiftWrapsToPubkey(user(key).pubkeyHex, since) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt new file mode 100644 index 000000000..c98e2577d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +fun filterGiftWrapsToPubkey( + pubkey: HexKey?, + since: Map?, +): List? { + if (pubkey == null || pubkey.isEmpty()) return null + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(GiftWrapEvent.KIND), + tags = mapOf("p" to listOf(pubkey)), + since = + since?.mapValues { + EOSETime(it.value.time - TimeUtils.twoDays()) + }, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt deleted file mode 100644 index 94fc8f3e7..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel - -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.NormalFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent - -// This allows multiple screen to be listening to tags, even the same tag -class ChannelFinderQueryState( - val channel: Channel, -) - -class ChannelFinderFilterAssembler( - client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - private fun createMetadataChangeFilter(keys: Set): List { - val channelsToWatch = keys.filter { it.channel is PublicChatChannel } - - if (channelsToWatch.isEmpty()) { - return emptyList() - } - - // downloads all the reactions to a given event. - return channelsToWatch.map { - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - NormalFilter( - kinds = listOf(ChannelMetadataEvent.KIND), - tags = mapOf("e" to listOf(it.channel.idHex)), - since = it.channel.updatedMetadataAt + 1, - ), - ) - } - } - - fun createLoadEventsIfNotLoadedFilter(keys: Set): TypedFilter? { - val interestedEvents = - keys.mapNotNullTo(mutableSetOf()) { - if (it.channel is PublicChatChannel && it.channel.event == null) { - it.channel.idHex - } else { - null - } - } - - if (interestedEvents.isEmpty()) { - return null - } - - // downloads linked events to this event. - return TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelCreateEvent.KIND), - ids = interestedEvents.toList(), - ), - ) - } - - fun createLoadStreamingIfNotLoadedFilter(keys: Set): List { - val directEventsToLoad = - keys.mapNotNull { - if (it.channel is LiveActivitiesChannel && it.channel.info == null) { - it.channel - } else { - null - } - } - - val interestedEvents = directEventsToLoad.map { it.idHex }.toSet() - - if (interestedEvents.isEmpty()) { - return emptyList() - } - - // downloads linked events to this event. - return directEventsToLoad.map { - it.address().let { aTag -> - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(aTag.kind), - tags = mapOf("d" to listOf(aTag.dTag)), - authors = listOf(aTag.pubKeyHex), - ), - ) - } - } - } - - val singleChannelChannel = requestNewSubscription() - - override fun updateSubscriptions(keys: Set) { - val missing = createLoadEventsIfNotLoadedFilter(keys) - val missingStreaming = createLoadStreamingIfNotLoadedFilter(keys) - val metadataWatcher = createMetadataChangeFilter(keys) - - singleChannelChannel.typedFilters = (listOfNotNull(missing) + missingStreaming + metadataWatcher).ifEmpty { null } - } -} 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 new file mode 100644 index 000000000..b0c3d328b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel + +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.ammolite.relays.NostrClient + +// This allows multiple screen to be listening to tags, even the same tag +class ChannelFinderQueryState( + val channel: Channel, +) + +class ChannelFinderFilterAssemblyGroup( + client: NostrClient, +) : ComposeSubscriptionManager() { + val group = + listOf( + // this is a constantly rotating filter, better to keep it isolated + ChannelLoaderSubAssembler(client, ::allKeys), + // Later we could switch these and use one subscription for each query + // ChannelMetadataWatcherSubAssembler(client, ::allKeys), + // LiveActivityWatcherSubAssembly(client, ::allKeys) + ChannelMetadataAndLiveActivityWatcherSubAssembler(client, ::allKeys), + ) + + override fun start() = group.forEach { it.start() } + + override fun stop() = group.forEach { it.stop() } + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun invalidateKeys() = invalidateFilters() + + override fun destroy() = group.forEach { it.destroy() } + + override fun printStats() = group.forEach { it.printStats() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblerSubscription.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt index d756710e0..8adc8ae08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt @@ -35,7 +35,7 @@ fun ChannelFinderFilterAssemblerSubscription( @Composable fun ChannelFinderFilterAssemblerSubscription( channel: Channel, - dataSource: ChannelFinderFilterAssembler, + dataSource: ChannelFinderFilterAssemblyGroup, ) { // different screens get different states // even if they are tracking the same tag. 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 new file mode 100644 index 000000000..2e8ef78b3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive + +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel +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 +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * This assembler observes modifications to the LiveActivity root events + * since they are replaceable and updates to the public chat metadata events. + * + * They are all cramped as multiple filters in a single subscription with + * only one EOSE for everybody. + */ +class ChannelMetadataAndLiveActivityWatcherSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? = + keys + .mapNotNull { key -> + if (key.channel is PublicChatChannel) { + filterChannelMetadataUpdatesById(key.channel, since) + } else if (key.channel is LiveActivitiesChannel) { + filterLiveStreamUpdatesByAddress(key.channel, since) + } else { + null + } + }.flatten() + + override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt new file mode 100644 index 000000000..cf0f0a8ef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * This assembler observes loads missing public chats when needed. + * + * It is a fast rotating filter that tends to zero as new events arrive. + * Because of that, we isolate to avoid interrupting the EOSE of filters + * with long loading times. + * + * This is one filter for everybody. + */ +class ChannelLoaderSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? = filterMissingChannelsById(keys, since) + + override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt new file mode 100644 index 000000000..de2d23287 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats + +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class ChannelMetadataWatcherSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: ChannelFinderQueryState, + since: Map?, + ): List? = + if (key.channel is PublicChatChannel) { + filterChannelMetadataUpdatesById(key.channel, since) + } else { + null + } + + /** + * Only one key per channel. + */ + override fun id(key: ChannelFinderQueryState) = key.channel.idHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt new file mode 100644 index 000000000..b83e7f5a6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats + +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent + +fun filterMissingChannelsById( + keys: List, + since: Map?, +): List? { + val firstTimers = + keys.mapNotNullTo(mutableSetOf()) { + if (it.channel is PublicChatChannel && it.channel.event == null) { + it.channel.idHex + } else { + null + } + } + + if (firstTimers.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(ChannelCreateEvent.KIND), + ids = firstTimers.toList(), + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt new file mode 100644 index 000000000..e876c90cc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats + +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent + +fun filterChannelMetadataUpdatesById( + channel: PublicChatChannel, + since: Map?, +): List = + listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(ChannelMetadataEvent.KIND), + tags = mapOf("e" to listOf(channel.idHex)), + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt new file mode 100644 index 000000000..b326e30d1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities + +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter + +fun filterLiveStreamUpdatesByAddress( + channel: LiveActivitiesChannel, + since: Map?, +): List { + val stream = channel.address() + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(stream.kind), + tags = mapOf("d" to listOf(stream.dTag)), + authors = listOf(stream.pubKeyHex), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt new file mode 100644 index 000000000..be8d8a234 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities + +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * This assembler observes modifications to the LiveActivity root events + * since they are replaceable. + */ +class LiveActivityWatcherSubAssembly( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: ChannelFinderQueryState, + since: Map?, + ): List? = + if (key.channel is LiveActivitiesChannel) { + filterLiveStreamUpdatesByAddress(key.channel, since) + } else { + null + } + + /** + * Only one key per channel. + */ + override fun id(key: ChannelFinderQueryState) = key.channel.idHex +} 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 bf5d8bf34..0b76d0561 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,32 +20,11 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +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.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent // This allows multiple screen to be listening to tags, even the same tag class EventFinderQueryState( @@ -54,286 +33,22 @@ class EventFinderQueryState( class EventFinderFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - private fun createReactionsToWatchInAddressFilter(keys: Set): List? { - if (keys.isEmpty()) { - return null - } - - return groupByEOSEPresence(keys) - .map { - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - PollNoteEvent.KIND, - CommunityPostApprovalEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - ), - tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }), - since = findMinimumEOSEs(it), - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), - ), - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - DeletionEvent.KIND, - ), - tags = mapOf("a" to it.mapNotNull { it.address()?.toValue() }), - since = findMinimumEOSEs(it), - // Max amount of "replies" to download on a specific event. - limit = 10, - ), - ), - ) - }.flatten() - } - - private fun createAddressFilter(keys: Set): List? { - val myAddressesToWatch = keys.filter { it.event == null } - - if (myAddressesToWatch.isEmpty()) { - return null - } - - return myAddressesToWatch.map { - it.address().let { aTag -> - if (aTag.kind < 25000 && aTag.dTag.isBlank()) { - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(aTag.kind), - authors = listOf(aTag.pubKeyHex), - limit = 5, - ), - ) - } else { - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(aTag.kind), - tags = mapOf("d" to listOf(aTag.dTag)), - authors = listOf(aTag.pubKeyHex), - limit = 5, - ), - ) - } - } - } - } - - private fun createRepliesAndReactionsFilter(keys: Set): List? { - if (keys.isEmpty()) { - return null - } - - return groupByEOSEPresence(keys) - .map { - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - PollNoteEvent.KIND, - OtsEvent.KIND, - TextNoteModificationEvent.KIND, - GitReplyEvent.KIND, - ), - tags = mapOf("e" to it.map { it.idHex }), - since = findMinimumEOSEs(it), - // Max amount of "replies" to download on a specific event. - limit = 10000, - ), - ), - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - DeletionEvent.KIND, - NIP90ContentDiscoveryResponseEvent.KIND, - NIP90StatusEvent.KIND, - TorrentCommentEvent.KIND, - ), - tags = mapOf("e" to it.map { it.idHex }), - since = findMinimumEOSEs(it), - limit = 100, - ), - ), - ) - }.flatten() - } - - private fun createQuotesFilter(keys: Set): List? = - groupByEOSEPresence(keys) - .map { - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(TextNoteEvent.KIND), - tags = mapOf("q" to it.map { it.idHex }), - since = findMinimumEOSEs(it), - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), - ), - ) - }.flatten() - - fun createLoadEventsIfNotLoadedFilter(keys: Set): List? { - val directEventsToLoad = keys.filter { it.event == null } - - val threadingEventsToLoad = - keys - .mapNotNull { it.replyTo } - .flatten() - .filter { it !is AddressableNote && it.event == null } - - val interestedEvents = (directEventsToLoad + threadingEventsToLoad).map { it.idHex }.toSet().toList() - - if (interestedEvents.isEmpty()) { - return null - } - - // downloads linked events to this event. - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = SincePerRelayFilter(ids = interestedEvents), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + NoteEventLoaderSubAssembler(client, ::allKeys), + EventWatcherSubAssembler(client, ::allKeys), ) - } - val singleEventChannel = - requestNewSubscription { time, relayUrl -> - // Ignores EOSE if it is in the middle of a filter change. - if (isUpdatingFilters()) return@requestNewSubscription + override fun start() = group.forEach { it.start() } - checkNotInMainThread() + override fun stop() = group.forEach { it.stop() } - forEachSubscriber { - val eose = it.note.lastReactionsDownloadTime[relayUrl] - if (eose == null) { - it.note.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time)) - } else { - eose.time = time - } - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - // Many relays operate with limits in the amount of filters. - // As information comes, the filters will be rotated to get more data. - invalidateFilters() - } + override fun invalidateKeys() = invalidateFilters() - override fun updateSubscriptions(keys: Set) { - val addressables = - keys - .mapNotNullTo(mutableSetOf()) { - it.note as? AddressableNote - }.toSet() + override fun destroy() = group.forEach { it.destroy() } - val events = - keys - .mapNotNullTo(mutableSetOf()) { - if (it.note !is AddressableNote) { - it.note - } else { - null - } - }.toSet() - - val eventReactions = createRepliesAndReactionsFilter(events) - val addressReactions = createReactionsToWatchInAddressFilter(addressables) - - val missingEvents = createLoadEventsIfNotLoadedFilter(events) - val missingAddresses = createAddressFilter(addressables) - - val quotes = createQuotesFilter(events + addressables) - - singleEventChannel.typedFilters = - listOfNotNull(missingEvents, missingAddresses, eventReactions, addressReactions, quotes).flatten().ifEmpty { null } - } -} - -fun groupByEOSEPresence(notes: Set): Collection> = - notes - .groupBy { - it.lastReactionsDownloadTime.keys - .sorted() - .joinToString(",") - }.values - .map { - it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again - } - -fun groupByEOSEPresence(users: Iterable): Collection> = - users - .groupBy { - it.latestEOSEs.keys - .sorted() - .joinToString(",") - }.values - .map { - it.sortedBy { it.pubkeyHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again - } - -fun findMinimumEOSEs(notes: List): Map { - val minLatestEOSEs = mutableMapOf() - - notes.forEach { note -> - note.lastReactionsDownloadTime.forEach { - val minEose = minLatestEOSEs[it.key] - if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time - } - } - } - - return minLatestEOSEs -} - -fun findMinimumEOSEsForUsers(users: List): Map { - val minLatestEOSEs = mutableMapOf() - - users.forEach { - it.latestEOSEs.forEach { - val minEose = minLatestEOSEs[it.key] - if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time - } - } - } - - return minLatestEOSEs + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt new file mode 100644 index 000000000..65e0db72e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address + +fun filterMissingAddressables( + keys: List, + since: Map?, +): List? { + val missingAddressables = mutableSetOf
() + + keys.forEach { + if (it.note is AddressableNote && it.note.event == null) { + missingAddressables.add(it.note.address()) + } + + // loads threading that is event-based + it.note.replyTo?.forEach { + if (it is AddressableNote && it.event == null) { + missingAddressables.add(it.address()) + } + } + } + + return filterMissingAddressables(missingAddressables, since) +} + +fun filterMissingAddressables( + missingAddressables: Set
, + since: Map?, +): List? { + if (missingAddressables.isEmpty()) return null + + return missingAddressables.map { aTag -> + if (aTag.kind < 25000 && aTag.dTag.isBlank()) { + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(aTag.kind), + authors = listOf(aTag.pubKeyHex), + limit = 1, + ), + ) + } else { + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(aTag.kind), + tags = mapOf("d" to listOf(aTag.dTag)), + authors = listOf(aTag.pubKeyHex), + limit = 1, + ), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt new file mode 100644 index 000000000..6b867a00d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +fun filterMissingEvents( + keys: List, + since: Map?, +): List? { + val missingEvents = mutableSetOf() + + keys.forEach { + if (it.note !is AddressableNote && it.note.event == null) { + missingEvents.add(it.note.idHex) + } + + // loads threading that is event-based + it.note.replyTo?.forEach { + if (it !is AddressableNote && it.event == null) { + missingEvents.add(it.idHex) + } + } + } + + return filterMissingEvents(missingEvents, since) +} + +fun filterMissingEvents( + missingEventIds: Set, + since: Map?, +): List? { + if (missingEventIds.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = SincePerRelayFilter(ids = missingEventIds.toList()), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt new file mode 100644 index 000000000..92aa9d4ae --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class NoteEventLoaderSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun updateFilter( + keys: List, + since: Map?, + ) = listOfNotNull( + filterMissingEvents(keys, since), + filterMissingAddressables(keys, since), + ).flatten() + + override fun distinct(key: EventFinderQueryState) = key.note +} 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 new file mode 100644 index 000000000..0e688d8fe --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers + +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.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class EventWatcherSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + var lastNotesOnFilter = emptyList() + + override fun newEose( + relayUrl: String, + time: Long, + ) { + lastNotesOnFilter.forEach { + it.lastReactionsDownloadTime.newEose(relayUrl, time) + } + super.newEose(relayUrl, time) + } + + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + if (keys.isEmpty()) { + return null + } + + lastNotesOnFilter = keys.map { it.note } + val addressables = lastNotesOnFilter.filterIsInstance() + val events = keys.mapNotNull { if (it.note !is AddressableNote) it.note else null } + + return groupByEOSEPresence(lastNotesOnFilter) + .map { + listOfNotNull( + filterRepliesAndReactionsToNotes(events), + filterRepliesAndReactionsToAddresses(addressables), + filterQuotesToNotes(it), + ).flatten() + }.flatten() + } + + override fun distinct(key: EventFinderQueryState) = key.note +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt new file mode 100644 index 000000000..3adc6f313 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent + +fun filterQuotesToNotes(keys: List): List? { + if (keys.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(TextNoteEvent.KIND), + tags = mapOf("q" to keys.map { it.idHex }), + since = findMinimumEOSEs(keys), + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt new file mode 100644 index 000000000..30d1fefd6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent + +fun filterRepliesAndReactionsToAddresses(keys: List): List? { + if (keys.isEmpty()) return null + + val addresses = keys.map { it.address().toValue() } + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + PollNoteEvent.KIND, + CommunityPostApprovalEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + ), + tags = mapOf("a" to addresses), + since = findMinimumEOSEs(keys), + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + DeletionEvent.KIND, + ), + tags = mapOf("a" to addresses), + since = findMinimumEOSEs(keys), + // Max amount of "replies" to download on a specific event. + limit = 10, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt new file mode 100644 index 000000000..e524a0ccd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent + +fun filterRepliesAndReactionsToNotes(keys: List): List? { + if (keys.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + PollNoteEvent.KIND, + OtsEvent.KIND, + TextNoteModificationEvent.KIND, + GitReplyEvent.KIND, + ), + tags = mapOf("e" to keys.map { it.idHex }), + since = findMinimumEOSEs(keys), + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + DeletionEvent.KIND, + NIP90ContentDiscoveryResponseEvent.KIND, + NIP90StatusEvent.KIND, + TorrentCommentEvent.KIND, + ), + tags = mapOf("e" to keys.map { it.idHex }), + since = findMinimumEOSEs(keys), + limit = 100, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt new file mode 100644 index 000000000..ef1ba45cb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/GroupByEOSEPresence.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +fun groupByEOSEPresence(notes: Iterable): Collection> = + notes + .groupBy { + it.lastReactionsDownloadTime.relayList.keys + .sorted() + .joinToString(",") + }.values + .map { + it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again + } + +fun findMinimumEOSEs(notes: List): Map { + val minLatestEOSEs = mutableMapOf() + + notes.forEach { note -> + note.lastReactionsDownloadTime.relayList.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, EOSETime(it.value.time)) + } else if (it.value.time < minEose.time) { + minEose.time = it.value.time + } + } + } + + return minLatestEOSEs +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt new file mode 100644 index 000000000..c0a8b38bc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent + +fun filterNWCPaymentsFromRequests( + serviceKeys: Set, + paymentRequets: Set, + fromUsers: Set, + since: Map?, +): List = + listOf( + TypedFilter( + types = setOf(FeedType.WALLET_CONNECT), + filter = + SincePerRelayFilter( + kinds = listOf(LnZapPaymentResponseEvent.KIND), + authors = serviceKeys.toList(), + tags = + mapOf( + "e" to paymentRequets.toList(), + "p" to fromUsers.toList(), + ), + ), + ), + ) 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 135570a1a..81d43f13d 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,12 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent // This allows multiple screen to be listening to tags, even the same tag class NWCPaymentQueryState( @@ -36,38 +32,21 @@ class NWCPaymentQueryState( class NWCPaymentFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - fun createAccountMetadataFilter(keys: Set): TypedFilter? { - val fromAuthors = keys.mapTo(mutableSetOf()) { it.fromServiceHex } - val replyingToPayments = keys.mapTo(mutableSetOf()) { it.replyingToHex } - val aboutUsers = keys.mapTo(mutableSetOf()) { it.toUserHex } - - if (fromAuthors.isEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.WALLET_CONNECT), - filter = - SincePerRelayFilter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), - authors = fromAuthors.toList(), - tags = - mapOf( - "e" to replyingToPayments.toList(), - "p" to aboutUsers.toList(), - ), - limit = 1, - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + NWCPaymentWatcherSubAssembler(client, ::allKeys), ) - } - val channel = requestNewSubscription() + override fun start() = group.forEach { it.start() } - override fun updateSubscriptions(keys: Set) { - val filter = createAccountMetadataFilter(keys) - if (filter != null) { - channel.typedFilters = listOf(filter) - } else { - channel.typedFilters = null - } - } + override fun stop() = group.forEach { it.stop() } + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun invalidateKeys() = invalidateFilters() + + override fun destroy() = group.forEach { it.destroy() } + + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt new file mode 100644 index 000000000..dfa6a9f35 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class NWCPaymentWatcherSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + val fromAuthors = keys.mapTo(mutableSetOf()) { it.fromServiceHex } + val replyingToPayments = keys.mapTo(mutableSetOf()) { it.replyingToHex } + val aboutUsers = keys.mapTo(mutableSetOf()) { it.toUserHex } + + if (fromAuthors.isEmpty()) return null + + return filterNWCPaymentsFromRequests(fromAuthors, replyingToPayments, aboutUsers, since) + } + + override fun distinct(key: NWCPaymentQueryState) = key.replyingToHex +} 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 b667da3e7..da0d2b467 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 @@ -21,20 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.findMinimumEOSEsForUsers -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.groupByEOSEPresence -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserLoaderSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent // This allows multiple screen to be listening to tags, even the same tag class UserFinderQueryState( @@ -43,90 +33,22 @@ class UserFinderQueryState( class UserFinderFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - fun createUserMetadataFilter(keys: Set): List? { - if (keys.isEmpty()) return null - - val firstTimers = keys.filter { it.user.latestMetadata == null }.map { it.user.pubkeyHex }.distinct() - - if (firstTimers.isEmpty()) return null - - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND), - authors = firstTimers, - ), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + UserLoaderSubAssembler(client, ::allKeys), + UserWatcherSubAssembler(client, ::allKeys), ) - } - fun createUserMetadataStatusReportFilter(keys: Set): List? { - if (keys.isEmpty()) return null + override fun start() = group.forEach { it.start() } - val secondTimers = keys.filter { it.user.latestMetadata != null }.map { it.user }.toSet() + override fun stop() = group.forEach { it.stop() } - if (secondTimers.isEmpty()) return null + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - return groupByEOSEPresence(secondTimers) - .map { group -> - val groupIds = group.map { it.pubkeyHex } - val minEOSEs = findMinimumEOSEsForUsers(group) + override fun invalidateKeys() = invalidateFilters() - if (groupIds.isNotEmpty()) { - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - StatusEvent.KIND, - RelationshipStatusEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - ), - authors = groupIds, - since = minEOSEs, - ), - ), - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ReportEvent.KIND), - tags = mapOf("p" to groupIds), - since = minEOSEs, - ), - ), - ) - } else { - listOf() - } - }.flatten() - } + override fun destroy() = group.forEach { it.destroy() } - val userChannel = - requestNewSubscription { time, relayUrl -> - forEachSubscriber { - val eose = it.user.latestEOSEs[relayUrl] - if (eose == null) { - it.user.latestEOSEs = it.user.latestEOSEs + Pair(relayUrl, EOSETime(time)) - } else { - eose.time = time - } - } - } - - override fun updateSubscriptions(keys: Set) { - userChannel.typedFilters = - listOfNotNull( - createUserMetadataFilter(keys), - createUserMetadataStatusReportFilter(keys), - ).flatten() - .ifEmpty { null } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt new file mode 100644 index 000000000..cb59d40ad --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent + +fun filterNewUserMetadataForKey( + authors: Set, + since: Map?, +): List = + listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + AdvertisedRelayListEvent.KIND, + ), + authors = authors.toList(), + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt new file mode 100644 index 000000000..19cc60393 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class UserLoaderSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + val firstTimers = keys.filter { it.user.latestMetadata == null }.mapTo(mutableSetOf()) { it.user.pubkeyHex } + + if (firstTimers.isEmpty()) return null + + return filterNewUserMetadataForKey(firstTimers, since) + } + + override fun distinct(key: UserFinderQueryState) = key.user +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt new file mode 100644 index 000000000..4c793688e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip56Reports.ReportEvent + +fun filterReportsToKeys( + authors: Set, + since: Map?, +): List = + listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(ReportEvent.KIND), + tags = mapOf("p" to authors.toList()), + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt new file mode 100644 index 000000000..d46ad920b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent + +fun filterUserMetadataForKey( + authors: Set, + since: Map?, +): List = + listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + StatusEvent.KIND, + RelationshipStatusEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + ), + authors = authors.toList(), + since = since, + ), + ), + ) 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 new file mode 100644 index 000000000..a6c001915 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers + +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.EOSEAccountFast +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class UserWatcherSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + var lastUsersOnFilter: Set = emptySet() + var latestEOSEs: EOSEAccountFast = EOSEAccountFast(2000) + + override fun newEose( + relayUrl: String, + time: Long, + ) { + lastUsersOnFilter.forEach { + latestEOSEs.newEose(it, relayUrl, time) + } + super.newEose(relayUrl, time) + } + + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + if (keys.isEmpty()) return null + + lastUsersOnFilter = keys.filter { it.user.latestMetadata != null }.map { it.user }.toSet() + + if (lastUsersOnFilter.isEmpty()) return null + + return groupByRelayPresence(lastUsersOnFilter, latestEOSEs) + .map { group -> + val groupIds = group.map { it.pubkeyHex }.toSet() + if (groupIds.isNotEmpty()) { + val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs) + filterUserMetadataForKey(groupIds, minEOSEs) + filterReportsToKeys(groupIds, minEOSEs) + } else { + emptyList() + } + }.flatten() + } + + fun groupByRelayPresence( + users: Iterable, + eoseCache: EOSEAccountFast, + ): Collection> = + users + .groupBy { eoseCache.since(it)?.keys?.hashCode() } + .values + .map { + // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again + it.sortedBy { it.pubkeyHex } + } + + fun findMinimumEOSEsForUsers( + users: List, + eoseCache: EOSEAccountFast, + ): Map { + val minLatestEOSEs = mutableMapOf() + + users.forEach { + eoseCache.since(it)?.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, EOSETime(it.value.time)) + } else if (it.value.time < minEose.time) { + minEose.time = it.value.time + } + } + } + + return minLatestEOSEs + } + + override fun distinct(key: UserFinderQueryState) = key.user +} 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 ce70753f3..0d8ce16ac 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,50 +21,11 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES -import com.vitorpamplona.ammolite.relays.FeedType +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.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent -import com.vitorpamplona.quartz.experimental.nns.NNSEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray -import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed -import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay -import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip19Bech32.entities.Note -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.Hex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -80,172 +41,21 @@ class SearchFilterAssembler( val cache: LocalCache, client: NostrClient, scope: CoroutineScope, -) : MutableQueryBasedSubscriptionOrchestrator(client, scope) { - fun filterByAuthor(pubKey: HexKey) = +) : MutableComposeSubscriptionManager(scope) { + val group = listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - authors = listOfNotNull(pubKey), - limit = 1, - ), - ), + SearchWatcherSubAssembler(cache, client, ::allKeys), ) - fun filterByEvent(eventId: HexKey) = - listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - ids = listOfNotNull(eventId), - ), - ), - ) + override fun start() = group.forEach { it.start() } - fun filterByAddress(parsed: NAddress) = - listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - authors = listOfNotNull(parsed.author), - limit = 1, - ), - ), - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(parsed.kind), - authors = listOfNotNull(parsed.author), - tags = mapOf("d" to listOf(parsed.dTag)), - limit = 5, - ), - ), - ) + override fun stop() = group.forEach { it.stop() } - private fun createAnythingWithIDFilter(keys: Set): List { - if (keys.isEmpty()) return emptyList() + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - val uniqueQueries = - keys.mapNotNullTo(mutableSetOf()) { - if (!it.searchQuery.value.isBlank()) { - it.searchQuery.value - } else { - null - } - } + override fun invalidateKeys() = invalidateFilters() - return uniqueQueries - .map { mySearchString -> - val directFilters = - runCatching { - if (Hex.isHex(mySearchString)) { - val key = Hex.decode(mySearchString).toHexKey() - filterByAuthor(key) + filterByEvent(key) - } else { - when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) { - is NSec -> filterByAuthor(Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()) - is NPub -> filterByAuthor(parsed.hex) - is NProfile -> filterByAuthor(parsed.hex) - is Note -> filterByEvent(parsed.hex) - is NEvent -> filterByEvent(parsed.hex) - is NEmbed -> { - cache.justConsume(parsed.event, null) - emptyList() - } + override fun destroy() = group.forEach { it.destroy() } - is NRelay -> emptyList() - is NAddress -> filterByAddress(parsed) - else -> emptyList() - } - } - }.getOrDefault(emptyList()) - - val searchFilters = - listOfNotNull( - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - search = mySearchString, - limit = 1000, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - LongTextNoteEvent.KIND, - BadgeDefinitionEvent.KIND, - PeopleListEvent.KIND, - BookmarkListEvent.KIND, - AudioHeaderEvent.KIND, - AudioTrackEvent.KIND, - PinListEvent.KIND, - PollNoteEvent.KIND, - ChannelCreateEvent.KIND, - ), - search = mySearchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - ChannelMetadataEvent.KIND, - ClassifiedsEvent.KIND, - CommunityDefinitionEvent.KIND, - EmojiPackEvent.KIND, - HighlightEvent.KIND, - LiveActivitiesEvent.KIND, - PollNoteEvent.KIND, - NNSEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - search = mySearchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - InteractiveStoryPrologueEvent.KIND, - InteractiveStorySceneEvent.KIND, - FollowListEvent.KIND, - ), - search = mySearchString, - limit = 100, - ), - ), - ) - - directFilters + searchFilters - }.flatten() - } - - val searchChannel = requestNewSubscription() - - override fun updateSubscriptions(keys: Set) { - searchChannel.typedFilters = - logTime( - debugMessage = { "Search DataSource UpdateSubscriptions with ${it?.size} filter size" }, - block = { createAnythingWithIDFilter(keys).ifEmpty { null } }, - ) - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt new file mode 100644 index 000000000..ee1ffdbe7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer.types +import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress + +fun filterByAddress(address: NAddress) = + listOf( + TypedFilter( + types = ALL_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(MetadataEvent.KIND), + authors = listOfNotNull(address.author), + limit = 1, + ), + ), + if (address.kind < 25000 && address.dTag.isBlank()) { + TypedFilter( + types = ALL_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(address.kind), + authors = listOf(address.author), + limit = 5, + ), + ) + } else { + TypedFilter( + types = ALL_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(address.kind), + tags = mapOf("d" to listOf(address.dTag)), + authors = listOf(address.author), + limit = 5, + ), + ) + }, + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt new file mode 100644 index 000000000..9ff484b3a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent + +fun filterByAuthor(pubKey: HexKey) = + listOf( + TypedFilter( + types = ALL_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(MetadataEvent.KIND), + authors = listOfNotNull(pubKey), + limit = 1, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt new file mode 100644 index 000000000..537823ad3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +fun filterByEvent(eventId: HexKey) = + listOf( + TypedFilter( + types = ALL_FEED_TYPES, + filter = + SincePerRelayFilter( + ids = listOfNotNull(eventId), + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt new file mode 100644 index 000000000..8fe799504 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent + +fun searchPeopleByName(searchString: HexKey) = + listOf( + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = listOf(MetadataEvent.KIND), + search = searchString, + limit = 1000, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt new file mode 100644 index 000000000..040d6336e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nns.NNSEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun searchPostsByText(searchString: HexKey) = + listOf( + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + BadgeDefinitionEvent.KIND, + PeopleListEvent.KIND, + BookmarkListEvent.KIND, + AudioHeaderEvent.KIND, + AudioTrackEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + ChannelCreateEvent.KIND, + ), + search = searchString, + limit = 100, + ), + ), + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = + listOf( + ChannelMetadataEvent.KIND, + ClassifiedsEvent.KIND, + CommunityDefinitionEvent.KIND, + EmojiPackEvent.KIND, + HighlightEvent.KIND, + LiveActivitiesEvent.KIND, + PollNoteEvent.KIND, + NNSEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ), + search = searchString, + limit = 100, + ), + ), + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = + listOf( + InteractiveStoryPrologueEvent.KIND, + InteractiveStorySceneEvent.KIND, + FollowListEvent.KIND, + ), + search = searchString, + limit = 100, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt new file mode 100644 index 000000000..55be931a3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay +import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import com.vitorpamplona.quartz.nip19Bech32.entities.Note +import com.vitorpamplona.quartz.utils.Hex + +/** + * Creates a new sub for each Search Screen. The StateFlow of the text field + * that has the search ter is the id of the sub. + */ +class SearchWatcherSubAssembler( + val cache: LocalCache, + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: SearchQueryState, + since: Map?, + ): List? { + val mySearchString = key.searchQuery.value + + if (mySearchString.isBlank()) return null + + val directFilters = + runCatching { + if (Hex.isHex(mySearchString)) { + val key = Hex.decode(mySearchString).toHexKey() + filterByAuthor(key) + filterByEvent(key) + } else { + when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) { + is NSec -> filterByAuthor(Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()) + is NPub -> filterByAuthor(parsed.hex) + is NProfile -> filterByAuthor(parsed.hex) + is Note -> filterByEvent(parsed.hex) + is NEvent -> filterByEvent(parsed.hex) + is NEmbed -> { + cache.justConsume(parsed.event, null) + emptyList() + } + + is NRelay -> emptyList() + is NAddress -> filterByAddress(parsed) + else -> emptyList() + } + } + }.getOrDefault(emptyList()) + + val searchFilters = searchPeopleByName(mySearchString) + searchPostsByText(mySearchString) + + return directFilters + searchFilters + } + + override fun id(key: SearchQueryState) = key.searchQuery.hashCode().toString() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index e022a7f89..cc247ea2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -21,12 +21,14 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache +import com.vitorpamplona.amethyst.model.LocalCache.users import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.plus + +class EOSERelayList { + var relayList: Map = emptyMap() -class EOSERelayList( - var relayList: Map = emptyMap(), -) { fun addOrUpdate( relayUrl: String, time: Long, @@ -35,14 +37,29 @@ class EOSERelayList( if (eose == null) { relayList = relayList + Pair(relayUrl, EOSETime(time)) } else { - eose.time = time + if (time > eose.time) { + eose.update(time) + } } } + + fun clear() { + relayList = emptyMap() + } + + fun since() = relayList + + fun newEose( + relayUrl: String, + time: Long, + ) = addOrUpdate(relayUrl, time) } class EOSEFollowList( - var followList: Map = emptyMap(), + cacheSize: Int = 200, ) { + var followList: LruCache = LruCache(cacheSize) + fun addOrUpdate( listCode: String, relayUrl: String, @@ -52,16 +69,26 @@ class EOSEFollowList( if (relayList == null) { val newList = EOSERelayList() newList.addOrUpdate(relayUrl, time) - followList = followList + mapOf(listCode to newList) + followList.put(listCode, newList) } else { relayList.addOrUpdate(relayUrl, time) } } + + fun since(listCode: String) = followList[listCode]?.relayList + + fun newEose( + listCode: String, + relayUrl: String, + time: Long, + ) = addOrUpdate(listCode, relayUrl, time) } class EOSEAccount( - var users: LruCache = LruCache(20), + cacheSize: Int = 20, ) { + var users: LruCache = LruCache(cacheSize) + fun addOrUpdate( user: User, listCode: String, @@ -81,4 +108,50 @@ class EOSEAccount( fun removeDataFor(user: User) { users.remove(user) } + + fun since( + key: User, + listCode: String, + ) = users[key]?.followList?.get(listCode)?.relayList + + fun newEose( + user: User, + listCode: String, + relayUrl: String, + time: Long, + ) = addOrUpdate(user, listCode, relayUrl, time) +} + +class EOSEAccountFast( + cacheSize: Int = 20, +) { + private val users: LruCache = LruCache(cacheSize) + + fun addOrUpdate( + user: User, + relayUrl: String, + time: Long, + ) { + val relayList = users[user] + if (relayList == null) { + val newList = EOSERelayList() + users.put(user, newList) + + newList.addOrUpdate(relayUrl, time) + } else { + relayList.addOrUpdate(relayUrl, time) + } + } + + fun removeDataFor(user: User) { + users.remove(user) + } + + fun since(key: User) = users[key]?.relayList + + fun newEose( + user: User, + relayUrl: String, + time: Long, + ) = addOrUpdate(user, relayUrl, time) } 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 99f3867ca..beba04e53 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 @@ -21,104 +21,35 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey // This allows multiple screen to be listening to tags, even the same tag class ChatroomQueryState( val room: ChatroomKey, val account: Account, -) +) { + val listId = room.hashCode().toString() +} class ChatroomFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - private val latestEOSEs = EOSEAccount() - - fun createMessagesToMeFilter(key: ChatroomQueryState): TypedFilter? = - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = key.room.users.toList(), - tags = mapOf("p" to listOf(key.account.userProfile().pubkeyHex)), - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.room.hashCode().toString()) - ?.relayList, - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + ChatroomFilterSubAssembler(client, ::allKeys), ) - fun createMessagesFromMeFilter(key: ChatroomQueryState): TypedFilter? = - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = listOf(key.account.userProfile().pubkeyHex), - tags = mapOf("p" to key.room.users.map { it }), - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.room.hashCode().toString()) - ?.relayList, - ), - ) + override fun start() = group.forEach { it.start() } - fun clearEOSEs(account: Account) { - latestEOSEs.removeDataFor(account.userProfile()) - } + override fun stop() = group.forEach { it.stop() } - fun mergeAllFilters(key: ChatroomQueryState): List? = - listOfNotNull( - createMessagesToMeFilter(key), - createMessagesFromMeFilter(key), - ).ifEmpty { null } + override fun invalidateKeys() = invalidateFilters() - fun newSub(key: ChatroomQueryState): Subscription = - requestNewSubscription { time, relayUrl -> - latestEOSEs.addOrUpdate(key.account.userProfile(), key.room.hashCode().toString(), relayUrl, time) - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - val userSubscriptionMap = mutableMapOf() + override fun destroy() = group.forEach { it.start() } - fun findOrCreateSubFor(key: ChatroomQueryState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - dismissSubscription(it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt new file mode 100644 index 000000000..d4bfabf3f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class ChatroomFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: ChatroomQueryState, + since: Map?, + ): List? = + filterNip04DMs( + key.room.users, + key.account.userProfile().pubkeyHex, + since, + ) + + override fun user(key: ChatroomQueryState) = key.account.userProfile() + + override fun list(key: ChatroomQueryState) = key.listId +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt new file mode 100644 index 000000000..59d9e894f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import kotlin.collections.toList + +fun filterNip04DMs( + group: Set?, + user: HexKey?, + since: Map?, +): List? { + if (group == null || group.isEmpty() || user == null || user.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.PRIVATE_DMS), + filter = + SincePerRelayFilter( + kinds = listOf(PrivateDmEvent.KIND), + authors = group.toList(), + tags = mapOf("p" to listOf(user)), + since = since, + ), + ), + TypedFilter( + types = setOf(FeedType.PRIVATE_DMS), + filter = + SincePerRelayFilter( + kinds = listOf(PrivateDmEvent.KIND), + authors = listOf(user), + tags = mapOf("p" to group.toList()), + since = since, + ), + ), + ) +} 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 de626f4d3..fa62a2827 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 @@ -22,21 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datas import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.amethyst.service.relays.EOSEFollowList -import com.vitorpamplona.ammolite.relays.FeedType +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.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent // This allows multiple screen to be listening to tags, even the same tag class ChannelQueryState( @@ -46,169 +35,24 @@ class ChannelQueryState( class ChannelFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - companion object { - val RELAY_SET = - setOf( - FeedType.FOLLOWS, - FeedType.PRIVATE_DMS, - FeedType.GLOBAL, - FeedType.SEARCH, - ) - } - - private val latestEOSEPerAccount = EOSEAccount() - private val latestEOSEPerChannel = EOSEFollowList() - - fun sinceUser(key: ChannelQueryState) = - latestEOSEPerAccount.users[key.account.userProfile()] - ?.followList - ?.get(key.channel.idHex) - ?.relayList - - fun since(key: ChannelQueryState) = latestEOSEPerChannel.followList.get(key.channel.idHex)?.relayList - - fun newEose( - key: ChannelQueryState, - relayUrl: String, - time: Long, - ) { - latestEOSEPerAccount.addOrUpdate( - key.account.userProfile(), - key.channel.idHex, - relayUrl, - time, +) : ComposeSubscriptionManager() { + val group = + listOf( + // Requests the latest messages + ChannelPublicFilterSubAssembler(client, ::allKeys), + // Requests the latest messages by logged in user + ChannelFromUserFilterSubAssembler(client, ::allKeys), ) - latestEOSEPerChannel.addOrUpdate(key.channel.idHex, relayUrl, time) - } + override fun start() = group.forEach { it.start() } - fun createMessagesByMeToChannelFilter(key: ChannelQueryState): TypedFilter? = - when (key.channel) { - is PublicChatChannel -> - TypedFilter( - types = RELAY_SET, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMessageEvent.KIND), - authors = listOf(key.account.userProfile().pubkeyHex), - limit = 50, - since = sinceUser(key), - ), - ) - is LiveActivitiesChannel -> - TypedFilter( - types = RELAY_SET, - filter = - SincePerRelayFilter( - kinds = listOf(LiveActivitiesChatMessageEvent.KIND), - authors = listOf(key.account.userProfile().pubkeyHex), - limit = 50, - since = sinceUser(key), - ), - ) - is EphemeralChatChannel -> - null // there is nothing in the past - else -> { - null - } - } + override fun stop() = group.forEach { it.stop() } - fun createMessagesToChannelFilter(key: ChannelQueryState): TypedFilter? { - val channel = key.channel + override fun invalidateKeys() = invalidateFilters() - return when (channel) { - is PublicChatChannel -> - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMessageEvent.KIND), - tags = mapOf("e" to listOfNotNull(channel.idHex)), - limit = 200, - since = since(key), - ), - ) - is LiveActivitiesChannel -> - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = listOf(LiveActivitiesChatMessageEvent.KIND), - tags = mapOf("a" to listOfNotNull(channel.idHex)), - limit = 200, - since = since(key), - ), - ) - is EphemeralChatChannel -> { - TypedFilter( - types = - setOf( - FeedType.FOLLOWS, - FeedType.PUBLIC_CHATS, - FeedType.GLOBAL, - ), - filter = - SincePerRelayFilter( - kinds = listOf(EphemeralChatEvent.KIND), - tags = - if (channel.roomId.id.isBlank()) { - mapOf("d" to listOf("_")) - } else { - mapOf("d" to listOfNotNull(channel.roomId.id)) - }, - limit = 200, - since = since(key), - ), - ) - } - else -> { - null - } - } - return null - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - fun mergeAllFilters(key: ChannelQueryState): List? = - listOfNotNull( - createMessagesToChannelFilter(key), - createMessagesByMeToChannelFilter(key), - ).ifEmpty { null } + override fun destroy() = group.forEach { it.start() } - fun newSub(key: ChannelQueryState): Subscription = - requestNewSubscription { time, relayUrl -> - newEose(key, relayUrl, time) - } - - val userSubscriptionMap = mutableMapOf() - - fun findOrCreateSubFor(key: ChannelQueryState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - dismissSubscription(it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt new file mode 100644 index 000000000..314a179bf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class ChannelFromUserFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: ChannelQueryState, + since: Map?, + ): List? = + when (val channel = key.channel) { + is EphemeralChatChannel -> filterMyMessagesToEphemeralChat(channel, userHex(key), since) + is PublicChatChannel -> filterMyMessagesToPublicChat(channel, user(key).pubkeyHex, since) + is LiveActivitiesChannel -> filterMyMessagesToLiveActivities(channel, userHex(key), since) + else -> null + } + + fun userHex(key: ChannelQueryState) = key.account.userProfile().pubkeyHex + + override fun user(key: ChannelQueryState) = key.account.userProfile() + + override fun list(key: ChannelQueryState) = key.channel.idHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt new file mode 100644 index 000000000..dc7021397 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class ChannelPublicFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: ChannelQueryState, + since: Map?, + ): List? = + when (val channel = key.channel) { + is EphemeralChatChannel -> filterMessagesToEphemeralChat(channel, since) + is PublicChatChannel -> filterMessagesToPublicChat(channel, since) + is LiveActivitiesChannel -> filterMessagesToLiveActivities(channel, since) + else -> null + } + + override fun id(key: ChannelQueryState) = key.channel.idHex +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt new file mode 100644 index 000000000..0b8187bec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent + +fun filterMessagesToEphemeralChat( + channel: EphemeralChatChannel, + since: Map?, +): List? = + listOf( + TypedFilter( + types = + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(EphemeralChatEvent.KIND), + tags = + if (channel.roomId.id.isBlank()) { + mapOf("d" to listOf("_")) + } else { + mapOf("d" to listOfNotNull(channel.roomId.id)) + }, + limit = 200, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt new file mode 100644 index 000000000..d5fd995e6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent + +fun filterMessagesToLiveActivities( + channel: LiveActivitiesChannel, + since: Map?, +): List? = + listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND), + tags = mapOf("a" to listOfNotNull(channel.idHex)), + limit = 200, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt new file mode 100644 index 000000000..73da66ca7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun filterMessagesToPublicChat( + channel: PublicChatChannel, + since: Map?, +): List? = + listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(ChannelMessageEvent.KIND), + tags = mapOf("e" to listOfNotNull(channel.idHex)), + limit = 200, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt new file mode 100644 index 000000000..1035f9d9d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +fun filterMyMessagesToEphemeralChat( + channel: EphemeralChatChannel, + pubKey: HexKey, + since: Map?, +): List? = + listOf( + TypedFilter( + types = + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(EphemeralChatEvent.KIND), + tags = + if (channel.roomId.id.isBlank()) { + mapOf("d" to listOf("_")) + } else { + mapOf("d" to listOfNotNull(channel.roomId.id)) + }, + authors = listOf(pubKey), + limit = 50, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt new file mode 100644 index 000000000..22f409d77 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent + +fun filterMyMessagesToLiveActivities( + channel: LiveActivitiesChannel, + pubKey: HexKey, + since: Map?, +): List? = + listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND), + tags = mapOf("a" to listOfNotNull(channel.idHex)), + authors = listOf(pubKey), + limit = 50, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt new file mode 100644 index 000000000..69ea2f605 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun filterMyMessagesToPublicChat( + channel: PublicChatChannel, + pubKey: HexKey, + since: Map?, +): List? = + listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), + filter = + SincePerRelayFilter( + kinds = listOf(ChannelMessageEvent.KIND), + tags = mapOf("e" to listOfNotNull(channel.idHex)), + authors = listOf(pubKey), + limit = 50, + since = since, + ), + ), + ) 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 2610ab331..ac18a54d0 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 @@ -21,22 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import kotlinx.coroutines.flow.toList -import kotlin.collections.flatten // This allows multiple screen to be listening to tags, even the same tag class ChatroomListState( @@ -45,194 +31,23 @@ class ChatroomListState( class ChatroomListFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val latestEOSEs = EOSEAccount() - val chatRoomListKey = "ChatroomList" - - fun since(key: ChatroomListState) = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(chatRoomListKey) - ?.relayList - - fun newEose( - key: ChatroomListState, - relayUrl: String, - time: Long, - ) { - latestEOSEs.addOrUpdate( - key.account.userProfile(), - chatRoomListKey, - relayUrl, - time, - ) - } - - fun createMessagesToMeFilter(key: ChatroomListState) = - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - tags = mapOf("p" to listOf(key.account.userProfile().pubkeyHex)), - since = since(key), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + DMsFromUserFilterSubAssembler(client, ::allKeys), + FollowingPublicChatSubAssembler(client, ::allKeys), + FollowingEphemeralChatSubAssembler(client, ::allKeys), ) - fun createMessagesFromMeFilter(key: ChatroomListState) = - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = listOf(key.account.userProfile().pubkeyHex), - since = since(key), - ), - ) + override fun start() = group.forEach { it.start() } - fun createChannelsCreatedbyMeFilter(key: ChatroomListState) = - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = - listOf( - ChannelCreateEvent.KIND, - ChannelMetadataEvent.KIND, - ), - authors = listOf(key.account.userProfile().pubkeyHex), - since = since(key), - ), - ) + override fun stop() = group.forEach { it.stop() } - fun createMyChannelsFilter(key: ChatroomListState): TypedFilter? { - val followingEvents = key.account.publicChatList.livePublicChatEventIdSet.value + override fun invalidateKeys() = invalidateFilters() - if (followingEvents.isEmpty()) return null + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - return TypedFilter( - // Metadata comes from any relay - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelCreateEvent.KIND), - ids = followingEvents.toList(), - since = since(key), - ), - ) - } + override fun destroy() = group.forEach { it.start() } - fun createLastChannelInfoFilter(key: ChatroomListState): List? { - val followingEvents = - key.account.publicChatList.livePublicChatEventIdSet.value - - if (followingEvents.isEmpty()) return null - - return followingEvents.map { - TypedFilter( - // Metadata comes from any relay - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMetadataEvent.KIND), - tags = mapOf("e" to listOf(it)), - since = since(key), - limit = 1, - ), - ) - } - } - - fun createLastMessageOfEachChannelFilter(key: ChatroomListState): List? { - val followingEvents = - key.account.publicChatList.livePublicChatEventIdSet.value - - if (followingEvents.isEmpty()) return null - - return followingEvents.map { - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMessageEvent.KIND), - tags = mapOf("e" to listOf(it)), - since = since(key), - // Remember to consider spam that is being removed from the UI - limit = 50, - ), - ) - } - } - - fun createLastMessageOfEachEphemeralChatFilter(key: ChatroomListState): List? { - val followingEvents = - key.account.ephemeralChatList.liveEphemeralChatList.value - .map { it.id } - - if (followingEvents.isEmpty()) return null - - return listOf( - TypedFilter( - // Metadata comes from any relay - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = listOf(EphemeralChatEvent.KIND), - tags = mapOf("d" to followingEvents), - since = since(key), - // Remember to consider spam that is being removed from the UI - limit = 50, - ), - ), - ) - } - - fun mergeAllFilters(key: ChatroomListState): List? = - listOfNotNull( - listOfNotNull( - createMessagesToMeFilter(key), - createMessagesFromMeFilter(key), - createMyChannelsFilter(key), - ), - createLastChannelInfoFilter(key), - createLastMessageOfEachChannelFilter(key), - createLastMessageOfEachEphemeralChatFilter(key), - ).flatten().ifEmpty { null } - - fun newSub(key: ChatroomListState): Subscription = - requestNewSubscription { time, relayUrl -> - newEose(key, relayUrl, time) - } - - val userSubscriptionMap = mutableMapOf() - - fun findOrCreateSubFor(key: ChatroomListState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - dismissSubscription(it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt new file mode 100644 index 000000000..70485caf7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class DMsFromUserFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: ChatroomListState, + since: Map?, + ): List? = + listOfNotNull( + filterNip04DMsToAndFromMe(key.account.userProfile().pubkeyHex, since), + ).flatten() + + override fun user(key: ChatroomListState) = key.account.userProfile() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt new file mode 100644 index 000000000..5846a0894 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId + +fun filterFollowingEphemeralChats( + followingChannels: Set?, + since: Map?, +): List? { + if (followingChannels == null || followingChannels.isEmpty()) return null + + val dTags = + followingChannels.map { + if (it.id.isBlank()) { + "_" + } else { + it.id + } + } + + return listOf( + TypedFilter( + // Metadata comes from any relay + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(EphemeralChatEvent.KIND), + tags = mapOf("d" to dTags), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt new file mode 100644 index 000000000..12f87043f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import kotlin.collections.toList + +fun filterFollowingPublicChats( + followingChannels: Set?, + since: Map?, +): List? { + if (followingChannels == null || followingChannels.isEmpty()) return null + + return listOf( + TypedFilter( + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(ChannelCreateEvent.KIND), + ids = followingChannels.toList(), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt new file mode 100644 index 000000000..2db612ce2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import kotlin.collections.toList + +fun filterLastMessageFollowingPublicChats( + followingChannels: Set?, + since: Map?, +): List? { + if (followingChannels == null || followingChannels.isEmpty()) return null + + return listOf( + TypedFilter( + // Metadata comes from any relay + types = EVENT_FINDER_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(ChannelMetadataEvent.KIND), + tags = mapOf("e" to followingChannels.toList()), + since = since, + limit = 1, + ), + ), + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(ChannelMessageEvent.KIND), + tags = mapOf("e" to followingChannels.toList()), + since = since, + // Remember to consider spam that is being removed from the UI + limit = 100, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt new file mode 100644 index 000000000..6a31f7cc2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent + +fun filterNip04DMsToAndFromMe( + user: HexKey?, + since: Map?, +): List? { + if (user == null || user.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.PRIVATE_DMS), + filter = + SincePerRelayFilter( + kinds = listOf(PrivateDmEvent.KIND), + tags = mapOf("p" to listOf(user)), + since = since, + ), + ), + TypedFilter( + types = setOf(FeedType.PRIVATE_DMS), + filter = + SincePerRelayFilter( + kinds = listOf(PrivateDmEvent.KIND), + authors = listOf(user), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt new file mode 100644 index 000000000..1b3e33139 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlin.collections.forEach + +class FollowingEphemeralChatSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: ChatroomListState, + since: Map?, + ): List? = + listOfNotNull( + filterFollowingEphemeralChats(key.account.ephemeralChatList.liveEphemeralChatList.value, since), + ).flatten() + + override fun user(key: ChatroomListState) = key.account.userProfile() + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: ChatroomListState): Subscription { + userJobMap[key.account.userProfile()]?.forEach { it.cancel() } + userJobMap[key.account.userProfile()] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.ephemeralChatList.liveEphemeralChatList.sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt new file mode 100644 index 000000000..b2432e501 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlin.collections.forEach + +class FollowingPublicChatSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: ChatroomListState, + since: Map?, + ): List? = + listOfNotNull( + filterLastMessageFollowingPublicChats(key.account.publicChatList.livePublicChatEventIdSet.value, since), + filterFollowingPublicChats(key.account.publicChatList.livePublicChatEventIdSet.value, since), + ).flatten() + + override fun user(key: ChatroomListState) = key.account.userProfile() + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: ChatroomListState): Subscription { + userJobMap[key.account.userProfile()]?.forEach { it.cancel() } + userJobMap[key.account.userProfile()] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.publicChatList.livePublicChatEventIdSet.sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} 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 new file mode 100644 index 000000000..2742e63b9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +class CommunityFeedFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + if (keys.isEmpty()) return emptyList() + + return keys.mapNotNull { + val commEvent = it.community.event + if (commEvent is CommunityDefinitionEvent) { + filterCommunityPosts(commEvent, since) + } else { + null + } + } + } + + override fun distinct(key: CommunityQueryState) = key.community.idHex +} 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 d8972b9e3..3d5e5afdf 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 @@ -21,13 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent // This allows multiple screen to be listening to tags, even the same tag class CommunityQueryState( @@ -36,37 +31,21 @@ class CommunityQueryState( class CommunityFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - private fun createLoadCommunityFilter(keys: Set): List { - if (keys.isEmpty()) return emptyList() +) : ComposeSubscriptionManager() { + val group = + listOf( + CommunityFeedFilterSubAssembler(client, ::allKeys), + ) - val uniqueCommunities = - keys.associate { - it.community.address.toValue() to it.community - } + override fun start() = group.forEach { it.start() } - return uniqueCommunities.mapNotNull { - val commEvent = it.value.event - if (commEvent is CommunityDefinitionEvent) { - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - authors = commEvent.moderators().map { it.pubKey }.plus(listOfNotNull(it.value.author?.pubkeyHex)), - tags = mapOf("a" to listOf(it.value.address.toValue())), - kinds = listOf(CommunityPostApprovalEvent.KIND), - limit = 500, - ), - ) - } else { - null - } - } - } + override fun stop() = group.forEach { it.stop() } - val loadCommunityChannel = requestNewSubscription() + override fun invalidateKeys() = invalidateFilters() - override fun updateSubscriptions(keys: Set) { - loadCommunityChannel.typedFilters = createLoadCommunityFilter(keys).ifEmpty { null } - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.start() } + + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt new file mode 100644 index 000000000..114d72d06 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +fun filterCommunityPosts( + community: CommunityDefinitionEvent, + since: Map?, +): TypedFilter = + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + authors = community.moderators().map { it.pubKey }.plus(community.pubKey), + tags = mapOf("a" to listOf(community.addressTag())), + kinds = listOf(CommunityPostApprovalEvent.KIND), + limit = 500, + since = since, + ), + ) 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 6d39ccf81..ac47897ff 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 @@ -21,31 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import java.util.Locale // This allows multiple screen to be listening to tags, even the same tag class DiscoveryQueryState( @@ -55,508 +33,22 @@ class DiscoveryQueryState( class DiscoveryFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val latestEOSEs = EOSEAccount() - - fun since(key: DiscoveryQueryState) = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultDiscoveryFollowList.value) - ?.relayList - - fun newEose( - key: DiscoveryQueryState, - relayUrl: String, - time: Long, - ) { - latestEOSEs.addOrUpdate( - key.account.userProfile(), - key.account.settings.defaultDiscoveryFollowList.value, - relayUrl, - time, - ) - } - - fun createMarketplaceFilter(key: DiscoveryQueryState): List { - val follows = - key.account.liveDiscoveryListAuthorsPerRelay.value - ?.ifEmpty { null } - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.hashtags - ?.toList() - ?.ifEmpty { null } - val geohashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.geotags - ?.toList() - ?.ifEmpty { null } - - return listOfNotNull( - TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(ClassifiedsEvent.KIND), - limit = 300, - since = since(key), - ), - ), - hashToLoad?.let { - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(ClassifiedsEvent.KIND), - tags = - mapOf( - "t" to - it - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten(), - ), - limit = 300, - since = since(key), - ), - ) - }, - geohashToLoad?.let { - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(ClassifiedsEvent.KIND), - tags = - mapOf( - "g" to it, - ), - limit = 300, - since = since(key), - ), - ) - }, - ) - } - - fun createNIP89Filter(key: DiscoveryQueryState): List = - listOfNotNull( - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(AppDefinitionEvent.KIND), - limit = 300, - tags = mapOf("k" to listOf("5300")), - since = since(key), - ), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + DiscoveryFollowsDiscoverySubAssembler(client, ::allKeys), + MixGeohashHashtagsDiscoverySubAssembler(client, ::allKeys), ) - fun createLiveStreamFilter(key: DiscoveryQueryState): List { - val follows = - key.account.liveDiscoveryFollowLists.value - ?.authors - ?.toList() - ?.ifEmpty { null } + override fun start() = group.forEach { it.start() } - val followsRelays = key.account.liveDiscoveryListAuthorsPerRelay.value + override fun stop() = group.forEach { it.stop() } - return listOfNotNull( - TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = followsRelays, - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), - limit = 300, - since = since(key), - ), - ), - follows?.let { - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - tags = mapOf("p" to it), - kinds = listOf(LiveActivitiesEvent.KIND), - limit = 100, - since = since(key), - ), - ) - }, - ) - } + override fun invalidateKeys() = invalidateFilters() - fun createPublicChatFilter(key: DiscoveryQueryState): List { - val follows = - key.account.liveDiscoveryListAuthorsPerRelay.value - ?.ifEmpty { null } - val followChats = - key.account.publicChatList.livePublicChatEventIdSet.value + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - return listOfNotNull( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(ChannelMessageEvent.KIND), - limit = 500, - since = since(key), - ), - ), - if (followChats.isNotEmpty()) { - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - ids = followChats.toList(), - kinds = - listOf( - ChannelCreateEvent.KIND, - ChannelMessageEvent.KIND, - ), - limit = 300, - since = since(key), - ), - ) - } else { - null - }, - ) - } + override fun destroy() = group.forEach { it.start() } - fun createFollowSetFilter(key: DiscoveryQueryState): List { - val follows = - key.account.liveDiscoveryListAuthorsPerRelay.value - ?.ifEmpty { null } - - return listOfNotNull( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(FollowListEvent.KIND), - limit = 300, - since = since(key), - ), - ), - ) - } - - fun createLongFormFilter(key: DiscoveryQueryState): List { - val follows = - key.account.liveDiscoveryListAuthorsPerRelay.value - ?.ifEmpty { null } - - return listOfNotNull( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, - since = since(key), - ), - ), - ) - } - - fun createCommunitiesFilter(key: DiscoveryQueryState): TypedFilter { - val follows = key.account.liveDiscoveryListAuthorsPerRelay.value - - return TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = - listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, - ), - limit = 300, - since = since(key), - ), - ) - } - - fun createLiveStreamTagsFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.hashtags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), - tags = - mapOf( - "t" to - hashToLoad - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten(), - ), - limit = 300, - since = since(key), - ), - ) - } - - fun createLiveStreamGeohashesFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.geotags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), - tags = mapOf("g" to hashToLoad), - limit = 300, - since = since(key), - ), - ) - } - - fun createPublicChatsTagsFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.hashtags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = - listOf( - ChannelCreateEvent.KIND, - ChannelMetadataEvent.KIND, - ChannelMessageEvent.KIND, - ), - tags = - mapOf( - "t" to - hashToLoad - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten(), - ), - limit = 300, - since = since(key), - ), - ) - } - - fun createPublicChatsGeohashesFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.geotags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = - listOf( - ChannelCreateEvent.KIND, - ChannelMetadataEvent.KIND, - ChannelMessageEvent.KIND, - ), - tags = - mapOf("g" to hashToLoad), - limit = 300, - since = since(key), - ), - ) - } - - fun createCommunitiesTagsFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.hashtags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = - listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, - ), - tags = - mapOf( - "t" to - hashToLoad - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten(), - ), - limit = 300, - since = since(key), - ), - ) - } - - fun createCommunitiesGeohashesFilter(key: DiscoveryQueryState): TypedFilter? { - val hashToLoad = - key.account.liveDiscoveryFollowLists.value - ?.geotags - ?.toList() - - if (hashToLoad.isNullOrEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = - listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, - ), - tags = mapOf("g" to hashToLoad), - limit = 300, - since = since(key), - ), - ) - } - - fun mergeAllFilters(key: DiscoveryQueryState): List? = - createLiveStreamFilter(key) - .plus(createNIP89Filter(key)) - .plus(createPublicChatFilter(key)) - .plus(createFollowSetFilter(key)) - .plus(createLongFormFilter(key)) - .plus(createMarketplaceFilter(key)) - .plus( - listOfNotNull( - createLiveStreamTagsFilter(key), - createLiveStreamGeohashesFilter(key), - createCommunitiesFilter(key), - createCommunitiesTagsFilter(key), - createCommunitiesGeohashesFilter(key), - createPublicChatsTagsFilter(key), - createPublicChatsGeohashesFilter(key), - ), - ).toList() - .ifEmpty { null } - - val userJobMap = mutableMapOf() - val userSubscriptionMap = mutableMapOf() - - fun newSub(key: DiscoveryQueryState): Subscription { - userJobMap[key.account.userProfile()]?.cancel() - userJobMap[key.account.userProfile()] = - key.scope.launch(Dispatchers.Default) { - key.account.liveDiscoveryFollowLists.collect { - invalidateFilters() - } - } - - return requestNewSubscription { time, relayUrl -> - newEose(key, relayUrl, time) - } - } - - fun endSub( - key: User, - subId: String, - ) { - dismissSubscription(subId) - userJobMap[key]?.cancel() - } - - fun findOrCreateSubFor(key: DiscoveryQueryState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt new file mode 100644 index 000000000..c9ea4e5fe --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByFollows +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlin.collections.flatten + +class DiscoveryFollowsDiscoverySubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: DiscoveryQueryState, + since: Map?, + ): List? = updateFilter(key.followsPerRelay(), key.followLists(), since) + + override fun user(key: DiscoveryQueryState) = key.account.userProfile() + + override fun list(key: DiscoveryQueryState) = key.listName() + + fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList + + fun DiscoveryQueryState.listName() = listNameFlow().value + + fun DiscoveryQueryState.followListsFlow() = account.liveDiscoveryFollowLists + + fun DiscoveryQueryState.followLists() = followListsFlow().value + + fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryListAuthorsPerRelay + + fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value + + fun updateFilter( + follows: Map>?, + followLists: Account.LiveFollowList?, + since: Map?, + ): List? { + if (follows != null && follows.isEmpty()) return null + + return listOfNotNull( + filterClassifiedsByFollows(follows, since), + filterFollowSetsByFollows(follows, since), + filterLongFormByFollows(follows, since), + filterPublicChatsByFollows(follows, since), + filterContentDVMsByFollows(follows, since), + filterLiveActivitiesByFollows(follows, followLists?.authorsPlusMe, since), + filterCommunitiesByFollows(follows, since), + ).flatten() + } + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: DiscoveryQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.scope.launch(Dispatchers.Default) { + key.listNameFlow().collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.followsPerRelayFlow().sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt new file mode 100644 index 000000000..839b4a610 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunityPostsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunityPostsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByHashtag +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class MixGeohashHashtagsDiscoverySubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: DiscoveryQueryState, + since: Map?, + ): List? = updateFilter(key.followLists()?.hashtags, key.followLists()?.geotags, since) + + fun updateFilter( + hashtags: Set?, + geotags: Set?, + since: Map?, + ): List? { + val hashtagFilters = + if (hashtags != null && !hashtags.isEmpty()) { + listOfNotNull( + filterClassifiedsByHashtag(hashtags, since), + filterPublicChatsByHashtag(hashtags, since), + filterFollowSetsByHashtag(hashtags, since), + filterLongFormByHashtag(hashtags, since), + filterContentDVMsByHashtag(hashtags, since), + filterLiveActivitiesByHashtag(hashtags, since), + filterCommunityPostsByHashtag(hashtags, since), + ).flatten() + } else { + emptyList() + } + + val geoHashFilters = + if (geotags != null && !geotags.isEmpty()) { + listOfNotNull( + filterClassifiedsByGeohash(geotags, since), + filterPublicChatsByGeohash(geotags, since), + filterFollowSetsByGeohash(geotags, since), + filterLongFormByGeohash(geotags, since), + filterContentDVMsByGeohash(geotags, since), + filterLiveActivitiesByGeohash(geotags, since), + filterCommunityPostsByGeohash(geotags, since), + ).flatten() + } else { + emptyList() + } + + return hashtagFilters + geoHashFilters + } + + override fun user(key: DiscoveryQueryState) = key.account.userProfile() + + fun DiscoveryQueryState.followListsFlow() = account.liveDiscoveryFollowLists + + fun DiscoveryQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: DiscoveryQueryState): Subscription { + val user = user(key) + userJobMap[user]?.cancel() + userJobMap[user] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt new file mode 100644 index 000000000..c1ed32c3c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +fun filterLongFormByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(LongTextNoteEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt new file mode 100644 index 000000000..99267d946 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +fun filterLongFormByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = listOf(LongTextNoteEvent.KIND), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt new file mode 100644 index 000000000..4a42c209f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +fun filterLongFormByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = + listOf( + LongTextNoteEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt new file mode 100644 index 000000000..b5c6757fc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun filterPublicChatsByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(ChannelMessageEvent.KIND), + limit = 500, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt new file mode 100644 index 000000000..c04d4e1be --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun filterPublicChatsByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, + ), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt new file mode 100644 index 000000000..cf7783be8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent + +fun filterPublicChatsByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt new file mode 100644 index 000000000..d7fb6ef38 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent + +fun filterFollowSetsByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(FollowListEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt new file mode 100644 index 000000000..7a2fc53a9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +fun filterFollowSetsByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = listOf(LongTextNoteEvent.KIND), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt new file mode 100644 index 000000000..d43cf142e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +fun filterFollowSetsByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = + listOf( + LongTextNoteEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt new file mode 100644 index 000000000..6ad3fe01c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent + +fun filterLiveActivitiesByFollows( + follows: Map>?, + followKeys: Set?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOfNotNull( + TypedFilter( + types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = + listOf( + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + ), + limit = 300, + since = since, + ), + ), + followKeys?.let { + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + tags = mapOf("p" to it.toList()), + kinds = listOf(LiveActivitiesEvent.KIND), + limit = 100, + since = since, + ), + ) + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt new file mode 100644 index 000000000..378010a2c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent + +fun filterLiveActivitiesByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = + listOf( + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + ), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt new file mode 100644 index 000000000..73fb9b5ec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent + +fun filterLiveActivitiesByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = + listOf( + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index df3f8aacf..85e90a7d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -109,7 +109,7 @@ open class DiscoverCommunityFeedFilter( private fun shouldInclude( aTag: Address?, params: FilterByListParams, - ) = aTag != null && aTag.kind == CommunityDefinitionEvent.Companion.KIND && params.match(aTag) + ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag) override fun sort(collection: Set): List { val lastNote = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt new file mode 100644 index 000000000..0be392a61 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +fun filterCommunitiesByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = + listOf( + CommunityDefinitionEvent.KIND, + CommunityPostApprovalEvent.KIND, + ), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt new file mode 100644 index 000000000..5f6707860 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +fun filterCommunityPostsByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = + listOf( + CommunityDefinitionEvent.KIND, + CommunityPostApprovalEvent.KIND, + ), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt new file mode 100644 index 000000000..acf5000bd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent + +fun filterCommunityPostsByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = + listOf( + CommunityDefinitionEvent.KIND, + CommunityPostApprovalEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt new file mode 100644 index 000000000..1040958b6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent + +fun filterContentDVMsByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = listOf(AppDefinitionEvent.KIND), + limit = 300, + tags = mapOf("k" to listOf("5300")), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt new file mode 100644 index 000000000..a1d971040 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent + +fun filterContentDVMsByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(AppDefinitionEvent.KIND), + tags = mapOf("k" to listOf("5300"), "g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt new file mode 100644 index 000000000..28d049b97 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent + +fun filterContentDVMsByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = + SincePerRelayFilter( + kinds = listOf(AppDefinitionEvent.KIND), + tags = mapOf("k" to listOf("5300"), "t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt new file mode 100644 index 000000000..354bacb1c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterClassifiedsByFollows( + follows: Map>?, + since: Map?, +): List? { + if (follows != null && follows.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt new file mode 100644 index 000000000..5ef6ccf9e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterClassifiedsByGeohash( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(ClassifiedsEvent.KIND), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt new file mode 100644 index 000000000..851e923b6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterClassifiedsByHashtag( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashtagAlts(hashToLoad).toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(ClassifiedsEvent.KIND), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt new file mode 100644 index 000000000..6c32b4fdc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterPostsByGeohash( + geohash: String, + since: Map?, +): List = + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("g" to listOf(geohash)), + kinds = + listOf( + TextNoteEvent.KIND, + ChannelMessageEvent.KIND, + LongTextNoteEvent.KIND, + PollNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ), + limit = 200, + since = since, + ), + ), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt new file mode 100644 index 000000000..747db27fc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class GeoHashFeedFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: GeohashQueryState, + since: Map?, + ): List? = filterPostsByGeohash(key.geohash, since) + + /** + * Only one key per hashtag. + */ + override fun id(key: GeohashQueryState) = key.lowercaseGeohash +} 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 e75a4bf9d..959d0f1e0 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,26 +21,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent // This allows multiple screen to be listening to tags, even the same tag class GeohashQueryState( val geohash: String, -) +) { + val lowercaseGeohash = geohash.lowercase() +} /** * Creates a filter for multiple geohashes at the same time. @@ -48,46 +37,21 @@ class GeohashQueryState( @Stable class GeoHashFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - companion object { - val GEO_COMPATIBLE_KINDS = - listOf( - TextNoteEvent.KIND, - ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, - PollNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ) - } - - fun createLoadHashtagFilter(keys: Set): TypedFilter? { - if (keys.isEmpty()) return null - - val unique = - keys - .mapTo(mutableSetOf()) { - it.geohash - }.toList() - - return TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("g" to unique), - kinds = GEO_COMPATIBLE_KINDS, - limit = 200, - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + GeoHashFeedFilterSubAssembler(client, ::allKeys), ) - } - val loadGeohashChannel = requestNewSubscription() + override fun start() = group.forEach { it.start() } - override fun updateSubscriptions(keys: Set) { - loadGeohashChannel.typedFilters = createLoadHashtagFilter(keys)?.let { listOf(it) } - } + override fun stop() = group.forEach { it.stop() } + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.start() } + + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt new file mode 100644 index 000000000..da8896d31 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterPostsByHashtags( + hashtag: String, + since: Map?, +): List { + val hashtagsToFollow = hashtagAlts(hashtag).toList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("t" to hashtagsToFollow), + kinds = + listOf( + TextNoteEvent.KIND, + ChannelMessageEvent.KIND, + LongTextNoteEvent.KIND, + PollNoteEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ), + limit = 400, + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("t" to hashtagsToFollow), + kinds = + listOf( + InteractiveStorySceneEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + ), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt new file mode 100644 index 000000000..209872cff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class HashtagFeedFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: HashtagQueryState, + since: Map?, + ): List = filterPostsByHashtags(key.hashtag, since) + + /** + * Only one key per hashtag. + */ + override fun id(key: HashtagQueryState) = key.lowercaseHashtag +} 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 2d0381acb..8a9ae8656 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,97 +20,33 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import java.util.Locale // This allows multiple screen to be listening to tags, even the same tag class HashtagQueryState( val hashtag: String, -) +) { + val lowercaseHashtag = hashtag.lowercase() +} class HashtagFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - companion object { - // keeps a maximum of 10 kinds per filter. - val TOPIC_COMPATIBLE_KINDS = - listOf( - TextNoteEvent.KIND, - ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, - PollNoteEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ) - - val TOPIC_COMPATIBLE_KINDS2 = - listOf( - InteractiveStorySceneEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - ) - } - - fun createLoadHashtagFilter(keys: Set): List { - if (keys.isEmpty()) return emptyList() - - val hashtagsToFollow = - keys - .mapTo(mutableSetOf()) { - setOf( - it.hashtag, - it.hashtag.lowercase(), - it.hashtag.uppercase(), - it.hashtag.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten() - - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("t" to hashtagsToFollow), - kinds = TOPIC_COMPATIBLE_KINDS, - limit = 200, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("t" to hashtagsToFollow), - kinds = TOPIC_COMPATIBLE_KINDS2, - limit = 200, - ), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + HashtagFeedFilterSubAssembler(client, ::allKeys), ) - } - val loadHashtagChannel = requestNewSubscription() + override fun start() = group.forEach { it.start() } - override fun updateSubscriptions(keys: Set) { - loadHashtagChannel.typedFilters = createLoadHashtagFilter(keys).ifEmpty { null } - } + override fun stop() = group.forEach { it.stop() } + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.start() } + + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index d6f5a7ee3..ed3be1348 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -71,7 +71,6 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.NoteCompose 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 fbc70815c..1b16dd5a7 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 @@ -21,39 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.mixGeohashHashtagsCommunities.MixGeohashHashtagsCommunityEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxUsersEoseManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import java.util.Locale // This allows multiple screen to be listening to tags, even the same tag class HomeQueryState( @@ -63,289 +36,27 @@ class HomeQueryState( class HomeFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val latestEOSEs = EOSEAccount() - - fun createFollowAccountsFilter(key: HomeQueryState): List { - val follows = key.account.liveHomeListAuthorsPerRelay.value - - return listOf( - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ClassifiedsEvent.KIND, - LongTextNoteEvent.KIND, - EphemeralChatEvent.KIND, - HighlightEvent.KIND, - ), - authors = follows, - limit = 400, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ), - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - PollNoteEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - PinListEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - WikiNoteEvent.KIND, - ), - authors = follows, - limit = 400, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + HomeOutboxEventsEoseManager(client, ::allKeys), + HomeOutboxUsersEoseManager(client, ::allKeys), + // We can break it down, one for each sub if needed. + // HashtagEventsFilterSubAssembler(client, ::allKeys), + // GeohashEventsFilterSubAssembler(client, ::allKeys), + // CommunityEventsFilterSubAssembler(client, ::allKeys), + MixGeohashHashtagsCommunityEoseManager(client, ::allKeys), ) - } - fun createFollowMetadataAndReleaseFilter(key: HomeQueryState): TypedFilter? { - val follows = key.account.liveHomeListAuthorsPerRelay.value + override fun start() = group.forEach { it.start() } - return if (!follows.isNullOrEmpty()) { - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - AdvertisedRelayListEvent.KIND, - ), - authors = follows, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ) - } else { - null - } - } + override fun stop() = group.forEach { it.stop() } - fun createFollowTagsFilter(key: HomeQueryState): TypedFilter? { - val hashToLoad = - key.account.liveHomeFollowLists.value - ?.hashtags ?: return null + override fun invalidateKeys() = invalidateFilters() - if (hashToLoad.isEmpty()) return null + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - return TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - AudioHeaderEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - CommentEvent.KIND, - WikiNoteEvent.KIND, - ), - tags = - mapOf( - "t" to - hashToLoad - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten(), - ), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ) - } + override fun destroy() = group.forEach { it.start() } - fun createFollowGeohashesFilter(key: HomeQueryState): TypedFilter? { - val hashToLoad = - key.account.liveHomeFollowLists.value - ?.geotags ?: return null - - if (hashToLoad.isEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - tags = - mapOf( - "g" to hashToLoad.toList(), - ), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ) - } - - fun createFollowCommunitiesFilter(key: HomeQueryState): TypedFilter? { - val communitiesToLoad = - key.account.liveHomeFollowLists.value - ?.addresses ?: return null - - if (communitiesToLoad.isEmpty()) return null - - return TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - CommunityPostApprovalEvent.KIND, - CommentEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - ), - tags = - mapOf( - "a" to communitiesToLoad.toList(), - ), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultHomeFollowList.value) - ?.relayList, - ), - ) - } - - fun mergeAllFilters(key: HomeQueryState): List? = - ( - createFollowAccountsFilter(key) + - listOfNotNull( - createFollowMetadataAndReleaseFilter(key), - createFollowCommunitiesFilter(key), - createFollowTagsFilter(key), - createFollowGeohashesFilter(key), - ) - ).ifEmpty { null } - - val userJobMap = mutableMapOf() - val userJobMap2 = mutableMapOf() - val userSubscriptionMap = mutableMapOf() - - fun newSub(key: HomeQueryState): Subscription { - userJobMap[key.account.userProfile()]?.cancel() - userJobMap[key.account.userProfile()] = - key.scope.launch(Dispatchers.Default) { - key.account.liveHomeFollowLists.collect { - invalidateFilters() - } - } - - userJobMap2[key.account.userProfile()]?.cancel() - userJobMap2[key.account.userProfile()] = - key.scope.launch(Dispatchers.Default) { - key.account.liveHomeListAuthorsPerRelay.collect { - invalidateFilters() - } - } - - return requestNewSubscription { time, relayUrl -> - latestEOSEs.addOrUpdate( - key.account.userProfile(), - key.account.settings.defaultHomeFollowList.value, - relayUrl, - time, - ) - } - } - - fun endSub( - key: User, - subId: String, - ) { - dismissSubscription(subId) - userJobMap[key]?.cancel() - userJobMap2[key]?.cancel() - } - - fun findOrCreateSubFor(key: HomeQueryState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt new file mode 100644 index 000000000..2c76f6396 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.mixGeohashHashtagsCommunities + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash.filterHomePostsByGeohashes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags.filterHomePostsByHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromCommunities +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class MixGeohashHashtagsCommunityEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterHomePostsByHashtags(key.followLists()?.hashtags, since), + filterHomePostsByGeohashes(key.followLists()?.geotags, since), + filterHomePostsFromCommunities(key.followLists()?.addresses, since), + ).flatten() + + override fun user(query: HomeQueryState) = query.account.userProfile() + + fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists + + fun HomeQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.cancel() + userJobMap[user] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt new file mode 100644 index 000000000..7a760b249 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterHomePostsByGeohashes( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ), + tags = mapOf("g" to hashToLoad.toList()), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt new file mode 100644 index 000000000..28e47001a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class GeohashEventsEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = filterHomePostsByGeohashes(key.followLists()?.geotags, since) + + override fun user(query: HomeQueryState) = query.account.userProfile() + + fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists + + fun HomeQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.cancel() + userJobMap[user] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt new file mode 100644 index 000000000..346060b51 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterHomePostsByHashtags( + hashToLoad: Set?, + since: Map?, +): List? { + if (hashToLoad == null || hashToLoad.isEmpty()) return null + + val hashtags = hashToLoad.map { hashtagAlts(it) }.flatten().distinct() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + AudioHeaderEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + CommentEvent.KIND, + WikiNoteEvent.KIND, + ), + tags = mapOf("t" to hashtags), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt new file mode 100644 index 000000000..f9317c724 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class HashtagEventsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = + filterHomePostsByHashtags( + key.followLists()?.hashtags, + since, + ) + + override fun user(query: HomeQueryState) = query.account.userProfile() + + fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists + + fun HomeQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.cancel() + userJobMap[user] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt new file mode 100644 index 000000000..5a550099e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterHomePostsByFollows( + follows: Map>?, + since: Map?, +): List { + if (follows != null && follows.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ClassifiedsEvent.KIND, + LongTextNoteEvent.KIND, + EphemeralChatEvent.KIND, + HighlightEvent.KIND, + ), + authors = follows, + limit = 400, + since = since, + ), + ), + TypedFilter( + types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + kinds = + listOf( + PollNoteEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + PinListEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + WikiNoteEvent.KIND, + ), + authors = follows, + limit = 400, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt new file mode 100644 index 000000000..ede780526 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent + +fun filterUserMetadataByFollows( + follows: Map>?, + since: Map?, +): List { + if (follows != null && follows.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = setOf(if (follows != null) FeedType.FOLLOWS else FeedType.GLOBAL), + filter = + SinceAuthorPerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + AdvertisedRelayListEvent.KIND, + ), + authors = follows, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt new file mode 100644 index 000000000..81732a336 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch + +class HomeOutboxEventsEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = filterHomePostsByFollows(key.followRelay(), since) + + override fun user(key: HomeQueryState) = key.account.userProfile() + + override fun list(key: HomeQueryState) = key.listName() + + fun HomeQueryState.listNameFlow() = account.settings.defaultHomeFollowList + + fun HomeQueryState.listName() = listNameFlow().value + + fun HomeQueryState.followRelayFlow() = account.liveHomeListAuthorsPerRelay + + fun HomeQueryState.followRelay() = followRelayFlow().value + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.scope.launch(Dispatchers.Default) { + key.listNameFlow().collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.followRelayFlow().sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt new file mode 100644 index 000000000..deb27f8aa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch + +class HomeOutboxUsersEoseManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = filterUserMetadataByFollows(key.followRelay(), since) + + override fun user(key: HomeQueryState) = key.account.userProfile() + + override fun list(key: HomeQueryState) = key.listName() + + fun HomeQueryState.listNameFlow() = account.settings.defaultHomeFollowList + + fun HomeQueryState.listName() = listNameFlow().value + + fun HomeQueryState.followRelayFlow() = account.liveHomeListAuthorsPerRelay + + fun HomeQueryState.followRelay() = followRelayFlow().value + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.scope.launch(Dispatchers.Default) { + key.listNameFlow().collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.followRelayFlow().sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt new file mode 100644 index 000000000..260fea96e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class CommunityEventsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: HomeQueryState, + since: Map?, + ): List? = + filterHomePostsFromCommunities( + key.followLists()?.addresses, + since, + ) + + override fun user(query: HomeQueryState) = query.account.userProfile() + + fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists + + fun HomeQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: HomeQueryState): Subscription { + val user = user(key) + userJobMap[user]?.cancel() + userJobMap[user] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt new file mode 100644 index 000000000..b62b052f2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities + +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +fun filterHomePostsFromCommunities( + communitiesToLoad: Set?, + since: Map?, +): List? { + if (communitiesToLoad == null || communitiesToLoad.isEmpty()) return null + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, + CommunityPostApprovalEvent.KIND, + CommentEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + ), + tags = + mapOf( + "a" to communitiesToLoad.toList(), + ), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt new file mode 100644 index 000000000..cfda579d6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent + +fun filterUserProfileFollowers( + key: HexKey, + since: Map?, +): List { + if (key.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf(key)), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt new file mode 100644 index 000000000..7121156fd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent + +fun filterUserProfileLists( + keys: Set, + since: Map?, +): List { + if (keys.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + BookmarkListEvent.KIND, + PeopleListEvent.KIND, + FollowListEvent.KIND, + AppRecommendationEvent.KIND, + ), + authors = keys.toList(), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt new file mode 100644 index 000000000..9678a2edf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent + +fun filterUserProfileMedia( + key: HexKey, + since: Map?, +): List { + if (key.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + PictureEvent.KIND, + ProfileGalleryEntryEvent.KIND, + VideoVerticalEvent.KIND, + VideoHorizontalEvent.KIND, + ), + authors = listOf(key), + limit = 200, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt new file mode 100644 index 000000000..3089ab809 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent + +fun filterUserProfileMetadata( + keys: Set, + since: Map?, +): List { + if (keys.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + MetadataEvent.KIND, + AdvertisedRelayListEvent.KIND, + ContactListEvent.KIND, + BadgeProfilesEvent.KIND, + ), + authors = keys.toList(), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt new file mode 100644 index 000000000..6ae05769e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent + +fun filterUserProfilePosts( + key: HexKey, + since: Map?, +): List { + if (key.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + TextNoteEvent.KIND, + GenericRepostEvent.KIND, + RepostEvent.KIND, + LongTextNoteEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, + ), + authors = listOf(key), + limit = 200, + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = + listOf( + TorrentEvent.KIND, + TorrentCommentEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + CommentEvent.KIND, + ), + authors = listOf(key), + limit = 50, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt new file mode 100644 index 000000000..d48bba1ff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent + +fun filterUserProfileZapsReceived( + key: HexKey, + since: Map?, +): List { + if (key.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + kinds = listOf(LnZapEvent.KIND), + tags = mapOf("p" to listOf(key)), + limit = 200, + since = since, + ), + ), + ) +} 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 5cd3bd6c5..6a53bb88b 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 @@ -21,39 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent // This allows multiple screen to be listening to tags, even the same tag class UserProfileQueryState( @@ -62,222 +31,26 @@ class UserProfileQueryState( class UserProfileFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val list = "A" - val latestEOSEs = EOSEAccount() - - fun since(key: UserProfileQueryState) = - latestEOSEs.users[key.user] - ?.followList - ?.get(list) - ?.relayList - - fun newEose( - key: UserProfileQueryState, - relayUrl: String, - time: Long, - ) { - latestEOSEs.addOrUpdate( - key.user, - list, - relayUrl, - time, +) : ComposeSubscriptionManager() { + val group = + listOf( + // 5 subs per visible user profile screen. + UserProfileMetadataFilterSubAssembler(client, ::allKeys), + UserProfilePostsFilterSubAssembler(client, ::allKeys), + UserProfileMediaFilterSubAssembler(client, ::allKeys), + UserProfileFollowersFilterSubAssembler(client, ::allKeys), + UserProfileZapsFilterSubAssembler(client, ::allKeys), ) - } - fun createUserInfoFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - authors = listOf(state.user.pubkeyHex), - limit = 1, - since = since(state), - ), - ) - } + override fun start() = group.forEach { it.start() } - fun createUserPostsFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - GenericRepostEvent.KIND, - RepostEvent.KIND, - LongTextNoteEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - PinListEvent.KIND, - PollNoteEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - ), - authors = listOf(state.user.pubkeyHex), - limit = 200, - since = since(state), - ), - ) - } + override fun stop() = group.forEach { it.stop() } - fun createUserPostsFilter2(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TorrentEvent.KIND, - TorrentCommentEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - CommentEvent.KIND, - ), - authors = listOf(state.user.pubkeyHex), - limit = 50, - since = since(state), - ), - ) - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - fun createUserReceivedZapsFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(LnZapEvent.KIND), - tags = mapOf("p" to listOf(state.user.pubkeyHex)), - limit = 200, - since = since(state), - ), - ) - } + override fun invalidateKeys() = invalidateFilters() - fun createFollowFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ContactListEvent.KIND), - authors = listOf(state.user.pubkeyHex), - limit = 1, - since = since(state), - ), - ) - } + override fun destroy() = group.forEach { it.destroy() } - fun createFollowersFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf(state.user.pubkeyHex)), - since = since(state), - ), - ) - } - - fun createAcceptedAwardsFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(BadgeProfilesEvent.KIND), - authors = listOf(state.user.pubkeyHex), - limit = 1, - since = since(state), - ), - ) - } - - fun createBookmarksFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - BookmarkListEvent.KIND, - PeopleListEvent.KIND, - FollowListEvent.KIND, - AppRecommendationEvent.KIND, - ), - authors = listOf(state.user.pubkeyHex), - limit = 100, - since = since(state), - ), - ) - } - - fun createProfileGalleryFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - ProfileGalleryEntryEvent.KIND, - PictureEvent.KIND, - VideoVerticalEvent.KIND, - VideoHorizontalEvent.KIND, - ), - authors = listOf(state.user.pubkeyHex), - limit = 1000, - since = since(state), - ), - ) - } - - fun createReceivedAwardsFilter(keys: List) = - keys.map { state -> - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(BadgeAwardEvent.KIND), - tags = mapOf("p" to listOf(state.user.pubkeyHex)), - limit = 20, - since = since(state), - ), - ) - } - - val userInfoChannel = - requestNewSubscription { time, relayUrl -> - forEachSubscriber { - newEose(it, relayUrl, time) - } - } - - override fun updateSubscriptions(keys1: Set) { - if (keys1.isEmpty()) return - - val keys = keys1.distinctBy { it.user.pubkeyHex } - - userInfoChannel.typedFilters = - listOfNotNull( - createUserInfoFilter(keys), - createUserPostsFilter(keys), - createUserPostsFilter2(keys), - createProfileGalleryFilter(keys), - createFollowFilter(keys), - createFollowersFilter(keys), - createUserReceivedZapsFilter(keys), - createAcceptedAwardsFilter(keys), - createReceivedAwardsFilter(keys), - createBookmarksFilter(keys), - ).flatten().ifEmpty { null } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt new file mode 100644 index 000000000..716a6231c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +class UserProfileFollowersFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: UserProfileQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterUserProfileFollowers(user(key).pubkeyHex, since), + ).flatten() + + override fun user(key: UserProfileQueryState) = key.user +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt new file mode 100644 index 000000000..c1db372aa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.flatten + +class UserProfileMediaFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: UserProfileQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterUserProfileMedia(user(key).pubkeyHex, since), + ).flatten() + + override fun user(key: UserProfileQueryState) = key.user +} 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 new file mode 100644 index 000000000..c702d4be4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.flatten + +class UserProfileMetadataFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + override fun updateFilter( + keys: List, + since: Map?, + ): List? { + val keys = keys.mapTo(mutableSetOf()) { key -> key.user.pubkeyHex } + + // TODO: Load outbox for each user. + + return listOfNotNull( + filterUserProfileMetadata(keys, since), + filterUserProfileLists(keys, since), + ).flatten() + } + + override fun distinct(key: UserProfileQueryState) = key.user +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt new file mode 100644 index 000000000..5e793e90f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.flatten + +class UserProfilePostsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: UserProfileQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterUserProfilePosts(user(key).pubkeyHex, since), + ).flatten() + + override fun user(key: UserProfileQueryState) = key.user +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt new file mode 100644 index 000000000..17bcd3800 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlin.collections.flatten + +class UserProfileZapsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: UserProfileQueryState, + since: Map?, + ): List? = + listOfNotNull( + filterUserProfileZapsReceived(user(key).pubkeyHex, since), + ).flatten() + + override fun user(key: UserProfileQueryState) = key.user +} 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 4adb4ebf5..424b9c21f 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,13 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.ThreadAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +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.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey // This allows multiple screen to be listening to tags, even the same tag @@ -36,72 +33,22 @@ class ThreadQueryState( class ThreadFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - fun createLoadEventsIfNotLoadedFilter(keys: Set): List { - if (keys.isEmpty()) return emptyList() - - val uniqueThreadIds = keys.mapTo(mutableSetOf()) { it.eventId } - - val branches = - uniqueThreadIds.map { - ThreadAssembler().findThreadFor(it) ?: return emptyList() - } - - val eventsToLoad = - branches - .mapNotNull { - it.allNotes - .filter { it.event == null } - .map { it.idHex } - .toSet() - .ifEmpty { null } - }.flatten() - .distinct() - - val addressRoots = - branches - .mapNotNull { - if (it.root is AddressableNote) it.root.idHex else null - }.distinct() - - val eventRoots = - branches - .mapNotNull { - if (it.root !is AddressableNote) it.root.idHex else it.root.event?.id - }.distinct() - - return listOfNotNull( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter(tags = mapOf("e" to eventRoots)), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter(tags = mapOf("a" to addressRoots)), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter(tags = mapOf("E" to eventRoots)), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter(tags = mapOf("A" to addressRoots)), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = SincePerRelayFilter(ids = eventsToLoad), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + ThreadFilterSubAssembler(client, ::allKeys), + ThreadEventLoaderSubAssembler(client, ::allKeys), ) - } - val loadEventsChannel = - requestNewSubscription { _, _ -> - // Many relays operate with limits in the amount of filters. - // As information comes, the filters will be rotated to get more data. - invalidateFilters() - } + override fun start() = group.forEach { it.start() } - override fun updateSubscriptions(keys: Set) { - loadEventsChannel.typedFilters = createLoadEventsIfNotLoadedFilter(keys).ifEmpty { null } - } + override fun stop() = group.forEach { it.stop() } + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.start() } + + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt new file mode 100644 index 000000000..a58045623 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter + +fun filterEventsInThreadForRoot( + root: Note, + since: Map?, +): List { + val addressRoot = if (root is AddressableNote) root.idHex else null + val eventRoot = if (root !is AddressableNote) root.idHex else root.event?.id + + val address = + if (addressRoot != null) { + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("a" to listOf(addressRoot)), + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("A" to listOf(addressRoot)), + since = since, + ), + ), + ) + } else { + emptyList() + } + + val event = + if (eventRoot != null) { + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("e" to listOf(eventRoot)), + since = since, + ), + ), + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + SincePerRelayFilter( + tags = mapOf("E" to listOf(eventRoot)), + since = since, + ), + ), + ) + } else { + emptyList() + } + + return address + event +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt new file mode 100644 index 000000000..a962bd458 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address + +fun filterMissingEventsForThread( + threadInfo: ThreadAssembler.ThreadInfo, + since: Map?, +): List { + val missingEvents = mutableSetOf() + val missingAddresses = mutableSetOf
() + + if (threadInfo.root.event == null) { + if (threadInfo.root is AddressableNote) { + missingAddresses.add(threadInfo.root.address) + } else { + missingEvents.add(threadInfo.root.idHex) + } + } + + threadInfo.allNotes.forEach { + if (it.event == null) { + if (it is AddressableNote) { + missingAddresses.add(it.address) + } else { + missingEvents.add(it.idHex) + } + } + } + + val missingEventsFilter = filterMissingEvents(missingEvents, since) ?: emptyList() + val missingAddressFilter = filterMissingAddressables(missingAddresses, since) ?: emptyList() + + return missingEventsFilter + missingAddressFilter +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt new file mode 100644 index 000000000..474923975 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies + +import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * Loads all missing events in each thread. + * + * This rotates really fast with the goal to get to zero + * events needed as fast as possible + * + * Each root has a relay subscription subscription for itself and + * saves the of the thread EOSEs in the long run to avoid re-downloading + */ +class ThreadEventLoaderSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun updateFilter( + key: ThreadQueryState, + since: Map?, + ): List? { + val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null + return filterMissingEventsForThread(branches, since) + } + + override fun id(key: ThreadQueryState) = key.eventId +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt new file mode 100644 index 000000000..d3e61aa8c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies + +import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime + +/** + * Loads all events in the Thread that cites the root post. + * + * Each root has a relay subscription subscription for itself and + * saves the of the thread EOSEs in the long run to avoid re-downloading + */ +class ThreadFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: ThreadQueryState, + since: Map?, + ): List? { + val root = ThreadAssembler().findRoot(key.eventId) ?: return null + + return filterEventsInThreadForRoot(root, since) + } + + override fun id(key: ThreadQueryState) = key.eventId +} 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 9a6c7519d..61299c0f5 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 @@ -21,25 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.QueryBasedSubscriptionOrchestrator -import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoMixGeohashHashtagsFilterSubAssembler +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import java.util.Locale val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() @@ -52,214 +38,22 @@ class VideoQueryState( class VideoFilterAssembler( client: NostrClient, -) : QueryBasedSubscriptionOrchestrator(client) { - val latestEOSEs = EOSEAccount() - - fun createContextualFilter(key: VideoQueryState): List { - val follows = key.account.liveStoriesListAuthorsPerRelay.value - - val types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS) - - return listOf( - TypedFilter( - types = types, - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - limit = 200, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), - TypedFilter( - types = types, - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - limit = 200, - tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES), - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), +) : ComposeSubscriptionManager() { + val group = + listOf( + VideoOutboxEventsFilterSubAssembler(client, ::allKeys), + VideoMixGeohashHashtagsFilterSubAssembler(client, ::allKeys), ) - } - fun createFollowTagsFilter(key: VideoQueryState): List { - val hashToLoad = - key.account.liveStoriesFollowLists.value - ?.hashtags - ?.toList() ?: return emptyList() + override fun start() = group.forEach { it.start() } - if (hashToLoad.isEmpty()) return emptyList() + override fun stop() = group.forEach { it.stop() } - val hashtags = - hashToLoad - .map { - listOf( - it, - it.lowercase(), - it.uppercase(), - it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - }, - ) - }.flatten() + override fun invalidateKeys() = invalidateFilters() - return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - tags = mapOf("t" to hashtags), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - tags = - mapOf( - "t" to hashtags, - "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, - ), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), - ) - } + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - fun createFollowGeohashesFilter(key: VideoQueryState): List { - val hashToLoad = - key.account.liveStoriesFollowLists.value - ?.geotags - ?.toList() ?: return emptyList() + override fun destroy() = group.forEach { it.start() } - if (hashToLoad.isEmpty()) return emptyList() - - val geoHashes = hashToLoad - - return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - tags = mapOf("g" to geoHashes), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - tags = - mapOf( - "g" to geoHashes, - "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, - ), - limit = 100, - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.account.settings.defaultStoriesFollowList.value) - ?.relayList, - ), - ), - ) - } - - fun mergeAllFilters(key: VideoQueryState): List? = - listOfNotNull( - createContextualFilter(key), - createFollowTagsFilter(key), - createFollowGeohashesFilter(key), - ).flatten().ifEmpty { null } - - val userJobMap = mutableMapOf() - val userSubscriptionMap = mutableMapOf() - - fun newSub(key: VideoQueryState): Subscription { - userJobMap[key.account.userProfile()]?.cancel() - userJobMap[key.account.userProfile()] = - key.scope.launch(Dispatchers.Default) { - key.account.liveStoriesFollowLists.collect { - invalidateFilters() - } - } - - return requestNewSubscription { time, relayUrl -> - latestEOSEs.addOrUpdate( - key.account.userProfile(), - key.account.settings.defaultStoriesFollowList.value, - relayUrl, - time, - ) - } - } - - fun endSub( - key: User, - subId: String, - ) { - dismissSubscription(subId) - userJobMap[key]?.cancel() - } - - fun findOrCreateSubFor(key: VideoQueryState): Subscription { - var subId = userSubscriptionMap[key.account.userProfile()] - return if (subId == null) { - newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } else { - getSub(subId) ?: newSub(key).also { userSubscriptionMap[key.account.userProfile()] = it.id } - } - } - - // One sub per subscribed account - override fun updateSubscriptions(keys: Set) { - val uniqueSubscribedAccounts = keys.distinctBy { it.account } - - val updated = mutableSetOf() - - uniqueSubscribedAccounts.forEach { - val user = it.account.userProfile() - val sub = findOrCreateSubFor(it) - sub.typedFilters = mergeAllFilters(it) - - updated.add(user) - } - - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } - } - } + override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt new file mode 100644 index 000000000..d97ce48ce --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent + +fun filterPictureAndVideoByFollows( + follows: Map>?, + since: Map?, +): List { + if (follows != null && follows.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), + limit = 200, + since = since, + ), + ), + TypedFilter( + types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), + filter = + SinceAuthorPerRelayFilter( + authors = follows, + kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), + limit = 200, + tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES), + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt new file mode 100644 index 000000000..13afec5c1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent + +fun filterPictureAndVideoByGeohash( + hashToLoad: Set?, + since: Map?, +): List { + if (hashToLoad == null || hashToLoad.isEmpty()) return emptyList() + + val geoHashes = hashToLoad.toList() + + return listOf( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), + tags = mapOf("g" to geoHashes), + limit = 100, + since = since, + ), + ), + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + SincePerRelayFilter( + kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), + tags = + mapOf( + "g" to geoHashes, + "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, + ), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt new file mode 100644 index 000000000..e035f9cda --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import kotlin.collections.flatten + +fun filterPictureAndVideoByHashtag( + hashToLoad: Set?, + since: Map?, +): List { + if (hashToLoad == null || hashToLoad.isEmpty()) return emptyList() + + val hashtags = hashToLoad.map { hashtagAlts(it) }.flatten().distinct() + + if (hashtags.isEmpty()) return emptyList() + + return listOf( + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), + tags = mapOf("t" to hashtags), + limit = 100, + since = since, + ), + ), + TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = + SincePerRelayFilter( + kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), + tags = + mapOf( + "t" to hashtags, + "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, + ), + limit = 100, + since = since, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt new file mode 100644 index 000000000..367870d46 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class VideoMixGeohashHashtagsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun updateFilter( + key: VideoQueryState, + since: Map?, + ): List? = + filterPictureAndVideoByHashtag( + key.followLists()?.hashtags, + since, + ) + + filterPictureAndVideoByGeohash( + key.followLists()?.geotags, + since, + ) + + override fun user(key: VideoQueryState) = key.account.userProfile() + + fun VideoQueryState.followListsFlow() = account.liveStoriesFollowLists + + fun VideoQueryState.followLists() = followListsFlow().value + + val userJobMap = mutableMapOf() + + override fun newSub(key: VideoQueryState): Subscription { + userJobMap[key.account.userProfile()]?.cancel() + userJobMap[key.account.userProfile()] = + key.scope.launch(Dispatchers.Default) { + key.followListsFlow().collectLatest { + invalidateFilters() + } + } + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.cancel() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt new file mode 100644 index 000000000..8ad7ca010 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState +import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.ammolite.relays.filters.EOSETime +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch + +class VideoOutboxEventsFilterSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: VideoQueryState, + since: Map?, + ): List? = filterPictureAndVideoByFollows(key.followsPerRelay(), since) + + override fun user(key: VideoQueryState) = key.account.userProfile() + + override fun list(key: VideoQueryState) = key.listName() + + fun VideoQueryState.listNameFlow() = account.settings.defaultStoriesFollowList + + fun VideoQueryState.listName() = listNameFlow().value + + fun VideoQueryState.followListsFlow() = account.liveStoriesFollowLists + + fun VideoQueryState.followLists() = followListsFlow().value + + fun VideoQueryState.followsPerRelayFlow() = account.liveStoriesListAuthorsPerRelay + + fun VideoQueryState.followsPerRelay() = followsPerRelayFlow().value + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: VideoQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.scope.launch(Dispatchers.Default) { + key.listNameFlow().collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.followsPerRelayFlow().sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + return super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionOrchestrator.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt similarity index 91% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionOrchestrator.kt rename to ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt index 2c06f67d1..cec31055a 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionOrchestrator.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import java.util.concurrent.atomic.AtomicBoolean @@ -35,9 +34,10 @@ import java.util.concurrent.atomic.AtomicBoolean * Semantically groups Nostr filters and subscriptions in data source objects that * maintain the desired active filter with the relay. */ -abstract class SubscriptionOrchestrator( +class SubscriptionController( val client: NostrClient, -) { + val updateSubscriptions: () -> Unit, +) : SubscriptionControllerService { private val subscriptions = SubscriptionSet() private var active: Boolean = false private val changingFilters = AtomicBoolean() @@ -50,13 +50,13 @@ abstract class SubscriptionOrchestrator( event: Event, subscriptionId: String, relay: Relay, + arrivalTime: Long, afterEOSE: Boolean, ) { if (subscriptions.contains(subscriptionId)) { stats.add(subscriptionId, event.kind) - if (afterEOSE) { - runAfterEOSE(subscriptionId, relay) + runAfterEOSE(subscriptionId, relay, arrivalTime) } } } @@ -64,18 +64,20 @@ abstract class SubscriptionOrchestrator( override fun onEOSE( relay: Relay, subscriptionId: String, + arrivalTime: Long, ) { if (subscriptions.contains(subscriptionId)) { - runAfterEOSE(subscriptionId, relay) + runAfterEOSE(subscriptionId, relay, arrivalTime) } } } - fun runAfterEOSE( + private fun runAfterEOSE( subscriptionId: String, relay: Relay, + arrivalTime: Long, ) { - subscriptions[subscriptionId]?.callEose(TimeUtils.oneMinuteAgo(), relay.url) + subscriptions[subscriptionId]?.callEose(arrivalTime, relay.url) } init { @@ -83,7 +85,7 @@ abstract class SubscriptionOrchestrator( client.subscribe(clientListener) } - fun destroy() { + override fun destroy() { // makes sure to run Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe") stop() @@ -91,14 +93,14 @@ abstract class SubscriptionOrchestrator( bundler.cancel() } - open fun start() { + override fun start() { Log.d("${this.javaClass.simpleName}", "Start") active = true invalidateFilters() } @OptIn(DelicateCoroutinesApi::class) - open fun stop() { + override fun stop() { active = false Log.d("${this.javaClass.simpleName}", "Stop") @@ -108,13 +110,13 @@ abstract class SubscriptionOrchestrator( } } + override fun printStats(tag: String) = stats.printCounter(tag) + fun getSub(subId: String) = subscriptions.get(subId) fun requestNewSubscription(onEOSE: ((Long, String) -> Unit)? = null): Subscription = subscriptions.newSub(onEOSE) - fun dismissSubscription(subId: String) { - getSub(subId)?.let { dismissSubscription(it) } - } + fun dismissSubscription(subId: String) = getSub(subId)?.let { dismissSubscription(it) } fun dismissSubscription(subscription: Subscription) { client.close(subscription.id) @@ -127,7 +129,7 @@ abstract class SubscriptionOrchestrator( // Refreshes observers in batches. private val bundler = BundledUpdate(300, Dispatchers.Default) - fun invalidateFilters() { + override fun invalidateFilters() { bundler.invalidate { // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") @@ -211,6 +213,4 @@ abstract class SubscriptionOrchestrator( } } } - - abstract fun updateSubscriptions() } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt new file mode 100644 index 000000000..3ba32a178 --- /dev/null +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2024 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.ammolite.relays.datasources + +interface SubscriptionControllerService { + fun start() + + fun stop() + + fun invalidateFilters() + + fun destroy() + + fun printStats(tag: String) +}