mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 00:37:41 +00:00
3rd large migration to outbox.
This commit is contained in:
Generated
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinNotebookOptionsProvider">
|
||||
<option name="shouldAddProjectLibrariesToClasspath" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JsCompilerArguments">
|
||||
<option name="moduleKind" value="plain" />
|
||||
</component>
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="1.8" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.1.0" />
|
||||
</component>
|
||||
|
||||
@@ -174,7 +174,7 @@ Lastly, the user's account information (private key/pub key) is stored in the An
|
||||
## Setup
|
||||
|
||||
Make sure to have the following pre-requisites installed:
|
||||
1. Java 17+
|
||||
1. Java 21+
|
||||
2. Android Studio
|
||||
3. Android 8.0+ Phone or Emulation setup
|
||||
|
||||
|
||||
@@ -37,17 +37,19 @@ import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
|
||||
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -89,18 +91,26 @@ class Amethyst : Application() {
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
val factory =
|
||||
OkHttpWebSocket.BuilderFactory { _, useProxy ->
|
||||
okHttpClients.getHttpClient(useProxy)
|
||||
}
|
||||
val torProxySettingsAnchor = ProxySettingsAnchor()
|
||||
|
||||
// Provides a relay pool
|
||||
val client: NostrClient = NostrClient(factory)
|
||||
// Connects the NostrClient class with okHttp
|
||||
val websocketBuilder =
|
||||
OkHttpWebSocket.Builder { url ->
|
||||
okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url))
|
||||
}
|
||||
|
||||
// Caches all events in Memory
|
||||
val cache: LocalCache = LocalCache
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
|
||||
// Provides a relay pool
|
||||
val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope)
|
||||
|
||||
// Watches for changes on Tor and Relay List Settings
|
||||
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, applicationIOScope)
|
||||
|
||||
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||
|
||||
@@ -112,15 +122,9 @@ class Amethyst : Application() {
|
||||
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
||||
|
||||
// Trash.
|
||||
val serviceManager = ServiceManager(client, applicationIOScope)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ import kotlin.time.measureTimedValue
|
||||
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||
|
||||
fun debugState(context: Context) {
|
||||
Amethyst.instance.client
|
||||
.allSubscriptions()
|
||||
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
|
||||
// Amethyst.instance.client
|
||||
// .allSubscriptions()
|
||||
// .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") }
|
||||
|
||||
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
|
||||
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
|
||||
|
||||
@@ -27,16 +27,15 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.content.edit
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -53,7 +52,10 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -82,7 +84,6 @@ private object PrefKeys {
|
||||
const val SAVED_ACCOUNTS = "all_saved_accounts"
|
||||
const val NOSTR_PRIVKEY = "nostr_privkey"
|
||||
const val NOSTR_PUBKEY = "nostr_pubkey"
|
||||
const val RELAYS = "relays"
|
||||
const val LOCAL_RELAY_SERVERS = "localRelayServers"
|
||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
||||
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
||||
@@ -99,6 +100,9 @@ private object PrefKeys {
|
||||
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
|
||||
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
|
||||
const val LATEST_CHANNEL_LIST = "latestChannelList"
|
||||
const val LATEST_COMMUNITY_LIST = "latestCommunityList"
|
||||
const val LATEST_HASHTAG_LIST = "latestHashtagList"
|
||||
const val LATEST_GEOHASH_LIST = "latestGeohashList"
|
||||
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||
@@ -120,8 +124,8 @@ object LocalPreferences {
|
||||
private const val COMMA = ","
|
||||
|
||||
private var currentAccount: String? = null
|
||||
private var savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
|
||||
private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
||||
private val savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
|
||||
private val cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
||||
|
||||
suspend fun currentAccount(): String? {
|
||||
if (currentAccount == null) {
|
||||
@@ -249,7 +253,7 @@ object LocalPreferences {
|
||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
} else {
|
||||
return Amethyst.instance.encryptedStorage(npub)
|
||||
Amethyst.instance.encryptedStorage(npub)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +303,6 @@ object LocalPreferences {
|
||||
settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||
}
|
||||
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putString(PrefKeys.RELAYS, EventMapper.mapper.writeValueAsString(settings.localRelays))
|
||||
|
||||
putString(
|
||||
PrefKeys.DEFAULT_FILE_SERVER,
|
||||
@@ -364,8 +367,8 @@ object LocalPreferences {
|
||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.localRelayServers.isNotEmpty()) {
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
||||
if (settings.localRelayServers.value.isNotEmpty()) {
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
|
||||
} else {
|
||||
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
||||
}
|
||||
@@ -406,6 +409,33 @@ object LocalPreferences {
|
||||
remove(PrefKeys.LATEST_CHANNEL_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupCommunityList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_COMMUNITY_LIST,
|
||||
EventMapper.mapper.writeValueAsString(settings.backupCommunityList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_COMMUNITY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupHashtagList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_HASHTAG_LIST,
|
||||
EventMapper.mapper.writeValueAsString(settings.backupHashtagList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_HASHTAG_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupGeohashList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_HASHTAG_LIST,
|
||||
EventMapper.mapper.writeValueAsString(settings.backupGeohashList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_HASHTAG_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupEphemeralChatList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_EPHEMERAL_LIST,
|
||||
@@ -453,9 +483,8 @@ object LocalPreferences {
|
||||
prefs: SharedPreferences = encryptedPreferences(),
|
||||
) {
|
||||
Log.d("LocalPreferences", "Saving to shared settings")
|
||||
with(prefs.edit()) {
|
||||
prefs.edit {
|
||||
putString(PrefKeys.SHARED_SETTINGS, EventMapper.mapper.writeValueAsString(sharedSettings))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +543,7 @@ object LocalPreferences {
|
||||
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||
|
||||
val defaultHomeFollowList =
|
||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS
|
||||
val defaultStoriesFollowList =
|
||||
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
val defaultNotificationFollowList =
|
||||
@@ -522,8 +551,6 @@ object LocalPreferences {
|
||||
val defaultDiscoveryFollowList =
|
||||
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
|
||||
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()
|
||||
|
||||
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
|
||||
|
||||
@@ -538,8 +565,11 @@ object LocalPreferences {
|
||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
|
||||
val latestChannelList = parseEventOrNull<ChannelListEvent>(PrefKeys.LATEST_CHANNEL_LIST)
|
||||
val latestCommunityList = parseEventOrNull<CommunityListEvent>(PrefKeys.LATEST_COMMUNITY_LIST)
|
||||
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
|
||||
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
|
||||
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
|
||||
|
||||
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
@@ -559,14 +589,13 @@ object LocalPreferences {
|
||||
keyPair = keyPair,
|
||||
transientAccount = false,
|
||||
externalSignerPackageName = externalSignerPackageName,
|
||||
localRelays = localRelays,
|
||||
localRelayServers = localRelayServers,
|
||||
localRelayServers = MutableStateFlow(localRelayServers),
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
zapPaymentRequest = zapPaymentRequestServer?.normalize(),
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
@@ -579,6 +608,9 @@ object LocalPreferences {
|
||||
backupMuteList = latestMuteList,
|
||||
backupAppSpecificData = latestAppSpecificData,
|
||||
backupChannelList = latestChannelList,
|
||||
backupCommunityList = latestCommunityList,
|
||||
backupHashtagList = latestHashtagList,
|
||||
backupGeohashList = latestGeohashList,
|
||||
backupEphemeralChatList = latestEphemeralList,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import coil3.annotation.DelicateCoilApi
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class ServiceManager(
|
||||
val client: NostrClient,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
// to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var isStarted: Boolean = false
|
||||
|
||||
private var account: Account? = null
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
|
||||
private fun start(account: Account) {
|
||||
this.account = account
|
||||
start()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoilApi::class)
|
||||
private fun start() {
|
||||
Log.d("ServiceManager", "-- May Start (hasStarted: $isStarted) for account $account")
|
||||
if (isStarted && account != null) {
|
||||
Log.d("ServiceManager", "---- Restarting innactive relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
|
||||
client.reconnect()
|
||||
return
|
||||
}
|
||||
Log.d("ServiceManager", "---- Starting Relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
|
||||
|
||||
val myAccount = account
|
||||
|
||||
if (myAccount != null) {
|
||||
val relaySet = myAccount.connectToRelaysWithProxy.value
|
||||
client.reconnect(relaySet)
|
||||
|
||||
collectorJob?.cancel()
|
||||
collectorJob = null
|
||||
collectorJob =
|
||||
scope.launch {
|
||||
myAccount.connectToRelaysWithProxy.collectLatest {
|
||||
delay(500)
|
||||
if (isStarted) {
|
||||
client.reconnect(it, onlyIfChanged = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun pause() {
|
||||
Log.d("ServiceManager", "-- Pausing Relay Services")
|
||||
|
||||
collectorJob?.cancel()
|
||||
collectorJob = null
|
||||
|
||||
client.reconnect(null)
|
||||
isStarted = false
|
||||
}
|
||||
|
||||
fun cleanObservers() {
|
||||
LocalCache.cleanObservers()
|
||||
}
|
||||
|
||||
// This method keeps the pause/start in a Syncronized block to
|
||||
// avoid concurrent pauses and starts.
|
||||
@Synchronized
|
||||
fun forceRestart(
|
||||
account: Account? = null,
|
||||
start: Boolean = true,
|
||||
pause: Boolean = true,
|
||||
) {
|
||||
Log.d("ServiceManager", "-- Force Restart (start:$start) (pause:$pause) for $account")
|
||||
if (pause) {
|
||||
pause()
|
||||
}
|
||||
|
||||
if (start) {
|
||||
if (account != null) {
|
||||
start(account)
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAccountAndRestart(account: Account) {
|
||||
forceRestart(account, true, true)
|
||||
}
|
||||
|
||||
fun forceRestart() {
|
||||
forceRestart(null, true, true)
|
||||
}
|
||||
|
||||
fun justStartIfItHasAccount() {
|
||||
if (account != null) {
|
||||
forceRestart(null, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseForGood() {
|
||||
forceRestart(null, false, true)
|
||||
}
|
||||
|
||||
fun pauseAndLogOff() {
|
||||
account = null
|
||||
forceRestart(null, false, true)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.ammolite.relays.Constants
|
||||
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -43,11 +41,15 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -67,38 +69,29 @@ val DefaultChannelSet =
|
||||
val DefaultChannels =
|
||||
listOf(
|
||||
// Anigma's Nostr
|
||||
EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", "wss://nos.lol"),
|
||||
EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
|
||||
// Amethyst's Group
|
||||
EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "wss://nos.lol"),
|
||||
EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
|
||||
)
|
||||
|
||||
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
|
||||
|
||||
val DefaultNIP65List =
|
||||
listOf(
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nos.lol/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH),
|
||||
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
|
||||
)
|
||||
|
||||
val DefaultDMRelayList =
|
||||
listOf(
|
||||
RelayUrlFormatter.normalize("wss://auth.nostr1.com"),
|
||||
RelayUrlFormatter.normalize("wss://relay.0xchat.com"),
|
||||
RelayUrlFormatter.normalize("wss://nos.lol"),
|
||||
)
|
||||
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
|
||||
|
||||
val DefaultSearchRelayList =
|
||||
listOf(
|
||||
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
|
||||
RelayUrlFormatter.normalize("wss://nostr.wine"),
|
||||
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
|
||||
RelayUrlFormatter.normalize("wss://search.nos.today"),
|
||||
)
|
||||
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val GLOBAL_FOLLOWS = " Global "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val KIND3_FOLLOWS = " All Follows "
|
||||
val ALL_FOLLOWS = " All Follows "
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val AROUND_ME = " Around Me "
|
||||
@@ -108,14 +101,13 @@ class AccountSettings(
|
||||
val keyPair: KeyPair,
|
||||
val transientAccount: Boolean = false,
|
||||
var externalSignerPackageName: String? = null,
|
||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||
var localRelayServers: Set<String> = setOf(),
|
||||
var localRelayServers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
|
||||
var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0],
|
||||
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
|
||||
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(ALL_FOLLOWS),
|
||||
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||
var zapPaymentRequest: Nip47WalletConnect.Nip47URI? = null,
|
||||
var zapPaymentRequest: Nip47WalletConnect.Nip47URINorm? = null,
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
var hideNIP17WarningDialog: Boolean = false,
|
||||
@@ -128,6 +120,9 @@ class AccountSettings(
|
||||
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
||||
var backupAppSpecificData: AppSpecificDataEvent? = null,
|
||||
var backupChannelList: ChannelListEvent? = null,
|
||||
var backupCommunityList: CommunityListEvent? = null,
|
||||
var backupHashtagList: HashtagListEvent? = null,
|
||||
var backupGeohashList: GeohashListEvent? = null,
|
||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
@@ -200,7 +195,7 @@ class AccountSettings(
|
||||
return false
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?): Boolean {
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
|
||||
if (zapPaymentRequest != newServer) {
|
||||
zapPaymentRequest = newServer
|
||||
saveAccountSettings()
|
||||
@@ -256,11 +251,11 @@ class AccountSettings(
|
||||
// proxy settings
|
||||
// ---
|
||||
fun setTorSettings(newTorSettings: TorSettings): Boolean {
|
||||
if (torSettings.update(newTorSettings)) {
|
||||
return if (torSettings.update(newTorSettings)) {
|
||||
saveAccountSettings()
|
||||
return true
|
||||
true
|
||||
} else {
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,8 +296,8 @@ class AccountSettings(
|
||||
// ----
|
||||
|
||||
fun updateLocalRelayServers(servers: Set<String>) {
|
||||
if (localRelayServers != servers) {
|
||||
localRelayServers = servers
|
||||
if (localRelayServers.value != servers) {
|
||||
localRelayServers.update { servers }
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
@@ -377,6 +372,36 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGeohashListTo(newGeohashList: GeohashListEvent?) {
|
||||
if (newGeohashList == null || newGeohashList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupGeohashList?.id != newGeohashList.id) {
|
||||
backupGeohashList = newGeohashList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHashtagListTo(newHashtagList: HashtagListEvent?) {
|
||||
if (newHashtagList == null || newHashtagList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupHashtagList?.id != newHashtagList.id) {
|
||||
backupHashtagList = newHashtagList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCommunityListTo(newCommunityList: CommunityListEvent?) {
|
||||
if (newCommunityList == null || newCommunityList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupCommunityList?.id != newCommunityList.id) {
|
||||
backupCommunityList = newCommunityList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
|
||||
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
|
||||
|
||||
@@ -492,17 +517,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// local relays
|
||||
// ----
|
||||
|
||||
fun updateLocalRelays(newLocalRelays: Set<RelaySetupInfo>) {
|
||||
if (!localRelays.equals(newLocalRelays)) {
|
||||
localRelays = newLocalRelays
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// attestations
|
||||
// ---
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.util.Log
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.ammolite.relays.RelayStats
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -45,7 +45,7 @@ class AntiSpamFilter {
|
||||
|
||||
fun isSpam(
|
||||
event: Event,
|
||||
relay: RelayBriefInfoCache.RelayBriefInfo?,
|
||||
relay: NormalizedRelayUrl?,
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -74,14 +74,14 @@ class AntiSpamFilter {
|
||||
) {
|
||||
Log.w(
|
||||
"Potential SPAM Message",
|
||||
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}",
|
||||
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} $relay ${event.content.replace("\n", " | ")}",
|
||||
)
|
||||
|
||||
// Log down offenders
|
||||
logOffender(hash, event)
|
||||
|
||||
if (relay != null) {
|
||||
RelayStats.newSpam(relay.url, njumpLink(NEvent.create(event.id, event.pubKey, event.kind, relay.url)))
|
||||
RelayStats.newSpam(relay, njumpLink(NEvent.create(event.id, event.pubKey, event.kind, relay)))
|
||||
}
|
||||
|
||||
flowSpam.tryEmit(AntiSpamState(this))
|
||||
|
||||
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
@@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
@@ -51,7 +51,7 @@ class EphemeralChatChannel(
|
||||
|
||||
override fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
override fun relays() = listOf(roomId.relayUrl)
|
||||
override fun relays() = setOf(roomId.relayUrl)
|
||||
|
||||
override fun toBestDisplayName() = roomId.toDisplayKey()
|
||||
|
||||
@@ -68,17 +68,21 @@ class PublicChatChannel(
|
||||
) : Channel(idHex) {
|
||||
var event: ChannelCreateEvent? = null
|
||||
var infoTags = EmptyTagList
|
||||
var info = ChannelData(null, null, null, null)
|
||||
var info = ChannelDataNorm(null, null, null, null)
|
||||
|
||||
override fun relays() = info.relays ?: super.relays()
|
||||
override fun relays() = info.relays?.toSet() ?: super.relays()
|
||||
|
||||
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, *relays().toTypedArray())
|
||||
fun relayHintUrls() = relays().take(3)
|
||||
|
||||
fun relayHintUrl() = relays().firstOrNull()
|
||||
|
||||
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls())
|
||||
|
||||
fun toNostrUri() = "nostr:${toNEvent()}"
|
||||
|
||||
fun toEventHint() = event?.let { EventHintBundle<ChannelCreateEvent>(it, relays().firstOrNull(), null) }
|
||||
fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) }
|
||||
|
||||
fun toEventId() = EventIdHint(idHex, relays().firstOrNull())
|
||||
fun toEventId() = EventIdHintOptional(idHex, relayHintUrl())
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
@@ -99,7 +103,7 @@ class PublicChatChannel(
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
channelInfo: ChannelData,
|
||||
channelInfo: ChannelDataNorm,
|
||||
updatedAt: Long,
|
||||
) {
|
||||
this.info = channelInfo
|
||||
@@ -130,10 +134,12 @@ class LiveActivitiesChannel(
|
||||
|
||||
fun address() = address
|
||||
|
||||
override fun relays() = info?.allRelayUrls() ?: super.relays()
|
||||
override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays()
|
||||
|
||||
fun relayHintUrl() = relays().firstOrNull()
|
||||
|
||||
fun relayHintUrls() = relays().take(3)
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
channelInfo: LiveActivitiesEvent,
|
||||
@@ -150,11 +156,10 @@ class LiveActivitiesChannel(
|
||||
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
|
||||
|
||||
override fun anyNameStartsWith(prefix: String): Boolean =
|
||||
listOfNotNull(info?.title(), info?.summary())
|
||||
.filter { it.contains(prefix, true) }
|
||||
.isNotEmpty()
|
||||
info?.title()?.contains(prefix, true) == true ||
|
||||
info?.summary()?.contains(prefix, true) == true
|
||||
|
||||
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, *relays().toTypedArray())
|
||||
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrls())
|
||||
|
||||
fun toATag() = ATag(address, relayHintUrl())
|
||||
|
||||
@@ -173,7 +178,8 @@ abstract class Channel(
|
||||
var updatedMetadataAt: Long = 0
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
var lastNoteCreatedAt: Long = 0
|
||||
private var relays = mapOf<RelayBriefInfoCache.RelayBriefInfo, Counter>()
|
||||
|
||||
private var relays = mapOf<NormalizedRelayUrl, Counter>()
|
||||
|
||||
open fun idNote() = Hex.decode(idHex).toNEvent()
|
||||
|
||||
@@ -187,13 +193,13 @@ abstract class Channel(
|
||||
|
||||
open fun profilePicture(): String? = creator?.info?.banner
|
||||
|
||||
open fun relays() =
|
||||
open fun relays(): Set<NormalizedRelayUrl> =
|
||||
relays.keys
|
||||
.toSortedSet { o1, o2 ->
|
||||
val o1Count = relays[o1]?.number ?: 0
|
||||
val o2Count = relays[o2]?.number ?: 0
|
||||
o2Count.compareTo(o1Count) // descending
|
||||
}.map { it.url }
|
||||
}
|
||||
|
||||
open fun updateChannelInfo(
|
||||
creator: User,
|
||||
@@ -206,13 +212,13 @@ abstract class Channel(
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
|
||||
fun addRelaySync(briefInfo: NormalizedRelayUrl) {
|
||||
if (briefInfo !in relays) {
|
||||
relays = relays + Pair(briefInfo, Counter(1))
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: RelayBriefInfoCache.RelayBriefInfo) {
|
||||
fun addRelay(relay: NormalizedRelayUrl) {
|
||||
val counter = relays[relay]
|
||||
if (counter != null) {
|
||||
counter.number++
|
||||
@@ -223,7 +229,7 @@ abstract class Channel(
|
||||
|
||||
fun addNote(
|
||||
note: Note,
|
||||
relay: RelayBriefInfoCache.RelayBriefInfo? = null,
|
||||
relay: NormalizedRelayUrl? = null,
|
||||
) {
|
||||
notes.put(note.idHex, note)
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
object Constants {
|
||||
val nos = RelayUrlNormalizer.normalize("wss://nos.lol")
|
||||
val mom = RelayUrlNormalizer.normalize("wss://nostr.mom")
|
||||
val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net")
|
||||
val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io")
|
||||
val wine = RelayUrlNormalizer.normalize("wss://nostr.wine")
|
||||
val band = RelayUrlNormalizer.normalize("wss://relay.nostr.band")
|
||||
|
||||
val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com")
|
||||
val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org")
|
||||
|
||||
val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social")
|
||||
val bg = RelayUrlNormalizer.normalize("wss://relay.nostr.bg")
|
||||
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
|
||||
val fmtwiz = RelayUrlNormalizer.normalize("wss://nostr.fmt.wiz.biz")
|
||||
|
||||
val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today")
|
||||
|
||||
val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com")
|
||||
val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com")
|
||||
|
||||
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz, bg)
|
||||
|
||||
val defaultSearchRelaySet = setOf(band, wine, where)
|
||||
}
|
||||
@@ -50,19 +50,21 @@ import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.ui.components.HashTag
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderHashTagIconsPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
ThemeComparisonColumn {
|
||||
RenderRegular(
|
||||
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
||||
EmptyTagList,
|
||||
) { word, state ->
|
||||
when (word) {
|
||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
||||
is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,13 +22,11 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.service.replace
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue
|
||||
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
@@ -37,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
@@ -74,6 +73,8 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import com.vitorpamplona.quartz.utils.launchAndWaitAll
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -157,7 +158,7 @@ open class Note(
|
||||
var zapPayments = mapOf<Note, Note?>()
|
||||
private set
|
||||
|
||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
var relays = listOf<NormalizedRelayUrl>()
|
||||
private set
|
||||
|
||||
fun id() = Hex.decode(idHex)
|
||||
@@ -183,14 +184,26 @@ open class Note(
|
||||
}
|
||||
}
|
||||
|
||||
fun relayHintUrl(): String? {
|
||||
fun relayUrls(): List<NormalizedRelayUrl> {
|
||||
val authorRelay = author?.relayHints()?.ifEmpty { null }
|
||||
|
||||
return authorRelay ?: relays
|
||||
}
|
||||
|
||||
fun relayUrlsForReactions(): List<NormalizedRelayUrl> {
|
||||
val authorRelay = author?.inboxRelays()?.ifEmpty { null }
|
||||
|
||||
return authorRelay ?: relays
|
||||
}
|
||||
|
||||
fun relayHintUrl(): NormalizedRelayUrl? {
|
||||
val authorRelay = author?.latestMetadataRelay
|
||||
|
||||
return if (relays.isNotEmpty()) {
|
||||
if (authorRelay != null && relays.any { it.url == authorRelay }) {
|
||||
if (authorRelay != null && relays.any { it == authorRelay }) {
|
||||
authorRelay
|
||||
} else {
|
||||
relays.firstOrNull()?.url
|
||||
relays.firstOrNull()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -293,7 +306,7 @@ open class Note(
|
||||
zaps = mapOf<Note, Note?>()
|
||||
zapPayments = mapOf<Note, Note?>()
|
||||
zapsAmount = BigDecimal.ZERO
|
||||
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
relays = listOf<NormalizedRelayUrl>()
|
||||
|
||||
if (repliesChanged) flowSet?.replies?.invalidateData()
|
||||
if (reactionsChanged) flowSet?.reactions?.invalidateData()
|
||||
@@ -445,17 +458,17 @@ open class Note(
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
|
||||
if (briefInfo !in relays) {
|
||||
relays = relays + briefInfo
|
||||
fun addRelaySync(relay: NormalizedRelayUrl) {
|
||||
if (relay !in relays) {
|
||||
relays = relays + relay
|
||||
}
|
||||
}
|
||||
|
||||
fun hasRelay(relay: RelayBriefInfoCache.RelayBriefInfo) = relay !in relays
|
||||
fun hasRelay(relay: NormalizedRelayUrl) = relay !in relays
|
||||
|
||||
fun addRelay(brief: RelayBriefInfoCache.RelayBriefInfo) {
|
||||
if (brief !in relays) {
|
||||
addRelaySync(brief)
|
||||
fun addRelay(relay: NormalizedRelayUrl) {
|
||||
if (relay !in relays) {
|
||||
addRelaySync(relay)
|
||||
flowSet?.relays?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -779,9 +792,17 @@ open class Note(
|
||||
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
return boosts.firstOrNull {
|
||||
it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo()
|
||||
} != null // 5 minute protection
|
||||
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
|
||||
return boosts.any {
|
||||
it.author == loggedIn && (it.createdAt() ?: 0) > fiveMinsAgo
|
||||
}
|
||||
}
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: HexKey): Boolean {
|
||||
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
|
||||
return boosts.any {
|
||||
(it.createdAt() ?: 0) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
|
||||
}
|
||||
}
|
||||
|
||||
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
|
||||
@@ -823,7 +844,7 @@ open class Note(
|
||||
zapsAmount = BigDecimal.ZERO
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
fun isHiddenFor(accountChoices: HiddenUsersState.LiveHiddenUsers): Boolean {
|
||||
val thisEvent = event ?: return false
|
||||
val hash = thisEvent.pubKey.hashCode()
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache.RelayBriefInfo
|
||||
import com.vitorpamplona.quartz.lightning.Lud06
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
@@ -57,7 +58,7 @@ class User(
|
||||
var info: UserMetadata? = null
|
||||
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var latestMetadataRelay: String? = null
|
||||
var latestMetadataRelay: NormalizedRelayUrl? = null
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
|
||||
@@ -67,7 +68,7 @@ class User(
|
||||
var zaps = mapOf<Note, Note?>()
|
||||
private set
|
||||
|
||||
var relaysBeingUsed = mapOf<String, RelayInfo>()
|
||||
var relaysBeingUsed = mapOf<NormalizedRelayUrl, RelayInfo>()
|
||||
private set
|
||||
|
||||
var privateChatrooms = mapOf<ChatroomKey, Chatroom>()
|
||||
@@ -79,13 +80,21 @@ class User(
|
||||
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||
|
||||
fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent)
|
||||
|
||||
fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
|
||||
|
||||
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
|
||||
|
||||
fun relayHints() = authorRelayList()?.writeRelays()?.take(3) ?: listOfNotNull(latestMetadataRelay)
|
||||
fun outboxRelays() = authorRelayList()?.writeRelaysNorm() ?: listOfNotNull(latestMetadataRelay)
|
||||
|
||||
fun bestRelayHint() = authorRelayList()?.writeRelays()?.firstOrNull() ?: latestMetadataRelay
|
||||
fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay)
|
||||
|
||||
fun inboxRelays() = authorRelayList()?.readRelaysNorm() ?: listOfNotNull(latestMetadataRelay)
|
||||
|
||||
fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays()
|
||||
|
||||
fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay
|
||||
|
||||
fun toPTag() = PTag(pubkeyHex, bestRelayHint())
|
||||
|
||||
@@ -289,12 +298,12 @@ class User(
|
||||
}
|
||||
|
||||
fun addRelayBeingUsed(
|
||||
relay: RelayBriefInfo,
|
||||
relay: NormalizedRelayUrl,
|
||||
eventTime: Long,
|
||||
) {
|
||||
val here = relaysBeingUsed[relay.url]
|
||||
val here = relaysBeingUsed[relay]
|
||||
if (here == null) {
|
||||
relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1))
|
||||
relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1))
|
||||
} else {
|
||||
if (eventTime > here.lastEvent) {
|
||||
here.lastEvent = eventTime
|
||||
@@ -345,8 +354,7 @@ class User(
|
||||
type: ReportType,
|
||||
): Boolean =
|
||||
reports[loggedIn]?.firstOrNull {
|
||||
it.event is ReportEvent &&
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.type == type }
|
||||
(it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false
|
||||
} != null
|
||||
|
||||
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||
@@ -445,7 +453,7 @@ class UserFlowSet(
|
||||
|
||||
@Immutable
|
||||
data class RelayInfo(
|
||||
val url: String,
|
||||
val url: NormalizedRelayUrl,
|
||||
var lastEvent: Long,
|
||||
var counter: Long,
|
||||
)
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.edits
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PrivateStorageRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getPrivateOutboxRelayListAddress() = PrivateOutboxRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
|
||||
|
||||
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
||||
|
||||
fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getPrivateOutboxRelayListFlow()
|
||||
.map { normalizePrivateOutboxRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
onDone: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
val relayListForPrivateOutbox = getPrivateOutboxRelayList()
|
||||
|
||||
if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) {
|
||||
PrivateOutboxRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForPrivateOutbox,
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PrivateOutboxRelayListEvent.createFromScratch(
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupPrivateHomeRelayList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
|
||||
getPrivateOutboxRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? PrivateOutboxRelayListEvent)?.let {
|
||||
settings.updatePrivateHomeRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-21
@@ -20,29 +20,36 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.emphChat
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class EphemeralChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
|
||||
|
||||
@@ -52,31 +59,27 @@ class EphemeralChatListState(
|
||||
|
||||
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
|
||||
|
||||
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
|
||||
return ephemeralChatList(
|
||||
note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set<RoomId> {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateRoomIds(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveEphemeralChatList: StateFlow<Set<RoomId>> by lazy {
|
||||
getEphemeralChatListFlow()
|
||||
.transformLatest { noteState ->
|
||||
val set =
|
||||
tryAndWait { continuation ->
|
||||
(noteState.note.event as? EphemeralChatListEvent)?.publicAndPrivateRoomIds(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (set != null) {
|
||||
emit(set)
|
||||
}
|
||||
emit(ephemeralChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
val set =
|
||||
tryAndWait { continuation ->
|
||||
getEphemeralChatList()?.publicAndPrivateRoomIds(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (set != null) {
|
||||
emit(set)
|
||||
}
|
||||
emit(ephemeralChatListWithBackup(getEphemeralChatListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
@@ -89,6 +92,7 @@ class EphemeralChatListState(
|
||||
channel: EphemeralChatChannel,
|
||||
onDone: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
|
||||
if (ephemeralChatList == null) {
|
||||
@@ -113,6 +117,7 @@ class EphemeralChatListState(
|
||||
channel: EphemeralChatChannel,
|
||||
onDone: (EphemeralChatListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
|
||||
if (ephemeralChatList != null) {
|
||||
@@ -125,4 +130,26 @@ class EphemeralChatListState(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupEphemeralChatList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
|
||||
getEphemeralChatListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")
|
||||
(it.note.event as? EphemeralChatListEvent)?.let {
|
||||
settings.updateEphemeralChatListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.localRelays
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class LocalRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun normalizeLocalRelayListWithBackup(relayList: Set<String>): Set<NormalizedRelayUrl> {
|
||||
return relayList.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
settings.localRelayServers
|
||||
.map { normalizeLocalRelayListWithBackup(it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
normalizeLocalRelayListWithBackup(settings.localRelayServers.value),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
settings.updateLocalRelayServers(relays.map { it.url }.toSet())
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.collections.plus
|
||||
|
||||
class AccountOutboxRelayState(
|
||||
nip65: Nip65RelayListState,
|
||||
privateStorage: PrivateStorageRelayListState,
|
||||
local: LocalRelayListState,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
val flow =
|
||||
combine(
|
||||
nip65.outboxFlow,
|
||||
privateStorage.flow,
|
||||
local.flow,
|
||||
) { nip65Inbox, privateOutBox, localRelays ->
|
||||
nip65Inbox + privateOutBox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
nip65.outboxFlow.value +
|
||||
privateStorage.flow.value +
|
||||
local.flow.value,
|
||||
)
|
||||
}
|
||||
+19
-26
@@ -18,39 +18,32 @@
|
||||
* 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.relays
|
||||
package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import android.app.Application
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
* There should be only one instance of the Tor binding per app.
|
||||
*
|
||||
* Tor will connect as soon as status is listened to.
|
||||
*/
|
||||
class RelayManager(
|
||||
app: Application,
|
||||
class NotificationInboxRelayState(
|
||||
nip65RelayList: Nip65RelayListState,
|
||||
localRelayList: LocalRelayListState,
|
||||
scope: CoroutineScope,
|
||||
torManager: TorManager,
|
||||
connManager: ConnectivityManager,
|
||||
) {
|
||||
val relayService =
|
||||
val flow =
|
||||
combine(
|
||||
torManager.status,
|
||||
connManager.status,
|
||||
) { torStatus, connManager ->
|
||||
}
|
||||
|
||||
val status: StateFlow<RelayServiceStatus> =
|
||||
RelayService(app).status.stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(30000),
|
||||
RelayServiceStatus.Off,
|
||||
)
|
||||
nip65RelayList.inboxFlow,
|
||||
localRelayList.flow,
|
||||
) { nip65Inbox, localRelays ->
|
||||
nip65Inbox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
nip65RelayList.inboxFlow.value + localRelayList.flow.value,
|
||||
)
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserMetadataState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getUserMetadataUser(): User = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getUserMetadataFlow(): StateFlow<UserState> = getUserMetadataUser().flow().metadata.stateFlow
|
||||
|
||||
fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata
|
||||
|
||||
fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
picture: String? = null,
|
||||
banner: String? = null,
|
||||
website: String? = null,
|
||||
pronouns: String? = null,
|
||||
about: String? = null,
|
||||
nip05: String? = null,
|
||||
lnAddress: String? = null,
|
||||
lnURL: String? = null,
|
||||
twitter: String? = null,
|
||||
mastodon: String? = null,
|
||||
github: String? = null,
|
||||
onDone: (MetadataEvent) -> Unit,
|
||||
) {
|
||||
val latest = getUserMetadataEvent()
|
||||
|
||||
val template =
|
||||
if (latest != null) {
|
||||
MetadataEvent.updateFromPast(
|
||||
latest = latest,
|
||||
name = name,
|
||||
displayName = name,
|
||||
picture = picture,
|
||||
banner = banner,
|
||||
website = website,
|
||||
pronouns = pronouns,
|
||||
about = about,
|
||||
nip05 = nip05,
|
||||
lnAddress = lnAddress,
|
||||
lnURL = lnURL,
|
||||
twitter = twitter,
|
||||
mastodon = mastodon,
|
||||
github = github,
|
||||
)
|
||||
} else {
|
||||
MetadataEvent.createNew(
|
||||
name = name,
|
||||
displayName = name,
|
||||
picture = picture,
|
||||
banner = banner,
|
||||
website = website,
|
||||
pronouns = pronouns,
|
||||
about = about,
|
||||
nip05 = nip05,
|
||||
lnAddress = lnAddress,
|
||||
lnURL = lnURL,
|
||||
twitter = twitter,
|
||||
mastodon = mastodon,
|
||||
github = github,
|
||||
)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupUserMetadata?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}")
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
// saves contact list for the next time.
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
|
||||
getUserMetadataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
|
||||
settings.updateUserMetadata(it.user.latestMetadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.toSet
|
||||
|
||||
class FollowListOutboxRelays(
|
||||
kind3Follows: FollowListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
|
||||
|
||||
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
|
||||
|
||||
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
relayListNotes.mapNotNull {
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
|
||||
}
|
||||
}.map {
|
||||
it.flatten().toSet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
kind3Follows.flow.transformLatest { followList ->
|
||||
val flows: List<StateFlow<NoteState>> = allRelayListFlows(followList.authors)
|
||||
val relayListFlows = combineAllFlows(flows)
|
||||
emitAll(relayListFlows)
|
||||
}.onStart {
|
||||
kind3Follows.flow.value.authors.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()
|
||||
}.flatten().toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow.map { relayList ->
|
||||
relayList.map { it.url }.toSet()
|
||||
}.onStart {
|
||||
kind3Follows.flow.value.authors.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet()
|
||||
}.flatten().toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FollowListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getFollowListUser(): User = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getFollowListFlow(): StateFlow<UserState> = getFollowListUser().flow().follows.stateFlow
|
||||
|
||||
fun getFollowListEvent(): ContactListEvent? = getFollowListUser().latestContactList
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val innerFlow: Flow<Kind3Follows> =
|
||||
getFollowListFlow().transformLatest {
|
||||
emit(buildKind3Follows(it.user.latestContactList))
|
||||
}
|
||||
|
||||
val flow =
|
||||
innerFlow
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
buildKind3Follows(getFollowListEvent() ?: settings.backupContactList),
|
||||
)
|
||||
|
||||
/**
|
||||
This contains a big OR of everything the user wants to see in the a single feed.
|
||||
*/
|
||||
@Immutable
|
||||
class Kind3Follows(
|
||||
val authors: Set<String> = emptySet(),
|
||||
val authorsPlusMe: Set<String>,
|
||||
val hashtags: Set<String> = emptySet(),
|
||||
val geotags: Set<String> = emptySet(),
|
||||
val communities: Set<String> = emptySet(),
|
||||
) {
|
||||
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
|
||||
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
|
||||
}
|
||||
|
||||
fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows {
|
||||
// makes sure the output include only valid p tags
|
||||
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
||||
|
||||
return Kind3Follows(
|
||||
authors = verifiedFollowingUsers,
|
||||
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
|
||||
hashtags =
|
||||
latestContactList
|
||||
?.unverifiedFollowTagSet()
|
||||
?.map { it.lowercase() }
|
||||
?.toSet() ?: emptySet(),
|
||||
geotags =
|
||||
latestContactList
|
||||
?.geohashes()
|
||||
?.toSet() ?: emptySet(),
|
||||
communities =
|
||||
latestContactList
|
||||
?.verifiedFollowAddressSet()
|
||||
?.toSet() ?: emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(
|
||||
user: User,
|
||||
onDone: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val contactList = getFollowListEvent()
|
||||
|
||||
if (contactList != null) {
|
||||
ContactListEvent.followUser(contactList, user.pubkeyHex, signer, onReady = onDone)
|
||||
} else {
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)),
|
||||
followTags = emptyList(),
|
||||
followGeohashes = emptyList(),
|
||||
followCommunities = emptyList(),
|
||||
relayUse = emptyMap(),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
user: User,
|
||||
onDone: (ContactListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val contactList = getFollowListEvent()
|
||||
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
ContactListEvent.unfollowUser(
|
||||
contactList,
|
||||
user.pubkeyHex,
|
||||
signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupContactList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}")
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
// saves contact list for the next time.
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
|
||||
getFollowListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
|
||||
settings.updateContactListTo(it.user.latestContactList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.map
|
||||
|
||||
class FollowsPerOutboxRelay(
|
||||
kind3Follows: FollowListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
|
||||
|
||||
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
|
||||
|
||||
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
mapOfSet {
|
||||
relayListNotes.forEach { noteState ->
|
||||
noteState.note.author?.pubkeyHex?.let { authorHex ->
|
||||
(noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay ->
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
kind3Follows.flow.transformLatest { followList ->
|
||||
val flows: List<StateFlow<NoteState>> = allRelayListFlows(followList.authors)
|
||||
val relayListFlows = combineAllFlows(flows)
|
||||
emitAll(relayListFlows)
|
||||
}.onStart {
|
||||
emit(
|
||||
mapOfSet {
|
||||
kind3Follows.flow.value.authors.map { authorHex ->
|
||||
getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay ->
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyMap(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip17Dms
|
||||
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class DmInboxRelayState(
|
||||
// main relay
|
||||
dmRelayList: DmRelayListState,
|
||||
// backup relays
|
||||
nip65RelayList: Nip65RelayListState,
|
||||
privateOutbox: PrivateStorageRelayListState,
|
||||
localRelayList: LocalRelayListState,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
val flow =
|
||||
combine(
|
||||
nip65RelayList.inboxFlow,
|
||||
dmRelayList.flow,
|
||||
privateOutbox.flow,
|
||||
localRelayList.flow,
|
||||
) { nip65Inbox, dmRelayList, privateOutBox, localRelays ->
|
||||
nip65Inbox + dmRelayList + privateOutBox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
nip65RelayList.inboxFlow.value +
|
||||
dmRelayList.flow.value +
|
||||
privateOutbox.flow.value +
|
||||
localRelayList.flow.value,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip17Dms
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DmRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getDMRelayListAddress())
|
||||
|
||||
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
|
||||
|
||||
fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getDMRelayListFlow()
|
||||
.map { normalizeDMRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeDMRelayListWithBackup(getDMRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
dmRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (ChatMessageRelayListEvent) -> Unit,
|
||||
) {
|
||||
val relayListForDMs = getDMRelayList()
|
||||
if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
|
||||
ChatMessageRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForDMs,
|
||||
relays = dmRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
ChatMessageRelayListEvent.createFromScratch(
|
||||
relays = dmRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupDMRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
|
||||
getDMRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? ChatMessageRelayListEvent)?.let {
|
||||
settings.updateDMRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip18Reposts
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
|
||||
class RepostAction {
|
||||
companion object {
|
||||
fun repost(
|
||||
note: Note,
|
||||
signer: NostrSigner,
|
||||
onDone: (Event) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val noteEvent = note.event ?: return
|
||||
|
||||
if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) {
|
||||
// has already bosted in the past 5mins
|
||||
return
|
||||
}
|
||||
|
||||
val noteHint = note.relayHintUrl()
|
||||
val authorHint = note.author?.bestRelayHint()
|
||||
|
||||
val template =
|
||||
if (noteEvent.kind == 1) {
|
||||
RepostEvent.build(noteEvent, noteHint, authorHint)
|
||||
} else {
|
||||
GenericRepostEvent.build(noteEvent, noteHint, authorHint)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip25Reactions
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
|
||||
class ReactionAction {
|
||||
companion object {
|
||||
suspend fun reactTo(
|
||||
note: Note,
|
||||
reaction: String,
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onPublic: (ReactionEvent) -> Unit,
|
||||
onPrivate: (NIP17Factory.Result) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
if (note.hasReacted(by, reaction)) {
|
||||
// has already liked this note
|
||||
return
|
||||
}
|
||||
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is NIP17Group) {
|
||||
val users = noteEvent.groupMembers().toList()
|
||||
|
||||
if (reaction.startsWith(":")) {
|
||||
val emojiUrl = EmojiUrlTag.decode(reaction)
|
||||
if (emojiUrl != null) {
|
||||
note.toEventHint<Event>()?.let {
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
emojiUrl = emojiUrl,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
) {
|
||||
onPrivate(it)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
note.toEventHint<Event>()?.let {
|
||||
NIP17Factory().createReactionWithinGroup(
|
||||
content = reaction,
|
||||
originalNote = it,
|
||||
to = users,
|
||||
signer = signer,
|
||||
) {
|
||||
onPrivate(it)
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
if (reaction.startsWith(":")) {
|
||||
val emojiUrl = EmojiUrlTag.decode(reaction)
|
||||
if (emojiUrl != null) {
|
||||
note.event?.let {
|
||||
val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl()))
|
||||
|
||||
signer.sign(
|
||||
template,
|
||||
onReady = onPublic,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
note.toEventHint<Event>()?.let {
|
||||
signer.sign(
|
||||
ReactionEvent.build(reaction, it),
|
||||
onReady = onPublic,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
-24
@@ -20,18 +20,23 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip28PublicChats
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@@ -39,12 +44,14 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class PublicChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
|
||||
|
||||
@@ -54,31 +61,27 @@ class PublicChatListState(
|
||||
|
||||
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
|
||||
|
||||
suspend fun publicChatListWithBackup(note: Note): Set<EventIdHint> {
|
||||
return publicChatList(
|
||||
note.event as? ChannelListEvent ?: settings.backupChannelList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun publicChatList(event: ChannelListEvent?): Set<EventIdHint> {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateChannels(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val livePublicChatList: StateFlow<Set<EventIdHint>> by lazy {
|
||||
val flow: StateFlow<Set<EventIdHint>> by lazy {
|
||||
getChannelListFlow()
|
||||
.transformLatest { noteState ->
|
||||
val set =
|
||||
tryAndWait { continuation ->
|
||||
(noteState.note.event as? ChannelListEvent)?.publicAndPrivateChannels(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (set != null) {
|
||||
emit(set)
|
||||
}
|
||||
emit(publicChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
val set =
|
||||
tryAndWait { continuation ->
|
||||
getChannelList()?.publicAndPrivateChannels(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (set != null) {
|
||||
emit(set)
|
||||
}
|
||||
emit(publicChatListWithBackup(getChannelListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
@@ -88,8 +91,8 @@ class PublicChatListState(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val livePublicChatEventIdSet: StateFlow<Set<HexKey>> by lazy {
|
||||
livePublicChatList
|
||||
val flowSet: StateFlow<Set<HexKey>> by lazy {
|
||||
flow
|
||||
.map {
|
||||
it.mapTo(mutableSetOf()) { it.eventId }
|
||||
}.flowOn(Dispatchers.Default)
|
||||
@@ -104,6 +107,7 @@ class PublicChatListState(
|
||||
channel: PublicChatChannel,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
val fullHint = channel.toEventHint()
|
||||
@@ -127,6 +131,7 @@ class PublicChatListState(
|
||||
channels: List<PublicChatChannel>,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
val partialHint = channels.map { it.toEventId() }
|
||||
@@ -141,10 +146,33 @@ class PublicChatListState(
|
||||
channel: PublicChatChannel,
|
||||
onDone: (ChannelListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
if (publicChatList != null) {
|
||||
ChannelListEvent.removeChannel(publicChatList, channel.idHex, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupChannelList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Channel List Collector Start")
|
||||
getChannelListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}")
|
||||
(it.note.event as? ChannelListEvent)?.let {
|
||||
settings.updateChannelListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip30CustomEmojis
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||
@@ -67,7 +68,7 @@ class EmojiPackState(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveEmojiSelectionPack: StateFlow<List<StateFlow<NoteState>>?> by lazy {
|
||||
val flow: StateFlow<List<StateFlow<NoteState>>?> by lazy {
|
||||
getEmojiPackSelectionFlow()
|
||||
.transformLatest {
|
||||
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
|
||||
@@ -98,7 +99,7 @@ class EmojiPackState(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val myEmojis by lazy {
|
||||
liveEmojiSelectionPack
|
||||
flow
|
||||
.transformLatest { emojiList ->
|
||||
if (emojiList != null) {
|
||||
emitAll(
|
||||
@@ -116,4 +117,40 @@ class EmojiPackState(
|
||||
mergePack(convertEmojiSelectionPack(getEmojiPackSelection())?.map { it.value }?.toTypedArray() ?: emptyArray()),
|
||||
)
|
||||
}
|
||||
|
||||
fun addEmojiPack(
|
||||
emojiPack: Note,
|
||||
onDone: (EmojiPackSelectionEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
val emojiPackEvent = emojiPack.event
|
||||
if (emojiPackEvent !is EmojiPackEvent) return
|
||||
|
||||
val eventHint = emojiPack.toEventHint<EmojiPackEvent>() ?: return
|
||||
|
||||
val usersEmojiList = getEmojiPackSelection()
|
||||
if (usersEmojiList == null) {
|
||||
val template = EmojiPackSelectionEvent.build(listOf(eventHint))
|
||||
signer.sign(template, onDone)
|
||||
} else {
|
||||
val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint)
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEmojiPack(
|
||||
emojiPack: Note,
|
||||
onDone: (EmojiPackSelectionEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
val usersEmojiList = getEmojiPackSelection() ?: return
|
||||
|
||||
val emojiPackEvent = emojiPack.event
|
||||
if (emojiPackEvent !is EmojiPackEvent) return
|
||||
|
||||
val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent)
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip38UserStatuses
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
|
||||
class UserStatusAction {
|
||||
companion object {
|
||||
fun create(
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
onDone: (StatusEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
StatusEvent.create(newStatus, "general", expiration = null, signer, onReady = onDone)
|
||||
}
|
||||
|
||||
fun update(
|
||||
oldStatus: AddressableNote,
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
onDone: (StatusEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
|
||||
StatusEvent.update(oldEvent, newStatus, signer, onReady = onDone)
|
||||
}
|
||||
|
||||
fun delete(
|
||||
oldStatus: AddressableNote,
|
||||
signer: NostrSigner,
|
||||
onDone: (Event) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
|
||||
StatusEvent.clear(oldEvent, signer) { event ->
|
||||
onDone(event)
|
||||
|
||||
signer.sign(
|
||||
DeletionEvent.buildForVersionOnly(listOf(event)),
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip50Search
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getSearchRelayListAddress() = SearchRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress())
|
||||
|
||||
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
|
||||
|
||||
fun normalizeSearchRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList
|
||||
return event?.relays()?.toSet() ?: DefaultSearchRelayList
|
||||
}
|
||||
|
||||
val flow =
|
||||
getSearchRelayListFlow()
|
||||
.map { normalizeSearchRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeSearchRelayListWithBackup(getSearchRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
searchRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (SearchRelayListEvent) -> Unit,
|
||||
) {
|
||||
val relayListForSearch = getSearchRelayList()
|
||||
|
||||
if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) {
|
||||
SearchRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForSearch,
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
SearchRelayListEvent.createFromScratch(
|
||||
relays = searchRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupSearchRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Search Relay List Collector Start")
|
||||
getSearchRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? SearchRelayListEvent)?.let {
|
||||
settings.updateSearchRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class BlockPeopleListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey)
|
||||
|
||||
fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(getBlockListAddress())
|
||||
|
||||
fun getBlockListFlow(): StateFlow<NoteState> = getBlockListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
|
||||
|
||||
suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords {
|
||||
return blockList(
|
||||
note.event as? PeopleListEvent,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateUsersAndWords(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: PeopleListEvent.UsersAndWords()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getBlockListFlow()
|
||||
.map { blockListWithBackup(it.note) }
|
||||
.onStart { emit(blockListWithBackup(getBlockListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
PeopleListEvent.UsersAndWords(),
|
||||
)
|
||||
|
||||
fun hideUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.addUser(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithUser(
|
||||
name = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.removeUser(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWord(
|
||||
word: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.addWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithWord(
|
||||
name = PeopleListEvent.BLOCK_LIST_D_TAG,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showWord(
|
||||
word: String,
|
||||
onDone: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val blockList = getBlockList()
|
||||
|
||||
if (blockList != null) {
|
||||
PeopleListEvent.removeWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class GeohashListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getGeohashListAddress() = GeohashListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getGeohashListNote(): AddressableNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
|
||||
|
||||
fun getGeohashListFlow(): StateFlow<NoteState> = getGeohashListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent
|
||||
|
||||
suspend fun geohashListWithBackup(note: Note): Set<String> {
|
||||
return geohashList(
|
||||
note.event as? GeohashListEvent ?: settings.backupGeohashList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun geohashList(event: GeohashListEvent?): Set<String> {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateGeohash(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<String>> by lazy {
|
||||
getGeohashListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(geohashListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(geohashListWithBackup(getGeohashListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(
|
||||
geohashs: List<String>,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList == null) {
|
||||
GeohashListEvent.createGeohashs(geohashs, true, signer, onReady = onDone)
|
||||
} else {
|
||||
GeohashListEvent.addGeohashs(geohashList, geohashs, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
geohash: String,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList == null) {
|
||||
GeohashListEvent.createGeohash(geohash, true, signer, onReady = onDone)
|
||||
} else {
|
||||
GeohashListEvent.addGeohash(geohashList, geohash, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
geohash: String,
|
||||
onDone: (GeohashListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val geohashList = getGeohashList()
|
||||
|
||||
if (geohashList != null) {
|
||||
GeohashListEvent.removeGeohash(geohashList, geohash, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupGeohashList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
|
||||
getGeohashListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
|
||||
(it.note.event as? GeohashListEvent)?.let {
|
||||
settings.updateGeohashListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class HashtagListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
|
||||
|
||||
fun getHashtagListFlow(): StateFlow<NoteState> = getHashtagListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent
|
||||
|
||||
suspend fun hashtagListWithBackup(note: Note): Set<String> {
|
||||
return hashtagList(
|
||||
note.event as? HashtagListEvent ?: settings.backupHashtagList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun hashtagList(event: HashtagListEvent?): Set<String> {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateHashtag(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<String>> by lazy {
|
||||
getHashtagListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(hashtagListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(hashtagListWithBackup(getHashtagListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(
|
||||
hashtags: List<String>,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList == null) {
|
||||
HashtagListEvent.createHashtags(hashtags, true, signer, onReady = onDone)
|
||||
} else {
|
||||
HashtagListEvent.addHashtags(hashtagList, hashtags, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
hashtag: String,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList == null) {
|
||||
HashtagListEvent.createHashtag(hashtag, true, signer, onReady = onDone)
|
||||
} else {
|
||||
HashtagListEvent.addHashtag(hashtagList, hashtag, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
hashtag: String,
|
||||
onDone: (HashtagListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val hashtagList = getHashtagList()
|
||||
|
||||
if (hashtagList != null) {
|
||||
HashtagListEvent.removeHashtag(hashtagList, hashtag, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupHashtagList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
|
||||
getHashtagListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")
|
||||
(it.note.event as? HashtagListEvent)?.let {
|
||||
settings.updateHashtagListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class HiddenUsersState(
|
||||
val muteList: StateFlow<PeopleListEvent.UsersAndWords>,
|
||||
val blockList: StateFlow<PeopleListEvent.UsersAndWords>,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
|
||||
|
||||
@Immutable
|
||||
class LiveHiddenUsers(
|
||||
val hiddenUsers: Set<String>,
|
||||
val spammers: Set<String>,
|
||||
val hiddenWords: Set<String>,
|
||||
val showSensitiveContent: Boolean?,
|
||||
) {
|
||||
// speeds up isHidden calculations
|
||||
val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }
|
||||
val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() }
|
||||
val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }
|
||||
|
||||
fun isUserHidden(userHex: HexKey) = hiddenUsers.contains(userHex) || spammers.contains(userHex)
|
||||
}
|
||||
|
||||
suspend fun assembleLiveHiddenUsers(
|
||||
blockList: PeopleListEvent.UsersAndWords,
|
||||
muteList: PeopleListEvent.UsersAndWords,
|
||||
transientHiddenUsers: Set<String>,
|
||||
showSensitiveContent: Boolean?,
|
||||
): LiveHiddenUsers {
|
||||
return LiveHiddenUsers(
|
||||
hiddenUsers = blockList.users + muteList.users,
|
||||
hiddenWords = blockList.words + muteList.words,
|
||||
spammers = transientHiddenUsers,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
)
|
||||
}
|
||||
|
||||
val flow: StateFlow<LiveHiddenUsers> by lazy {
|
||||
combineTransform(
|
||||
blockList,
|
||||
muteList,
|
||||
transientHiddenUsers,
|
||||
settings.syncedSettings.security.showSensitiveContent,
|
||||
) { blockList, muteList, transientHiddenUsers, showSensitiveContent ->
|
||||
checkNotInMainThread()
|
||||
emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
runBlocking {
|
||||
assembleLiveHiddenUsers(
|
||||
blockList.value,
|
||||
muteList.value,
|
||||
transientHiddenUsers.value,
|
||||
settings.syncedSettings.security.showSensitiveContent.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun resetTransientUsers() {
|
||||
transientHiddenUsers.update {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(pubkeyHex: HexKey) {
|
||||
transientHiddenUsers.update { it - pubkeyHex }
|
||||
}
|
||||
|
||||
fun hideUser(pubkeyHex: HexKey) {
|
||||
transientHiddenUsers.update { it + pubkeyHex }
|
||||
}
|
||||
|
||||
fun isHidden(pubkeyHex: HexKey) = pubkeyHex in transientHiddenUsers.value
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class MuteListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getMuteListAddress() = MuteListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getMuteListNote() = cache.getOrCreateAddressableNote(getMuteListAddress())
|
||||
|
||||
fun getMuteListFlow(): StateFlow<NoteState> = getMuteListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
|
||||
|
||||
suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords {
|
||||
return muteList(
|
||||
note.event as? MuteListEvent ?: settings.backupMuteList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateUsersAndWords(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: PeopleListEvent.UsersAndWords()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getMuteListFlow()
|
||||
.map { muteListWithBackup(it.note) }
|
||||
.onStart { emit(muteListWithBackup(getMuteListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
PeopleListEvent.UsersAndWords(),
|
||||
)
|
||||
|
||||
fun hideUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.addUser(
|
||||
earlierVersion = muteList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
MuteListEvent.createListWithUser(
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(
|
||||
pubkeyHex: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.removeUser(
|
||||
earlierVersion = muteList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWord(
|
||||
word: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.addWord(
|
||||
earlierVersion = muteList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
MuteListEvent.createListWithWord(
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showWord(
|
||||
word: String,
|
||||
onDone: (MuteListEvent) -> Unit,
|
||||
) {
|
||||
val muteList = getMuteList()
|
||||
|
||||
if (muteList != null) {
|
||||
MuteListEvent.removeWord(
|
||||
earlierVersion = muteList,
|
||||
word = word,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupMuteList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Mute List Collector Start")
|
||||
getMuteListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")
|
||||
(it.note.event as? MuteListEvent)?.let {
|
||||
settings.updateMuteList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip56Reports
|
||||
|
||||
import android.R.attr.type
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
class ReportAction {
|
||||
companion object {
|
||||
fun report(
|
||||
user: User,
|
||||
type: ReportType,
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onDone: (ReportEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
if (user.hasReport(by, type)) {
|
||||
// has already reported this note
|
||||
return
|
||||
}
|
||||
|
||||
val template = ReportEvent.build(user.pubkeyHex, type)
|
||||
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
|
||||
suspend fun report(
|
||||
note: Note,
|
||||
type: ReportType,
|
||||
content: String = "",
|
||||
by: User,
|
||||
signer: NostrSigner,
|
||||
onDone: (ReportEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
if (note.hasReport(by, type)) {
|
||||
// has already reported this note
|
||||
return
|
||||
}
|
||||
|
||||
note.event?.let {
|
||||
signer.sign(ReportEvent.build(it, type), onDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip65RelayList
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Nip65RelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getNIP65RelayListAddress() = AdvertisedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getNIP65RelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress())
|
||||
|
||||
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
|
||||
|
||||
fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
|
||||
return event?.writeRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
|
||||
}
|
||||
|
||||
fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
|
||||
return event?.readRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
|
||||
}
|
||||
|
||||
fun normalizeNIP65AllRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
|
||||
return event?.relays()?.map { it.relayUrl }?.toSet() ?: Constants.eventFinderRelays
|
||||
}
|
||||
|
||||
val outboxFlow =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65WriteRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val inboxFlow =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65ReadRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val allFlow =
|
||||
getNIP65RelayListFlow()
|
||||
.map { normalizeNIP65AllRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeNIP65AllRelayListWithBackup(getNIP65RelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
relays: List<AdvertisedRelayInfo>,
|
||||
onDone: (AdvertisedRelayListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
|
||||
val nip65RelayList = getNIP65RelayList()
|
||||
|
||||
if (nip65RelayList != null) {
|
||||
AdvertisedRelayListEvent.replaceRelayListWith(
|
||||
earlierVersion = nip65RelayList,
|
||||
newRelays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
AdvertisedRelayListEvent.createFromScratch(
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupNIP65RelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start")
|
||||
getNIP65RelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${signer.pubKey}")
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.let {
|
||||
settings.updateNIP65RelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip65RelayList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class OutboxRelaySetState(
|
||||
usersToLoad: MutableStateFlow<Set<HexKey>>,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.Companion.createAddress(pubkey)
|
||||
|
||||
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
|
||||
|
||||
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
|
||||
|
||||
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
relayListNotes.mapNotNull {
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
|
||||
}
|
||||
}.map {
|
||||
it.flatten().toSet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
usersToLoad.transformLatest { followList ->
|
||||
val flows: List<StateFlow<NoteState>> = allRelayListFlows(followList)
|
||||
val relayListFlows = combineAllFlows(flows)
|
||||
emitAll(relayListFlows)
|
||||
}.onStart {
|
||||
emit(
|
||||
usersToLoad.value.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()
|
||||
}.flatten().toSet(),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow.map { relayList ->
|
||||
relayList.map { it.url }.toSet()
|
||||
}.onStart {
|
||||
emit(
|
||||
usersToLoad.value.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet()
|
||||
}.flatten().toSet(),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip72Communities
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CommunityListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getCommunityListAddress() = CommunityListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getCommunityListNote(): AddressableNote = cache.getOrCreateAddressableNote(getCommunityListAddress())
|
||||
|
||||
fun getCommunityListFlow(): StateFlow<NoteState> = getCommunityListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent
|
||||
|
||||
suspend fun communityListWithBackup(note: Note): Set<AddressHint> {
|
||||
return communityList(
|
||||
note.event as? CommunityListEvent ?: settings.backupCommunityList,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun communityList(event: CommunityListEvent?): Set<AddressHint> {
|
||||
return tryAndWait { continuation ->
|
||||
event?.publicAndPrivateCommunities(signer) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<AddressHint>> by lazy {
|
||||
getCommunityListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(communityListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(communityListWithBackup(getCommunityListNote()))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<String>> by lazy {
|
||||
flow
|
||||
.map {
|
||||
it.mapTo(mutableSetOf()) { it.addressId }
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(
|
||||
communities: List<AddressableNote>,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val communityList = getCommunityList()
|
||||
|
||||
val partialHint = communities.mapNotNull { it.toEventHint<CommunityDefinitionEvent>() }
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunities(partialHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
CommunityListEvent.addCommunities(communityList, partialHint, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
fun follow(
|
||||
community: AddressableNote,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val communityList = getCommunityList()
|
||||
|
||||
val fullHint = community.toEventHint<CommunityDefinitionEvent>()
|
||||
if (fullHint != null) {
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunity(fullHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
CommunityListEvent.addCommunity(communityList, fullHint, true, signer, onReady = onDone)
|
||||
}
|
||||
} else {
|
||||
val partialHint = community.toATag()
|
||||
if (communityList == null) {
|
||||
CommunityListEvent.createCommunity(partialHint, true, signer, onReady = onDone)
|
||||
} else {
|
||||
CommunityListEvent.addCommunity(communityList, partialHint, true, signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(
|
||||
community: AddressableNote,
|
||||
onDone: (CommunityListEvent) -> Unit,
|
||||
) {
|
||||
if (!signer.isWriteable()) return
|
||||
val communityList = getCommunityList()
|
||||
|
||||
if (communityList != null) {
|
||||
CommunityListEvent.removeCommunity(communityList, community.address.toValue(), signer, onReady = onDone)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupCommunityList?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
event.privateTags(signer) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Community List Collector Start")
|
||||
getCommunityListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}")
|
||||
(it.note.event as? CommunityListEvent)?.let {
|
||||
settings.updateCommunityListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip78AppSpecific
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class AppSpecificState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
companion object {
|
||||
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
|
||||
}
|
||||
|
||||
fun getAppSpecificDataAddress() = AppSpecificDataEvent.createAddress(signer.pubKey, APP_SPECIFIC_DATA_D_TAG)
|
||||
|
||||
fun getAppSpecificDataNote() = cache.getOrCreateAddressableNote(getAppSpecificDataAddress())
|
||||
|
||||
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
|
||||
|
||||
fun saveNewAppSpecificData(onDone: (AppSpecificDataEvent) -> Unit) {
|
||||
val toInternal = settings.syncedSettings.toInternal()
|
||||
signer.nip44Encrypt(EventMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted ->
|
||||
AppSpecificDataEvent.create(
|
||||
dTag = APP_SPECIFIC_DATA_D_TAG,
|
||||
description = encrypted,
|
||||
otherTags = emptyArray(),
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupAppSpecificData?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
LocalCache.justConsumeMyOwnEvent(event)
|
||||
signer.decrypt(event.content, event.pubKey) { decrypted ->
|
||||
try {
|
||||
val syncedSettings = EventMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.syncedSettings.updateFrom(syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
|
||||
getAppSpecificDataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}")
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
signer.decrypt(it.content, it.pubKey) { decrypted ->
|
||||
val syncedSettings =
|
||||
try {
|
||||
EventMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e)
|
||||
e.printStackTrace()
|
||||
AccountSyncedSettingsInternal()
|
||||
}
|
||||
|
||||
settings.updateAppSpecificData(it, syncedSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip96FileStorage
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class FileStorageServerListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getFileServersAddress() = FileServersEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getFileServersAddress())
|
||||
|
||||
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
|
||||
|
||||
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
|
||||
|
||||
fun normalizeServers(note: Note): List<String> {
|
||||
val event = note.event as? FileServersEvent
|
||||
return event?.servers() ?: emptyList()
|
||||
}
|
||||
|
||||
val fileServers =
|
||||
getFileServersListFlow()
|
||||
.map { normalizeServers(it.note) }
|
||||
.onStart { emit(normalizeServers(getFileServersNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun saveFileServersList(
|
||||
servers: List<String>,
|
||||
onDone: (FileServersEvent) -> Unit,
|
||||
) {
|
||||
val serverList = getFileServersList()
|
||||
|
||||
val template =
|
||||
if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||
FileServersEvent.replaceServers(serverList, servers)
|
||||
} else {
|
||||
FileServersEvent.build(servers)
|
||||
}
|
||||
|
||||
signer.sign(template, onDone)
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nipB7Blossom
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class BlossomServerListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlossomServersAddress())
|
||||
|
||||
fun getBlossomServersListFlow(): StateFlow<NoteState> = getBlossomServersNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent
|
||||
|
||||
fun normalizeServers(note: Note): List<String> {
|
||||
val event = note.event as? FileServersEvent
|
||||
return event?.servers() ?: emptyList()
|
||||
}
|
||||
|
||||
val fileServers =
|
||||
getBlossomServersListFlow()
|
||||
.map { normalizeServers(it.note) }
|
||||
.onStart { emit(normalizeServers(getBlossomServersNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun saveBlossomServersList(
|
||||
servers: List<String>,
|
||||
onDone: (BlossomServersEvent) -> Unit,
|
||||
) {
|
||||
val serverList = getBlossomServersList()
|
||||
|
||||
if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||
BlossomServersEvent.updateRelayList(
|
||||
earlierVersion = serverList,
|
||||
relays = servers,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
BlossomServersEvent.createFromScratch(
|
||||
relays = servers,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
@@ -51,10 +52,8 @@ class LatestByKindAndAuthor<T : Event>(
|
||||
if ((kind in 10000..19999) || (kind in 30000..39999)) {
|
||||
LocalCache.addressables
|
||||
.maxOrNullOf(
|
||||
filter = { idHex: String, note: AddressableNote ->
|
||||
note.event?.let {
|
||||
it.kind == kind && it.pubKey == pubkey
|
||||
} == true
|
||||
filter = { address: Address, note: AddressableNote ->
|
||||
address.kind == kind && address.pubKeyHex == pubkey
|
||||
},
|
||||
comparator = CreatedAtComparatorAddresses,
|
||||
)?.event as? T
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class MergedFollowListsState(
|
||||
val kind3List: FollowListState,
|
||||
val hashtagList: HashtagListState,
|
||||
val geohashList: GeohashListState,
|
||||
val communityList: CommunityListState,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun mergeLists(
|
||||
kind3: FollowListState.Kind3Follows,
|
||||
hashtages: Set<String>,
|
||||
geohashes: Set<String>,
|
||||
community: Set<AddressHint>,
|
||||
): FollowListState.Kind3Follows {
|
||||
return FollowListState.Kind3Follows(
|
||||
kind3.authors,
|
||||
kind3.authorsPlusMe,
|
||||
kind3.hashtags + hashtages,
|
||||
kind3.geotags + geohashes,
|
||||
kind3.communities + community.map { it.addressId },
|
||||
)
|
||||
}
|
||||
|
||||
val flow: StateFlow<FollowListState.Kind3Follows> =
|
||||
combine(
|
||||
kind3List.flow,
|
||||
hashtagList.flow,
|
||||
geohashList.flow,
|
||||
communityList.flow,
|
||||
) { kind3, hashtag, geohash, community ->
|
||||
mergeLists(kind3, hashtag, geohash, community)
|
||||
}
|
||||
.onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
kind3List.flow.value,
|
||||
hashtagList.flow.value,
|
||||
geohashList.flow.value,
|
||||
communityList.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
kind3List.flow.value,
|
||||
)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class MergedFollowPlusMineRelayListsState(
|
||||
val followsOutboxRelayList: FollowListOutboxRelays,
|
||||
val nip65RelayList: Nip65RelayListState,
|
||||
val privateOutboxRelayList: PrivateStorageRelayListState,
|
||||
val localRelayList: LocalRelayListState,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun mergeLists(
|
||||
kind3: Set<NormalizedRelayUrl>,
|
||||
outbox: Set<NormalizedRelayUrl>,
|
||||
inbox: Set<NormalizedRelayUrl>,
|
||||
private: Set<NormalizedRelayUrl>,
|
||||
local: Set<NormalizedRelayUrl>,
|
||||
): Set<NormalizedRelayUrl> {
|
||||
return kind3 + outbox + inbox + private + local
|
||||
}
|
||||
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
combine(
|
||||
followsOutboxRelayList.flow,
|
||||
nip65RelayList.outboxFlow,
|
||||
nip65RelayList.inboxFlow,
|
||||
privateOutboxRelayList.flow,
|
||||
localRelayList.flow,
|
||||
::mergeLists,
|
||||
)
|
||||
.onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
followsOutboxRelayList.flow.value,
|
||||
nip65RelayList.outboxFlow.value,
|
||||
nip65RelayList.inboxFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
mergeLists(
|
||||
followsOutboxRelayList.flow.value,
|
||||
nip65RelayList.outboxFlow.value,
|
||||
nip65RelayList.inboxFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import android.R.attr.host
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.czeal.rfc3986.URIReference
|
||||
|
||||
class MergedServerListState(
|
||||
val fileServers: StateFlow<List<String>>,
|
||||
val blossomServers: StateFlow<List<String>>,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun host(url: String): String =
|
||||
try {
|
||||
URIReference.parse(url).host.value
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
|
||||
fun mergeServerList(
|
||||
nip96: List<String>?,
|
||||
blossom: List<String>?,
|
||||
): List<ServerName> {
|
||||
val nip96servers = nip96?.map { ServerName(host(it), it, ServerType.NIP96) } ?: emptyList()
|
||||
val blossomServers = blossom?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList()
|
||||
|
||||
val result = (nip96servers + blossomServers).ifEmpty { DEFAULT_MEDIA_SERVERS }
|
||||
|
||||
return result + ServerName("NIP95", "", ServerType.NIP95)
|
||||
}
|
||||
|
||||
val liveServerList: StateFlow<List<ServerName>> by lazy {
|
||||
combine(fileServers, blossomServers) { nip96s, blossoms ->
|
||||
mergeServerList(nip96s, blossoms)
|
||||
}
|
||||
.onStart { emit(mergeServerList(fileServers.value, blossomServers.value)) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class TrustedRelayListsState(
|
||||
val nip65RelayList: Nip65RelayListState,
|
||||
val privateOutboxRelayList: PrivateStorageRelayListState,
|
||||
val localRelayList: LocalRelayListState,
|
||||
val dmRelayList: DmRelayListState,
|
||||
val searchRelayListState: SearchRelayListState,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun mergeLists(
|
||||
nip65: Set<NormalizedRelayUrl>,
|
||||
private: Set<NormalizedRelayUrl>,
|
||||
local: Set<NormalizedRelayUrl>,
|
||||
dm: Set<NormalizedRelayUrl>,
|
||||
search: Set<NormalizedRelayUrl>,
|
||||
): Set<NormalizedRelayUrl> {
|
||||
return nip65 + private + local + dm + search
|
||||
}
|
||||
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
combine(
|
||||
nip65RelayList.allFlow,
|
||||
privateOutboxRelayList.flow,
|
||||
localRelayList.flow,
|
||||
dmRelayList.flow,
|
||||
searchRelayListState.flow,
|
||||
::mergeLists,
|
||||
)
|
||||
.onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
nip65RelayList.allFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
dmRelayList.flow.value,
|
||||
searchRelayListState.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
mergeLists(
|
||||
nip65RelayList.allFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
dmRelayList.flow.value,
|
||||
searchRelayListState.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.ifEmpty
|
||||
|
||||
class CommunityRelayLoader {
|
||||
companion object {
|
||||
fun communitiesPerRelay(
|
||||
communityNotes: Array<NoteState>,
|
||||
cache: LocalCache,
|
||||
): Map<NormalizedRelayUrl, Set<HexKey>> {
|
||||
return mapOfSet {
|
||||
communityNotes.forEach { communityNote ->
|
||||
val relays =
|
||||
(communityNote.note.event as? CommunityDefinitionEvent)?.relayUrls()
|
||||
?.ifEmpty { null }
|
||||
?: cache.relayHints.hintsForAddress(communityNote.note.idHex)
|
||||
|
||||
relays.forEach {
|
||||
add(it, communityNote.note.idHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> communitiesPerRelaySnapshot(
|
||||
communities: Set<HexKey>,
|
||||
cache: LocalCache,
|
||||
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||
): T {
|
||||
val noteMetadata =
|
||||
communities.mapNotNull { addressId ->
|
||||
cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow?.value
|
||||
}.toTypedArray()
|
||||
return transformation(communitiesPerRelay(noteMetadata, cache))
|
||||
}
|
||||
|
||||
fun <T> toCommunitiesPerRelayFlow(
|
||||
communities: Set<HexKey>,
|
||||
cache: LocalCache,
|
||||
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||
): Flow<T> {
|
||||
val noteMetadataFlows =
|
||||
communities.mapNotNull { addressId ->
|
||||
cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow
|
||||
}
|
||||
|
||||
return combine(noteMetadataFlows) { communityNotes ->
|
||||
transformation(communitiesPerRelay(communityNotes, cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownFeedFlow
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class FeedTopNavFilterState(
|
||||
val feedFilterListName: MutableStateFlow<String>,
|
||||
val allFollows: StateFlow<FollowListState.Kind3Follows>,
|
||||
val locationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val signer: NostrSigner,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun loadFlowsFor(listName: String): IFeedFlowsType =
|
||||
when (listName) {
|
||||
GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays)
|
||||
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays)
|
||||
AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays)
|
||||
else -> {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
if (note != null) {
|
||||
NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays)
|
||||
} else {
|
||||
UnknownFeedFlow(listName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<IFeedTopNavFilter> =
|
||||
feedFilterListName.transformLatest { listName ->
|
||||
emitAll(loadFlowsFor(listName).flow())
|
||||
}
|
||||
.onStart {
|
||||
loadFlowsFor(feedFilterListName.value).startValue(this)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
AuthorsByOutboxTopNavFilter(emptySet()),
|
||||
)
|
||||
}
|
||||
+6
-9
@@ -18,16 +18,13 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relays
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
|
||||
sealed class RelayServiceStatus {
|
||||
data class Active(
|
||||
val client: NostrClient,
|
||||
) : RelayServiceStatus()
|
||||
interface IFeedFlowsType {
|
||||
fun flow(): Flow<IFeedTopNavFilter>
|
||||
|
||||
object Off : RelayServiceStatus()
|
||||
|
||||
object Connecting : RelayServiceStatus()
|
||||
suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>)
|
||||
}
|
||||
+9
-13
@@ -18,23 +18,19 @@
|
||||
* 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.filters
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface IPerRelayFilter {
|
||||
fun toRelay(forRelay: String): Filter
|
||||
interface IFeedTopNavFilter {
|
||||
fun matchAuthor(pubkey: HexKey): Boolean
|
||||
|
||||
fun toJson(forRelay: String): String
|
||||
fun match(noteEvent: Event): Boolean
|
||||
|
||||
fun match(
|
||||
event: Event,
|
||||
forRelay: String,
|
||||
): Boolean
|
||||
fun toPerRelayFlow(cache: LocalCache): Flow<IFeedTopNavPerRelayFilterSet>
|
||||
|
||||
fun toDebugJson(): String
|
||||
|
||||
// This only exists because some relays confuse empty lists with null lists
|
||||
fun isValidFor(url: String): Boolean
|
||||
fun startValue(cache: LocalCache): IFeedTopNavPerRelayFilterSet
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
interface IFeedTopNavPerRelayFilter
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
interface IFeedTopNavPerRelayFilterSet
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class MergedTopFeedAuthorListsState(
|
||||
val homeNavFilter: StateFlow<IFeedTopNavPerRelayFilterSet>,
|
||||
val videoNavFilter: StateFlow<IFeedTopNavPerRelayFilterSet>,
|
||||
val discoveryNavFilter: StateFlow<IFeedTopNavPerRelayFilterSet>,
|
||||
val notificationNavFilter: StateFlow<IFeedTopNavPerRelayFilterSet>,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun authorList(navFilter: IFeedTopNavPerRelayFilterSet): Map<NormalizedRelayUrl, Set<HexKey>?> {
|
||||
return when (navFilter) {
|
||||
is AllCommunitiesTopNavPerRelayFilterSet -> emptyMap()
|
||||
is AllFollowsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors }
|
||||
is AuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors }
|
||||
is GlobalTopNavPerRelayFilterSet -> emptyMap()
|
||||
is HashtagTopNavPerRelayFilterSet -> emptyMap()
|
||||
is LocationTopNavPerRelayFilterSet -> emptyMap()
|
||||
is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors }
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors }
|
||||
else -> emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun mergeLists(
|
||||
homeNavFilter: IFeedTopNavPerRelayFilterSet,
|
||||
videoNavFilter: IFeedTopNavPerRelayFilterSet,
|
||||
discoveryNavFilter: IFeedTopNavPerRelayFilterSet,
|
||||
notificationNavFilter: IFeedTopNavPerRelayFilterSet,
|
||||
): Map<NormalizedRelayUrl, Set<HexKey>> {
|
||||
return mapOfSet {
|
||||
authorList(homeNavFilter).forEach { (relay, authors) ->
|
||||
authors?.let { add(relay, authors) }
|
||||
}
|
||||
|
||||
authorList(videoNavFilter).forEach { (relay, authors) ->
|
||||
authors?.let { add(relay, authors) }
|
||||
}
|
||||
|
||||
authorList(discoveryNavFilter).forEach { (relay, authors) ->
|
||||
authors?.let { add(relay, authors) }
|
||||
}
|
||||
|
||||
authorList(notificationNavFilter).forEach { (relay, authors) ->
|
||||
authors?.let { add(relay, authors) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
combine(
|
||||
homeNavFilter,
|
||||
videoNavFilter,
|
||||
discoveryNavFilter,
|
||||
notificationNavFilter,
|
||||
::mergeLists,
|
||||
).onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
homeNavFilter.value,
|
||||
videoNavFilter.value,
|
||||
discoveryNavFilter.value,
|
||||
notificationNavFilter.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyMap(),
|
||||
)
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownTopNavPerRelayFilterSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class OutboxLoaderState(
|
||||
topNavFilter: StateFlow<IFeedTopNavFilter>,
|
||||
cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<IFeedTopNavPerRelayFilterSet> =
|
||||
topNavFilter.transformLatest { filterSettings ->
|
||||
emitAll(filterSettings.toPerRelayFlow(cache))
|
||||
}.onStart {
|
||||
emit(topNavFilter.value.startValue(cache))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Companion.Eagerly,
|
||||
UnknownTopNavPerRelayFilterSet,
|
||||
)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlin.collections.ifEmpty
|
||||
|
||||
class OutboxRelayLoader {
|
||||
companion object {
|
||||
private fun authorsPerRelay(
|
||||
outboxRelayNotes: Array<NoteState>,
|
||||
cache: LocalCache,
|
||||
): Map<NormalizedRelayUrl, Set<HexKey>> {
|
||||
return mapOfSet {
|
||||
outboxRelayNotes.forEach { outboxNote ->
|
||||
val relays =
|
||||
(outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null }
|
||||
?: outboxNote.note.author?.pubkeyHex ?.let { cache.relayHints.hintsForKey(it) }
|
||||
|
||||
relays?.forEach {
|
||||
add(it, outboxNote.note.idHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> authorsPerRelaySnapshot(
|
||||
authors: Set<HexKey>,
|
||||
cache: LocalCache,
|
||||
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||
): T {
|
||||
val noteMetadata =
|
||||
authors.map { pubkeyHex ->
|
||||
cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)).flow().metadata.stateFlow.value
|
||||
}.toTypedArray()
|
||||
return transformation(authorsPerRelay(noteMetadata, cache))
|
||||
}
|
||||
|
||||
fun <T> toAuthorsPerRelayFlow(
|
||||
authors: Set<HexKey>,
|
||||
cache: LocalCache,
|
||||
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||
): Flow<T> {
|
||||
val noteMetadataFlows =
|
||||
authors.map { pubkeyHex ->
|
||||
val note = cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
|
||||
note.flow().metadata.stateFlow
|
||||
}
|
||||
|
||||
return combine(noteMetadataFlows) { outboxRelays ->
|
||||
transformation(authorsPerRelay(outboxRelays, cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
/**
|
||||
* This is a big OR filter on all fields.
|
||||
*/
|
||||
@Immutable
|
||||
class AllFollowsByOutboxTopNavFilter(
|
||||
val authors: Set<String>? = null,
|
||||
val hashtags: Set<String>? = null,
|
||||
val geotags: Set<String>? = null,
|
||||
val communities: Set<String>? = null,
|
||||
val defaultRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.Companion.toScope(it) }
|
||||
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.Companion.toScope(it) }
|
||||
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean {
|
||||
return authors == null || pubkey in authors
|
||||
}
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return if (noteEvent is LiveActivitiesEvent) {
|
||||
(authors != null && noteEvent.participantsIntersect(authors)) ||
|
||||
(hashtags != null && noteEvent.isTaggedHashes(hashtags)) ||
|
||||
(geotags != null && noteEvent.isTaggedGeoHashes(geotags)) ||
|
||||
(communities != null && noteEvent.isTaggedAddressableNotes(communities))
|
||||
} else if (noteEvent is CommentEvent) {
|
||||
// ignore follows and checks only the root scope
|
||||
(authors != null && noteEvent.pubKey in authors) ||
|
||||
(hashtags != null && noteEvent.isTaggedHashes(hashtags)) ||
|
||||
(hashtagScopes != null && noteEvent.isTaggedScopes(hashtagScopes)) ||
|
||||
(geotags != null && noteEvent.isTaggedGeoHashes(geotags)) ||
|
||||
(geotagScopes != null && noteEvent.isTaggedScopes(geotagScopes)) ||
|
||||
(communities != null && noteEvent.isTaggedAddressableNotes(communities))
|
||||
} else {
|
||||
(authors != null && noteEvent.pubKey in authors) ||
|
||||
(hashtags != null && noteEvent.isTaggedHashes(hashtags)) ||
|
||||
(geotags != null && noteEvent.isTaggedGeoHashes(geotags)) ||
|
||||
(communities != null && noteEvent.isTaggedAddressableNotes(communities))
|
||||
}
|
||||
}
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AllFollowsByOutboxTopNavPerRelayFilterSet> {
|
||||
val authorsPerRelay =
|
||||
if (authors != null) {
|
||||
OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it }
|
||||
} else {
|
||||
MutableStateFlow(emptyMap())
|
||||
}
|
||||
val communitiesPerRelay =
|
||||
if (communities != null) {
|
||||
CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it }
|
||||
} else {
|
||||
MutableStateFlow(emptyMap())
|
||||
}
|
||||
|
||||
return combine(authorsPerRelay, communitiesPerRelay, defaultRelays) { perRelayAuthors, perRelayCommunities, default ->
|
||||
val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).ifEmpty { default }
|
||||
|
||||
AllFollowsByOutboxTopNavPerRelayFilterSet(
|
||||
allRelays.associateWith {
|
||||
AllFollowsByOutboxTopNavPerRelayFilter(
|
||||
authors = perRelayAuthors[it],
|
||||
hashtags = hashtags,
|
||||
geotags = geotags,
|
||||
communities = perRelayCommunities[it],
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): AllFollowsByOutboxTopNavPerRelayFilterSet {
|
||||
val authorsPerRelay =
|
||||
if (authors != null) {
|
||||
OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
val communitiesPerRelay =
|
||||
if (communities != null) {
|
||||
CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).ifEmpty { defaultRelays.value }
|
||||
|
||||
return AllFollowsByOutboxTopNavPerRelayFilterSet(
|
||||
allRelays.associateWith {
|
||||
AllFollowsByOutboxTopNavPerRelayFilter(
|
||||
authors = authorsPerRelay[it],
|
||||
hashtags = hashtags,
|
||||
geotags = geotags,
|
||||
communities = communitiesPerRelay[it],
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
-13
@@ -18,21 +18,23 @@
|
||||
* 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.relays.kind3
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
|
||||
/**
|
||||
* This is a big OR filter.
|
||||
*/
|
||||
@Immutable
|
||||
data class Kind3BasicRelaySetupInfo(
|
||||
val url: String,
|
||||
val read: Boolean,
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
val relayStat: RelayStat,
|
||||
val paidRelay: Boolean = false,
|
||||
) {
|
||||
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
|
||||
class AllFollowsByOutboxTopNavPerRelayFilter(
|
||||
val authors: Set<String>? = null,
|
||||
val hashtags: Set<String>? = null,
|
||||
val geotags: Set<String>? = null,
|
||||
val communities: Set<String>? = null,
|
||||
) : IFeedTopNavPerRelayFilter {
|
||||
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
|
||||
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AllFollowsByOutboxTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AllFollowsByOutboxTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AllFollowsFeedFlow(
|
||||
val allFollows: StateFlow<FollowListState.Kind3Follows?>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter {
|
||||
return if (kind3 != null) {
|
||||
AllFollowsByOutboxTopNavFilter(
|
||||
authors = kind3.authors,
|
||||
hashtags = kind3.hashtags,
|
||||
geotags = kind3.geotags,
|
||||
communities = kind3.communities,
|
||||
defaultRelays = followsRelays,
|
||||
)
|
||||
} else {
|
||||
AllFollowsByOutboxTopNavFilter(
|
||||
authors = emptySet(),
|
||||
defaultRelays = followsRelays,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flow() = allFollows.map(::convert)
|
||||
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
collector.emit(convert(allFollows.value))
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import com.fonfon.kgeohash.GeoHash
|
||||
|
||||
fun compute50kmLine(geoHash: GeoHash): List<String> {
|
||||
val hashes = mutableListOf<String>()
|
||||
|
||||
hashes.add(geoHash.toString())
|
||||
|
||||
var currentGeoHash = geoHash
|
||||
repeat(5) {
|
||||
currentGeoHash = currentGeoHash.westernNeighbour
|
||||
hashes.add(currentGeoHash.toString())
|
||||
}
|
||||
|
||||
currentGeoHash = geoHash
|
||||
repeat(5) {
|
||||
currentGeoHash = currentGeoHash.easternNeighbour
|
||||
hashes.add(currentGeoHash.toString())
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
|
||||
fun compute50kmRange(geoHash: GeoHash): List<String> {
|
||||
val hashes = mutableListOf<String>()
|
||||
|
||||
hashes.addAll(compute50kmLine(geoHash))
|
||||
|
||||
var currentGeoHash = geoHash
|
||||
repeat(5) {
|
||||
currentGeoHash = currentGeoHash.northernNeighbour
|
||||
hashes.addAll(compute50kmLine(currentGeoHash))
|
||||
}
|
||||
|
||||
currentGeoHash = geoHash
|
||||
repeat(5) {
|
||||
currentGeoHash = currentGeoHash.southernNeighbour
|
||||
hashes.addAll(compute50kmLine(currentGeoHash))
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AroundMeFeedFlow(
|
||||
val location: StateFlow<LocationState.LocationResult>,
|
||||
val allFollowRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
fun convert(result: LocationState.LocationResult): LocationTopNavFilter {
|
||||
return if (result is LocationState.LocationResult.Success) {
|
||||
// 2 neighbors deep = 25x25km
|
||||
LocationTopNavFilter(
|
||||
geotags = compute50kmRange(result.geoHash).toSet(),
|
||||
relays = allFollowRelays,
|
||||
)
|
||||
} else {
|
||||
// empty feed until we have a successful geohash
|
||||
LocationTopNavFilter(
|
||||
geotags = emptySet(),
|
||||
relays = allFollowRelays,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flow() = location.map(::convert)
|
||||
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
collector.emit(convert(location.value))
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Immutable
|
||||
class LocationTopNavFilter(
|
||||
val geotags: Set<String>,
|
||||
val relays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.Companion.toScope(it) }
|
||||
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean = true
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
if (geotags.isEmpty()) return false
|
||||
|
||||
return if (noteEvent is CommentEvent) {
|
||||
noteEvent.isTaggedGeoHashes(geotags) ||
|
||||
noteEvent.isTaggedScopes(geotagScopes)
|
||||
} else {
|
||||
noteEvent.isTaggedGeoHashes(geotags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<LocationTopNavPerRelayFilterSet> {
|
||||
return relays.map {
|
||||
LocationTopNavPerRelayFilterSet(it.associateWith { LocationTopNavPerRelayFilter(geotags) })
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet {
|
||||
return LocationTopNavPerRelayFilterSet(relays.value.associateWith { LocationTopNavPerRelayFilter(geotags) })
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
|
||||
@Immutable
|
||||
class LocationTopNavPerRelayFilter(
|
||||
val geotags: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter {
|
||||
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class LocationTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, LocationTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+14
-20
@@ -18,29 +18,23 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.global
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
object RelayBriefInfoCache {
|
||||
val cache = LruCache<String, RelayBriefInfo?>(50)
|
||||
class GlobalFeedFlow(
|
||||
val relays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
val default = GlobalTopNavFilter(relays)
|
||||
|
||||
@Immutable
|
||||
class RelayBriefInfo(
|
||||
val url: String,
|
||||
) {
|
||||
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern()
|
||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
|
||||
}
|
||||
override fun flow() = MutableStateFlow(default)
|
||||
|
||||
fun get(url: String): RelayBriefInfo {
|
||||
val info = cache[url]
|
||||
if (info != null) return info
|
||||
|
||||
val newInfo = RelayBriefInfo(url)
|
||||
cache.put(url, newInfo)
|
||||
return newInfo
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
collector.emit(default)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.global
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Immutable
|
||||
class GlobalTopNavFilter(
|
||||
val relays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean = true
|
||||
|
||||
override fun match(noteEvent: Event) = true
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<GlobalTopNavPerRelayFilterSet> {
|
||||
return relays.map {
|
||||
GlobalTopNavPerRelayFilterSet(it.associateWith { GlobalTopNavPerRelayFilter })
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): GlobalTopNavPerRelayFilterSet {
|
||||
return GlobalTopNavPerRelayFilterSet(
|
||||
relays.value.associateWith { GlobalTopNavPerRelayFilter },
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -18,14 +18,10 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.ammolite.relays
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.global
|
||||
|
||||
import com.vitorpamplona.ammolite.relays.filters.IPerRelayFilter
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
|
||||
class TypedFilter(
|
||||
val types: Set<FeedType>,
|
||||
val filter: IPerRelayFilter,
|
||||
) {
|
||||
// This only exists because some relays confuse empty lists with null lists
|
||||
fun isValidFor(url: String) = filter.isValidFor(url)
|
||||
}
|
||||
@Immutable
|
||||
object GlobalTopNavPerRelayFilter : IFeedTopNavPerRelayFilter
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.global
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class GlobalTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, GlobalTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Immutable
|
||||
class HashtagTopNavFilter(
|
||||
val hashtags: Set<String>,
|
||||
val relays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
|
||||
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean = true
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return if (noteEvent is CommentEvent) {
|
||||
noteEvent.isTaggedHashes(hashtags) || noteEvent.isTaggedScopes(hashtagScopes)
|
||||
} else {
|
||||
noteEvent.isTaggedHashes(hashtags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<HashtagTopNavPerRelayFilterSet> {
|
||||
return relays.map {
|
||||
HashtagTopNavPerRelayFilterSet(
|
||||
it.associateWith { HashtagTopNavPerRelayFilter(hashtags) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): HashtagTopNavPerRelayFilterSet {
|
||||
return HashtagTopNavPerRelayFilterSet(
|
||||
relays.value.associateWith {
|
||||
HashtagTopNavPerRelayFilter(hashtags)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
-16
@@ -18,23 +18,15 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
|
||||
@Immutable
|
||||
data class RelaySetupInfo(
|
||||
val url: String,
|
||||
val read: Boolean,
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class RelaySetupInfoToConnect(
|
||||
val url: String,
|
||||
val forceProxy: Boolean,
|
||||
val read: Boolean,
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
)
|
||||
class HashtagTopNavPerRelayFilter(
|
||||
val hashtags: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter {
|
||||
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class HashtagTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, HashtagTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased
|
||||
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
class NoteFeedFlow(
|
||||
val metadataFlow: StateFlow<NoteState?>,
|
||||
val signer: NostrSigner,
|
||||
val allFollowRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
suspend fun FlowCollector<IFeedTopNavFilter>.process(noteEvent: Event) {
|
||||
when (noteEvent) {
|
||||
is PeopleListEvent -> {
|
||||
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users))
|
||||
}
|
||||
} else {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(AuthorsByOutboxTopNavFilter(it.users))
|
||||
}
|
||||
}
|
||||
}
|
||||
is MuteListEvent -> {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users))
|
||||
}
|
||||
}
|
||||
is FollowListEvent -> {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet()))
|
||||
}
|
||||
is CommunityListEvent -> {
|
||||
emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet()))
|
||||
|
||||
noteEvent.publicAndPrivateCommunities(signer)?.let {
|
||||
val communities = it.map { it.addressId }.toSet()
|
||||
emit(AllCommunitiesTopNavFilter(communities))
|
||||
}
|
||||
}
|
||||
is HashtagListEvent -> {
|
||||
emit(HashtagTopNavFilter(noteEvent.publicHashtags().toSet(), allFollowRelays))
|
||||
|
||||
noteEvent.publicAndPrivateHashtag(signer)?.let {
|
||||
emit(HashtagTopNavFilter(it, allFollowRelays))
|
||||
}
|
||||
}
|
||||
is GeohashListEvent -> {
|
||||
emit(LocationTopNavFilter(noteEvent.publicGeohashes().toSet(), allFollowRelays))
|
||||
|
||||
noteEvent.publicAndPrivateGeohash(signer)?.let {
|
||||
emit(LocationTopNavFilter(it, allFollowRelays))
|
||||
}
|
||||
}
|
||||
is CommunityDefinitionEvent -> {
|
||||
SingleCommunityTopNavFilter(
|
||||
community = noteEvent.addressTag(),
|
||||
authors = noteEvent.moderatorKeys().toSet().ifEmpty { null },
|
||||
relays = noteEvent.relayUrls().toSet(),
|
||||
)
|
||||
}
|
||||
else -> AuthorsByOutboxTopNavFilter(emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun flow() =
|
||||
metadataFlow.transformLatest { noteState ->
|
||||
val noteEvent = noteState?.note?.event
|
||||
if (noteEvent == null) {
|
||||
AuthorsByOutboxTopNavFilter(emptySet())
|
||||
} else {
|
||||
process(noteEvent)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
val noteEvent = metadataFlow.value?.note?.event
|
||||
if (noteEvent == null) {
|
||||
AuthorsByOutboxTopNavFilter(emptySet())
|
||||
} else {
|
||||
collector.process(noteEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Immutable
|
||||
class AllCommunitiesTopNavFilter(
|
||||
val communities: Set<String>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean = true
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return noteEvent.isTaggedAddressableNotes(communities)
|
||||
}
|
||||
|
||||
fun convert(map: Map<NormalizedRelayUrl, Set<HexKey>>) =
|
||||
AllCommunitiesTopNavPerRelayFilterSet(
|
||||
map.mapValues { AllCommunitiesTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AllCommunitiesTopNavPerRelayFilterSet> {
|
||||
return CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) {
|
||||
convert(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet {
|
||||
return CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) {
|
||||
convert(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
|
||||
class AllCommunitiesTopNavPerRelayFilter(
|
||||
val communities: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AllCommunitiesTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AllCommunitiesTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Immutable
|
||||
class AuthorsByOutboxTopNavFilter(
|
||||
val authors: Set<String>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = pubkey in authors
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return if (noteEvent is LiveActivitiesEvent) {
|
||||
noteEvent.participantsIntersect(authors)
|
||||
} else {
|
||||
noteEvent.pubKey in authors
|
||||
}
|
||||
}
|
||||
|
||||
fun convert(map: Map<NormalizedRelayUrl, Set<HexKey>>) =
|
||||
AuthorsByOutboxTopNavPerRelayFilterSet(
|
||||
map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsByOutboxTopNavPerRelayFilterSet> {
|
||||
return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert)
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet {
|
||||
return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
|
||||
@Immutable
|
||||
class AuthorsByOutboxTopNavPerRelayFilter(
|
||||
val authors: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AuthorsByOutboxTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AuthorsByOutboxTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Immutable
|
||||
class SingleCommunityTopNavFilter(
|
||||
val community: String,
|
||||
val authors: Set<String>?,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = authors == null || pubkey in authors
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return if (noteEvent is LiveActivitiesEvent) {
|
||||
(authors != null && noteEvent.participantsIntersect(authors)) || noteEvent.isTaggedAddressableNote(community)
|
||||
} else if (noteEvent is CommentEvent) {
|
||||
(authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community)
|
||||
} else {
|
||||
(authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<SingleCommunityTopNavPerRelayFilterSet> {
|
||||
// relay field takes priority
|
||||
if (relays.isNotEmpty()) {
|
||||
return MutableStateFlow(
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
relays.associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (authors != null) {
|
||||
// go by authors
|
||||
return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) {
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
it.mapValues {
|
||||
SingleCommunityTopNavPerRelayFilter(community, it.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// go by hints
|
||||
return MutableStateFlow(
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
cache.relayHints.hintsForAddress(community).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): SingleCommunityTopNavPerRelayFilterSet {
|
||||
// relay field takes priority
|
||||
if (relays.isNotEmpty()) {
|
||||
return SingleCommunityTopNavPerRelayFilterSet(
|
||||
relays.associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (authors != null) {
|
||||
// go by authors
|
||||
return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) {
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
it.mapValues {
|
||||
SingleCommunityTopNavPerRelayFilter(community, it.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// go by hints
|
||||
return SingleCommunityTopNavPerRelayFilterSet(
|
||||
cache.relayHints.hintsForAddress(community).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
|
||||
@Immutable
|
||||
class SingleCommunityTopNavPerRelayFilter(
|
||||
val community: String,
|
||||
val authors: Set<String>?,
|
||||
) : IFeedTopNavPerRelayFilter
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class SingleCommunityTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, SingleCommunityTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Immutable
|
||||
class MutedAuthorsByOutboxTopNavFilter(
|
||||
val authors: Set<String>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = pubkey in authors
|
||||
|
||||
override fun match(noteEvent: Event): Boolean {
|
||||
return if (noteEvent is LiveActivitiesEvent) {
|
||||
noteEvent.participantsIntersect(authors)
|
||||
} else {
|
||||
noteEvent.pubKey in authors
|
||||
}
|
||||
}
|
||||
|
||||
fun convert(map: Map<NormalizedRelayUrl, Set<HexKey>>) =
|
||||
MutedAuthorsByOutboxTopNavPerRelayFilterSet(
|
||||
map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<MutedAuthorsByOutboxTopNavPerRelayFilterSet> {
|
||||
return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert)
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet {
|
||||
return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
|
||||
@Immutable
|
||||
class MutedAuthorsByOutboxTopNavPerRelayFilter(
|
||||
val authors: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class MutedAuthorsByOutboxTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, MutedAuthorsByOutboxTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.unknown
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class UnknownFeedFlow(
|
||||
val feedName: String,
|
||||
) : IFeedFlowsType {
|
||||
override fun flow() = MutableStateFlow(UnknownTopNavFilter(feedName))
|
||||
|
||||
// empty feed
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
collector.emit(UnknownTopNavFilter(feedName))
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -18,23 +18,27 @@
|
||||
* 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.relays.recommendations
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.unknown
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.ammolite.relays.FeedType
|
||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Immutable
|
||||
data class Kind3RelayProposalSetupInfo(
|
||||
val url: String,
|
||||
val read: Boolean,
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
val relayStat: RelayStat,
|
||||
val paidRelay: Boolean = false,
|
||||
val users: List<HexKey>,
|
||||
) {
|
||||
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
|
||||
class UnknownTopNavFilter(
|
||||
val feedName: String,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = false
|
||||
|
||||
override fun match(noteEvent: Event) = false
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<UnknownTopNavPerRelayFilterSet> {
|
||||
return MutableStateFlow(UnknownTopNavPerRelayFilterSet)
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache) = UnknownTopNavPerRelayFilterSet
|
||||
}
|
||||
+3
-10
@@ -18,15 +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.quartz.nip01Core.relay
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds.unknown
|
||||
|
||||
enum class RelayState {
|
||||
// Websocket connected
|
||||
CONNECTED,
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
|
||||
// Websocket disconnecting
|
||||
DISCONNECTING,
|
||||
|
||||
// Websocket disconnected
|
||||
DISCONNECTED,
|
||||
}
|
||||
object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.torState
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion
|
||||
|
||||
class TorRelayEvaluation(
|
||||
val torSettings: TorRelaySettings,
|
||||
val trustedRelayList: Set<NormalizedRelayUrl>,
|
||||
val dmRelayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
fun useTor(relay: NormalizedRelayUrl): Boolean {
|
||||
return if (torSettings.torType == TorType.OFF) {
|
||||
false
|
||||
} else {
|
||||
if (relay.isLocalHost()) {
|
||||
false
|
||||
} else if (relay.isOnion()) {
|
||||
torSettings.onionRelaysViaTor
|
||||
} else if (relay in dmRelayList) {
|
||||
torSettings.dmRelaysViaTor
|
||||
} else if (relay in trustedRelayList) {
|
||||
torSettings.trustedRelaysViaTor
|
||||
} else {
|
||||
torSettings.newRelaysViaTor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.torState
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
|
||||
class TorRelaySettings(
|
||||
val torType: TorType = TorType.OFF,
|
||||
val onionRelaysViaTor: Boolean = true,
|
||||
val dmRelaysViaTor: Boolean = false,
|
||||
val trustedRelaysViaTor: Boolean = false,
|
||||
val newRelaysViaTor: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.torState
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class TorRelayState(
|
||||
val trustedRelayState: TrustedRelayListsState,
|
||||
val dmRelayState: DmRelayListState,
|
||||
val settings: AccountSettings,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val torSettings =
|
||||
combine(
|
||||
settings.torSettings.torType,
|
||||
settings.torSettings.onionRelaysViaTor,
|
||||
settings.torSettings.dmRelaysViaTor,
|
||||
settings.torSettings.trustedRelaysViaTor,
|
||||
settings.torSettings.newRelaysViaTor,
|
||||
) {
|
||||
torType: TorType,
|
||||
onionRelaysViaTor: Boolean,
|
||||
dmRelaysViaTor: Boolean,
|
||||
trustedRelaysViaTor: Boolean,
|
||||
newRelaysViaTor: Boolean,
|
||||
->
|
||||
TorRelaySettings(torType, onionRelaysViaTor, dmRelaysViaTor, trustedRelaysViaTor, newRelaysViaTor)
|
||||
}.onStart {
|
||||
emit(
|
||||
TorRelaySettings(
|
||||
settings.torSettings.torType.value,
|
||||
settings.torSettings.onionRelaysViaTor.value,
|
||||
settings.torSettings.dmRelaysViaTor.value,
|
||||
settings.torSettings.trustedRelaysViaTor.value,
|
||||
settings.torSettings.newRelaysViaTor.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
TorRelaySettings(
|
||||
settings.torSettings.torType.value,
|
||||
settings.torSettings.onionRelaysViaTor.value,
|
||||
settings.torSettings.dmRelaysViaTor.value,
|
||||
settings.torSettings.trustedRelaysViaTor.value,
|
||||
settings.torSettings.newRelaysViaTor.value,
|
||||
),
|
||||
)
|
||||
|
||||
val flow =
|
||||
combineTransform(
|
||||
torSettings,
|
||||
trustedRelayState.flow,
|
||||
dmRelayState.flow,
|
||||
) { torSettings: TorRelaySettings, trustedRelayList: Set<NormalizedRelayUrl>, dmRelayList: Set<NormalizedRelayUrl> ->
|
||||
emit(TorRelayEvaluation(torSettings, trustedRelayList, dmRelayList))
|
||||
}.onStart {
|
||||
emit(
|
||||
TorRelayEvaluation(
|
||||
torSettings.value,
|
||||
trustedRelayState.flow.value,
|
||||
dmRelayState.flow.value,
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
TorRelayEvaluation(
|
||||
torSettings.value,
|
||||
trustedRelayState.flow.value,
|
||||
dmRelayState.flow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
+30
-35
@@ -22,8 +22,9 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Call
|
||||
@@ -49,25 +50,23 @@ object Nip11CachedRetriever {
|
||||
|
||||
class RetrieveResultLoading : RetrieveResult(TimeUtils.now())
|
||||
|
||||
private val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
|
||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(100)
|
||||
private val retriever = Nip11Retriever()
|
||||
|
||||
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
|
||||
val result = relayInformationDocumentCache.get(RelayUrlFormatter.getHttpsUrl(dirtyUrl)) ?: return null
|
||||
fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation? {
|
||||
val result = relayInformationDocumentCache.get(relay) ?: return null
|
||||
if (result is RetrieveResultSuccess) return result.data
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun loadRelayInfo(
|
||||
dirtyUrl: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
val url = RelayUrlFormatter.getHttpsUrl(dirtyUrl)
|
||||
val doc = relayInformationDocumentCache.get(url)
|
||||
|
||||
val doc = relayInformationDocumentCache.get(relay)
|
||||
if (doc != null) {
|
||||
if (doc is RetrieveResultSuccess) {
|
||||
onInfo(doc.data)
|
||||
@@ -75,41 +74,37 @@ object Nip11CachedRetriever {
|
||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
||||
// just wait.
|
||||
} else {
|
||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||
retrieve(relay, okHttpClient, onInfo, onError)
|
||||
}
|
||||
} else if (doc is RetrieveResultError) {
|
||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
||||
onError(dirtyUrl, doc.error, null)
|
||||
onError(relay, doc.error, null)
|
||||
} else {
|
||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||
retrieve(relay, okHttpClient, onInfo, onError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||
retrieve(relay, okHttpClient, onInfo, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieve(
|
||||
url: String,
|
||||
dirtyUrl: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
relayInformationDocumentCache.put(url, RetrieveResultLoading())
|
||||
relayInformationDocumentCache.put(relay, RetrieveResultLoading())
|
||||
retriever.loadRelayInfo(
|
||||
url = url,
|
||||
dirtyUrl = dirtyUrl,
|
||||
relay = relay,
|
||||
okHttpClient = okHttpClient,
|
||||
onInfo = {
|
||||
checkNotInMainThread()
|
||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||
relayInformationDocumentCache.put(relay, RetrieveResultSuccess(it))
|
||||
onInfo(it)
|
||||
},
|
||||
onError = { dirtyUrl, code, errorMsg ->
|
||||
checkNotInMainThread()
|
||||
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||
onError(url, code, errorMsg)
|
||||
onError = { relay, code, errorMsg ->
|
||||
relayInformationDocumentCache.put(relay, RetrieveResultError(code, errorMsg))
|
||||
onError(relay, code, errorMsg)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -124,13 +119,13 @@ class Nip11Retriever {
|
||||
}
|
||||
|
||||
suspend fun loadRelayInfo(
|
||||
url: String,
|
||||
dirtyUrl: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (String, ErrorCode, String?) -> Unit,
|
||||
onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
val url = relay.toHttp()
|
||||
try {
|
||||
val request: Request =
|
||||
Request
|
||||
@@ -154,16 +149,16 @@ class Nip11Retriever {
|
||||
if (it.isSuccessful) {
|
||||
onInfo(Nip11RelayInformation.fromJson(body))
|
||||
} else {
|
||||
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||
onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(
|
||||
"RelayInfoFail",
|
||||
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
||||
"Resulting Message from Relay ${relay.url} in not parseable: $body",
|
||||
e,
|
||||
)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
|
||||
onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,15 +167,15 @@ class Nip11Retriever {
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
|
||||
Log.e("RelayInfoFail", "${relay.url} unavailable", e)
|
||||
onError(relay, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||
Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e)
|
||||
onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ package com.vitorpamplona.amethyst.service
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.collectSuccessfulOperations
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
@@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.collectSuccessfulOperations
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -173,6 +175,16 @@ class ZapPaymentHandler(
|
||||
val user: User? = null,
|
||||
)
|
||||
|
||||
fun receivingRelaySet(userHex: HexKey): Set<NormalizedRelayUrl>? {
|
||||
return (
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(userHex),
|
||||
)?.event as? AdvertisedRelayListEvent
|
||||
)
|
||||
?.readRelaysNorm()?.toSet()
|
||||
}
|
||||
|
||||
suspend fun signAllZapRequests(
|
||||
note: Note,
|
||||
pollOption: Int?,
|
||||
@@ -181,18 +193,6 @@ class ZapPaymentHandler(
|
||||
zapsToSend: List<BaseZapSplitSetup>,
|
||||
onAllDone: suspend (List<ZapRequestReady>) -> Unit,
|
||||
) {
|
||||
val authorRelayList =
|
||||
note.author
|
||||
?.pubkeyHex
|
||||
?.let {
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(it),
|
||||
)?.event as? AdvertisedRelayListEvent?
|
||||
)?.readRelays()
|
||||
}?.toSet()
|
||||
|
||||
collectSuccessfulOperations<BaseZapSplitSetup, ZapRequestReady>(
|
||||
items = zapsToSend,
|
||||
runRequestFor = { next: BaseZapSplitSetup, onReady ->
|
||||
@@ -203,18 +203,12 @@ class ZapPaymentHandler(
|
||||
}
|
||||
}
|
||||
} else if (next is ZapSplitSetup) {
|
||||
val user = LocalCache.getUserIfExists(next.pubKeyHex)
|
||||
val userRelayList =
|
||||
(
|
||||
(
|
||||
LocalCache
|
||||
.getAddressableNoteIfExists(
|
||||
AdvertisedRelayListEvent.createAddressTag(next.pubKeyHex),
|
||||
)?.event as? AdvertisedRelayListEvent?
|
||||
)?.readRelays()?.toSet() ?: emptySet()
|
||||
) + (authorRelayList ?: emptySet())
|
||||
val authorRelayList = note.author?.let { receivingRelaySet(it.pubkeyHex) } ?: emptySet()
|
||||
val userRelayList = receivingRelaySet(next.pubKeyHex) ?: emptySet()
|
||||
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson ->
|
||||
val user = LocalCache.getOrCreateUser(next.pubKeyHex)
|
||||
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList + authorRelayList) { zapRequestJson ->
|
||||
onReady(ZapRequestReady(next, zapRequestJson, user))
|
||||
}
|
||||
}
|
||||
@@ -387,7 +381,7 @@ class ZapPaymentHandler(
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null,
|
||||
additionalRelays: Set<String>? = null,
|
||||
additionalRelays: Set<NormalizedRelayUrl>? = null,
|
||||
onReady: (String?) -> Unit,
|
||||
) {
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
|
||||
+7
-6
@@ -25,11 +25,12 @@ import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.tryAndWait
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.utils.launchAndWaitAll
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -51,7 +52,7 @@ class RegisterAccounts(
|
||||
|
||||
private suspend fun signAllAuths(
|
||||
notificationToken: String,
|
||||
remainingTos: List<Pair<AccountSettings, List<String>>>,
|
||||
remainingTos: List<Pair<AccountSettings, List<NormalizedRelayUrl>>>,
|
||||
output: MutableList<RelayAuthEvent>,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||
) {
|
||||
@@ -99,15 +100,15 @@ class RegisterAccounts(
|
||||
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val nip65Read = acc.backupNIP65RelayList?.readRelays() ?: emptyList()
|
||||
val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList()
|
||||
|
||||
Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ")}")
|
||||
|
||||
val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList<String>()
|
||||
val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList()
|
||||
|
||||
Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ")}")
|
||||
|
||||
val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
|
||||
val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList()
|
||||
|
||||
Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ class DualHttpClientManager(
|
||||
) {
|
||||
val factory = OkHttpClientFactory(keyCache)
|
||||
|
||||
private val defaultHttpClient: StateFlow<OkHttpClient> =
|
||||
val defaultHttpClient: StateFlow<OkHttpClient> =
|
||||
combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile ->
|
||||
factory.buildHttpClient(proxy, mobile, userAgent)
|
||||
}.stateIn(
|
||||
@@ -48,7 +48,7 @@ class DualHttpClientManager(
|
||||
factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent),
|
||||
)
|
||||
|
||||
private val defaultHttpClientWithoutProxy: StateFlow<OkHttpClient> =
|
||||
val defaultHttpClientWithoutProxy: StateFlow<OkHttpClient> =
|
||||
isMobileDataProvider
|
||||
.map { mobile ->
|
||||
factory.buildHttpClient(mobile, userAgent)
|
||||
|
||||
+36
-23
@@ -20,27 +20,50 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.okhttp
|
||||
|
||||
import android.system.Os.socket
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class OkHttpWebSocket(
|
||||
val url: String,
|
||||
val forceProxy: Boolean,
|
||||
val httpClient: (url: String, forceProxy: Boolean) -> OkHttpClient,
|
||||
val url: NormalizedRelayUrl,
|
||||
val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient,
|
||||
val out: WebSocketListener,
|
||||
) : WebSocket {
|
||||
private val listener = OkHttpWebsocketListener()
|
||||
private var usingOkHttp: OkHttpClient? = null
|
||||
private var socket: okhttp3.WebSocket? = null
|
||||
|
||||
fun buildRequest() = Request.Builder().url(url.trim()).build()
|
||||
fun buildRequest() = Request.Builder().url(url.url).build()
|
||||
|
||||
override fun needsReconnect(): Boolean {
|
||||
val myUsingOkHttp = usingOkHttp
|
||||
if (myUsingOkHttp == null) return true
|
||||
|
||||
val currentOkHttp = httpClient(url)
|
||||
|
||||
val usingProxy = myUsingOkHttp.proxy
|
||||
val currentProxy = currentOkHttp.proxy
|
||||
|
||||
if (usingProxy != null && currentProxy != null && usingProxy != currentProxy) return true
|
||||
if (usingProxy == null && currentProxy != null) return true
|
||||
if (usingProxy != null && currentProxy == null) return true
|
||||
|
||||
if (currentOkHttp.readTimeoutMillis != myUsingOkHttp.readTimeoutMillis) return true
|
||||
if (currentOkHttp.writeTimeoutMillis != myUsingOkHttp.writeTimeoutMillis) return true
|
||||
if (currentOkHttp.connectTimeoutMillis != myUsingOkHttp.connectTimeoutMillis) return true
|
||||
if (currentOkHttp.callTimeoutMillis != myUsingOkHttp.callTimeoutMillis) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
socket = httpClient(url, forceProxy).newWebSocket(buildRequest(), listener)
|
||||
usingOkHttp = httpClient(url)
|
||||
socket = usingOkHttp?.newWebSocket(buildRequest(), listener)
|
||||
}
|
||||
|
||||
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
||||
@@ -49,7 +72,7 @@ class OkHttpWebSocket(
|
||||
response: Response,
|
||||
) = out.onOpen(
|
||||
response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
||||
response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false,
|
||||
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
|
||||
)
|
||||
|
||||
override fun onMessage(
|
||||
@@ -73,31 +96,21 @@ class OkHttpWebSocket(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
t: Throwable,
|
||||
response: Response?,
|
||||
) = out.onFailure(t, response?.message)
|
||||
) = out.onFailure(t, response?.code, response?.message)
|
||||
}
|
||||
|
||||
class Builder(
|
||||
val forceProxy: Boolean,
|
||||
val httpClient: (String, Boolean) -> OkHttpClient,
|
||||
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||
) : WebsocketBuilder {
|
||||
// Called when connecting.
|
||||
override fun build(
|
||||
url: String,
|
||||
url: NormalizedRelayUrl,
|
||||
out: WebSocketListener,
|
||||
) = OkHttpWebSocket(url, forceProxy, httpClient, out)
|
||||
) = OkHttpWebSocket(url, httpClient, out)
|
||||
}
|
||||
|
||||
class BuilderFactory(
|
||||
val httpClient: (String, Boolean) -> OkHttpClient,
|
||||
) : WebsocketBuilderFactory {
|
||||
override fun build(
|
||||
url: String,
|
||||
forceProxy: Boolean,
|
||||
) = Builder(forceProxy, httpClient)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
socket?.cancel()
|
||||
override fun disconnect() {
|
||||
socket?.close(1000, "Normal closure")
|
||||
}
|
||||
|
||||
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user