diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000000..91f95584dd
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinNotebook.xml b/.idea/kotlinNotebook.xml
new file mode 100644
index 0000000000..665e38dae3
--- /dev/null
+++ b/.idea/kotlinNotebook.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index bb4493707f..bdfaa5e068 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,5 +1,11 @@
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 97d6e99dfb..bab0a73aaf 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
index 91b9e0ad57..fc3031dba1 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
@@ -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) }
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
index 766ae8b776..7caf3ef17b 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
@@ -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)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
index 5349ffe8ff..b9c909df51 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
@@ -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?> = MutableStateFlow(null)
- private var cachedAccounts: MutableMap = mutableMapOf()
+ private val savedAccounts: MutableStateFlow?> = MutableStateFlow(null)
+ private val cachedAccounts: MutableMap = 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>(PrefKeys.RELAYS) ?: emptySet()
-
val zapPaymentRequestServer = parseOrNull(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
@@ -538,8 +565,11 @@ object LocalPreferences {
val latestMuteList = parseEventOrNull(PrefKeys.LATEST_MUTE_LIST)
val latestPrivateHomeRelayList = parseEventOrNull(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
val latestAppSpecificData = parseEventOrNull(PrefKeys.LATEST_APP_SPECIFIC_DATA)
- val latestEphemeralList = parseEventOrNull(PrefKeys.LATEST_EPHEMERAL_LIST)
val latestChannelList = parseEventOrNull(PrefKeys.LATEST_CHANNEL_LIST)
+ val latestCommunityList = parseEventOrNull(PrefKeys.LATEST_COMMUNITY_LIST)
+ val latestHashtagList = parseEventOrNull(PrefKeys.LATEST_HASHTAG_LIST)
+ val latestGeohashList = parseEventOrNull(PrefKeys.LATEST_GEOHASH_LIST)
+ val latestEphemeralList = parseEventOrNull(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),
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt
deleted file mode 100644
index bc464ebdc1..0000000000
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
index f153af0263..d4820c0a7e 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -21,40 +21,55 @@
package com.vitorpamplona.amethyst.model
import android.util.Log
-import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import com.fasterxml.jackson.module.kotlin.readValue
-import com.fonfon.kgeohash.GeoHash
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
+import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState
+import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
+import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
+import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
+import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction
+import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
-import com.vitorpamplona.amethyst.service.checkNotInMainThread
+import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction
+import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.BlockPeopleListState
+import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState
+import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState
+import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
+import com.vitorpamplona.amethyst.model.nip51Lists.MuteListState
+import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction
+import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
+import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
+import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
+import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
+import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
+import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
+import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
+import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
+import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
+import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
+import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
+import com.vitorpamplona.amethyst.model.topNavFeeds.MergedTopFeedAuthorListsState
+import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
+import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
import com.vitorpamplona.amethyst.service.uploads.FileHeader
-import com.vitorpamplona.amethyst.tryAndWait
-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 com.vitorpamplona.amethyst.ui.tor.TorType
-import com.vitorpamplona.ammolite.relays.Constants
-import com.vitorpamplona.ammolite.relays.FeedType
-import com.vitorpamplona.ammolite.relays.Relay
-import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
-import com.vitorpamplona.ammolite.relays.RelaySetupInfo
-import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect
-import com.vitorpamplona.ammolite.relays.TypedFilter
-import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
-import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent
-import com.vitorpamplona.quartz.blossom.BlossomServersEvent
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
-import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
-import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
+import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
@@ -80,13 +95,20 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
-import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
-import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
+import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
+import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent
+import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
+import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
+import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
-import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags
+import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
@@ -97,9 +119,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
-import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
-import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
-import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
@@ -110,7 +129,6 @@ import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
-import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
@@ -125,29 +143,26 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
-import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
-import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
+import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
+import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
+import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
-import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
-import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
-import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response
-import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
-import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
-import com.vitorpamplona.quartz.nip56Reports.ReportEvent
+import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
+import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -157,17 +172,13 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
-import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
-import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
+import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip68Picture.PictureMeta
import com.vitorpamplona.quartz.nip68Picture.pictureIMeta
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
-import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
-import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
-import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
@@ -180,36 +191,34 @@ import com.vitorpamplona.quartz.nip94FileMetadata.magnet
import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
-import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
-import com.vitorpamplona.quartz.utils.DualCase
+import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
+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.FlowPreview
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
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.combineTransform
import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.czeal.rfc3986.URIReference
import java.math.BigDecimal
import java.util.Base64
import java.util.Locale
-import kotlin.coroutines.cancellation.CancellationException
+import kotlin.Boolean
+import kotlin.collections.filter
+import kotlin.collections.flatten
+import kotlin.collections.ifEmpty
+import kotlin.collections.map
+import kotlin.collections.plus
+import kotlin.collections.toSet
import kotlin.coroutines.resume
@OptIn(DelicateCoroutinesApi::class)
@@ -217,905 +226,124 @@ import kotlin.coroutines.resume
class Account(
val settings: AccountSettings = AccountSettings(KeyPair()),
val signer: NostrSigner = settings.createSigner(),
+ val geolocationFlow: StateFlow,
+ val cache: LocalCache,
+ val client: NostrClient,
val scope: CoroutineScope,
) {
- companion object {
- const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
- }
-
- var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf())
-
- @Immutable
- class LiveFollowList(
- val authors: Set = emptySet(),
- val authorsPlusMe: Set,
- val hashtags: Set = emptySet(),
- val geotags: Set = emptySet(),
- val addresses: Set = emptySet(),
- ) {
- val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
- val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
- }
-
- class FeedsBaseFlows(
- val listName: String,
- val peopleList: StateFlow = MutableStateFlow(NoteState(Note(" "))),
- val kind3: StateFlow = MutableStateFlow(null),
- val location: StateFlow = MutableStateFlow(null),
- )
-
- val connectToRelaysFlow =
- combineTransform(
- getNIP65RelayListFlow(),
- getDMRelayListFlow(),
- getSearchRelayListFlow(),
- getPrivateOutboxRelayListFlow(),
- userProfile().flow().relays.stateFlow,
- ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
- checkNotInMainThread()
- emit(
- normalizeAndCombineRelayListsWithFallbacks(
- kind3RelayList = kind3Relays(),
- newDMRelayEvent = dmRelayList.note.event as? ChatMessageRelayListEvent,
- searchRelayEvent = searchRelayList.note.event as? SearchRelayListEvent,
- privateOutboxRelayEvent = privateOutBox.note.event as? PrivateOutboxRelayListEvent,
- nip65RelayEvent = nip65RelayList.note.event as? AdvertisedRelayListEvent,
- ).toTypedArray(),
- )
- }
-
- private fun normalizeAndCombineRelayListsWithFallbacks(
- kind3RelayList: Array? = null,
- newDMRelayEvent: ChatMessageRelayListEvent? = null,
- searchRelayEvent: SearchRelayListEvent? = null,
- privateOutboxRelayEvent: PrivateOutboxRelayListEvent? = null,
- nip65RelayEvent: AdvertisedRelayListEvent? = null,
- localRelayList: Set? = null,
- ) = normalizeAndCombineRelayLists(
- baseRelaySet = kind3RelayList ?: convertLocalRelays(),
- newDMRelayEvent = newDMRelayEvent ?: settings.backupDMRelayList,
- searchRelayEvent = searchRelayEvent ?: settings.backupSearchRelayList,
- privateOutboxRelayEvent = privateOutboxRelayEvent ?: settings.backupPrivateHomeRelayList,
- nip65RelayEvent = nip65RelayEvent ?: settings.backupNIP65RelayList,
- localRelayList = localRelayList ?: settings.localRelayServers,
- )
-
- private fun normalizeAndCombineRelayLists(
- baseRelaySet: Array,
- newDMRelayEvent: ChatMessageRelayListEvent?,
- searchRelayEvent: SearchRelayListEvent?,
- privateOutboxRelayEvent: PrivateOutboxRelayListEvent?,
- nip65RelayEvent: AdvertisedRelayListEvent?,
- localRelayList: Set,
- ): List {
- val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet()
- val nip65RelaySet =
- nip65RelayEvent?.relays()?.map {
- AdvertisedRelayListEvent.AdvertisedRelayInfo(
- RelayUrlFormatter.normalize(it.relayUrl),
- it.type,
- )
- }
- val privateOutboxRelaySet = privateOutboxRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- val localRelaySet = localRelayList.map { RelayUrlFormatter.normalize(it) }.toSet()
-
- return combineRelayLists(
- baseRelaySet = baseRelaySet,
- newDMRelaySet = newDMRelaySet,
- searchRelaySet = searchRelaySet,
- privateOutboxRelaySet = privateOutboxRelaySet,
- nip65RelaySet = nip65RelaySet,
- localRelaySet = localRelaySet,
- )
- }
-
- private fun combineRelayLists(
- baseRelaySet: Array,
- newDMRelaySet: Set,
- searchRelaySet: Set,
- privateOutboxRelaySet: Set,
- nip65RelaySet: List?,
- localRelaySet: Set,
- ): List {
- // ------
- // DMs
- // ------
- var mappedRelaySet =
- baseRelaySet.map {
- if (newDMRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + FeedType.PRIVATE_DMS)
- } else {
- it
- }
- }
-
- newDMRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // ------
- // SEARCH
- // ------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (searchRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, it.write || false, it.feedTypes + FeedType.SEARCH)
- } else {
- it
- }
- }
-
- searchRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- false,
- setOf(
- FeedType.SEARCH,
- ),
- )
- }
- }
-
- // --------------
- // PRIVATE OUTBOX
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (privateOutboxRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
- } else {
- it
- }
- }
-
- privateOutboxRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- FeedType.GLOBAL,
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // --------------
- // Local Storage
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (localRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
- } else {
- it
- }
- }
-
- localRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- FeedType.GLOBAL,
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // --------------
- // NIP-65 Public Inbox/Outbox
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map { relay ->
- val nip65setup = nip65RelaySet?.firstOrNull { relay.url == it.relayUrl }
- if (nip65setup != null) {
- val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
-
- RelaySetupInfo(
- relay.url,
- true,
- relay.write || write,
- relay.feedTypes +
- setOf(
- FeedType.FOLLOWS,
- FeedType.GLOBAL,
- FeedType.PUBLIC_CHATS,
- ),
- )
- } else {
- relay
- }
- }
-
- nip65RelaySet?.forEach { newNip65Setup ->
- if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) {
- val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
-
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newNip65Setup.relayUrl,
- true,
- write,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- ),
- )
- }
- }
- return mappedRelaySet
- }
-
- val connectToRelays =
- connectToRelaysFlow
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeAndCombineRelayListsWithFallbacks(
- kind3Relays(),
- getDMRelayList(),
- getSearchRelayList(),
- getPrivateOutboxRelayList(),
- getNIP65RelayList(),
- ).toTypedArray(),
- )
-
- val connectToRelaysWithProxy =
- combineTransform(
- connectToRelays,
- settings.torSettings.torType,
- settings.torSettings.onionRelaysViaTor,
- settings.torSettings.trustedRelaysViaTor,
- ) { relays, torType, useTorForOnionRelays, useTorForTrustedRelays ->
- emit(
- relays
- .map {
- RelaySetupInfoToConnect(
- it.url,
- torType != TorType.OFF && checkLocalHostOnionAndThen(it.url, useTorForOnionRelays, useTorForTrustedRelays),
- it.read,
- it.write,
- it.feedTypes,
- )
- }.toTypedArray(),
- )
- }.flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeAndCombineRelayListsWithFallbacks(
- kind3Relays(),
- getDMRelayList(),
- getSearchRelayList(),
- getPrivateOutboxRelayList(),
- getNIP65RelayList(),
- ).map {
- RelaySetupInfoToConnect(
- it.url,
- settings.torSettings.torType.value != TorType.OFF &&
- checkLocalHostOnionAndThen(
- it.url,
- settings.torSettings.onionRelaysViaTor.value,
- settings.torSettings.trustedRelaysViaTor.value,
- ),
- it.read,
- it.write,
- it.feedTypes,
- )
- }.toTypedArray(),
- )
-
- fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
- // makes sure the output include only valid p tags
- val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
-
- return LiveFollowList(
- authors = verifiedFollowingUsers,
- authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
- hashtags =
- latestContactList
- ?.unverifiedFollowTagSet()
- ?.map { it.lowercase() }
- ?.toSet() ?: emptySet(),
- geotags =
- latestContactList
- ?.geohashes()
- ?.toSet() ?: emptySet(),
- addresses =
- latestContactList
- ?.verifiedFollowAddressSet()
- ?.toSet() ?: emptySet(),
- )
- }
-
- fun normalizeDMRelayListWithBackup(note: Note): Set {
- val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
- return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedDmRelaySet =
- getDMRelayListFlow()
- .map { normalizeDMRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeDMRelayListWithBackup(getDMRelayListNote()),
- )
-
- fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set {
- val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
- return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedPrivateOutBoxRelaySet =
- getPrivateOutboxRelayListFlow()
- .map { normalizePrivateOutboxRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()),
- )
-
- fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set {
- val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
- return event?.writeRelays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedNIP65WriteRelayList =
- getNIP65RelayListFlow()
- .map { normalizeNIP65WriteRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeNIP65WriteRelayListWithBackup(getNIP65RelayListNote()),
- )
-
- @OptIn(ExperimentalCoroutinesApi::class)
- val liveKind3FollowsFlow: Flow =
- userProfile().flow().follows.stateFlow.transformLatest {
- emit(buildFollowLists(it.user.latestContactList))
- }
-
- val liveKind3Follows =
- liveKind3FollowsFlow
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
- )
-
- fun loadFlowsFor(listName: String): FeedsBaseFlows =
- when (listName) {
- GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
- KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
- AROUND_ME ->
- FeedsBaseFlows(
- listName,
- location = Amethyst.instance.locationManager.geohashStateFlow,
- )
- else -> {
- val note = LocalCache.checkGetOrCreateAddressableNote(listName)
- if (note != null) {
- FeedsBaseFlows(
- listName,
- peopleList =
- note
- .flow()
- .metadata.stateFlow,
- )
- } else {
- FeedsBaseFlows(listName)
- }
- }
- }
-
- fun compute50kmLine(geoHash: GeoHash): List {
- val hashes = mutableListOf()
-
- 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 {
- val hashes = mutableListOf()
-
- 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
- }
-
- suspend fun mapIntoFollowLists(
- listName: String,
- kind3: LiveFollowList?,
- noteState: NoteState,
- location: LocationState.LocationResult?,
- ): LiveFollowList? =
- if (listName == GLOBAL_FOLLOWS) {
- null
- } else if (listName == KIND3_FOLLOWS) {
- kind3
- } else if (listName == AROUND_ME) {
- val geohashResult = location ?: Amethyst.instance.locationManager.geohashStateFlow.value
- if (geohashResult is LocationState.LocationResult.Success) {
- // 2 neighbors deep = 25x25km
- LiveFollowList(
- authorsPlusMe = setOf(signer.pubKey),
- geotags = compute50kmRange(geohashResult.geoHash).toSet(),
- )
- } else {
- LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- }
- } else {
- val noteEvent = noteState.note.event
- if (noteEvent is GeneralListEvent) {
- waitToDecrypt(noteEvent) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- } else if (noteEvent is FollowListEvent) {
- LiveFollowList(authors = noteEvent.pubKeys().toSet(), authorsPlusMe = setOf(signer.pubKey) + noteEvent.pubKeys())
- } else {
- LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- fun combinePeopleListFlows(peopleListFollowsSource: Flow): Flow =
- peopleListFollowsSource
- .transformLatest { listName ->
- val followList = loadFlowsFor(listName)
- emitAll(
- combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location ->
- mapIntoFollowLists(followList.listName, kind3, peopleList, location)
- },
- )
- }
-
- val liveHomeFollowLists: StateFlow by lazy {
- combinePeopleListFlows(settings.defaultHomeFollowList)
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- runBlocking {
- loadAndCombineFlows(settings.defaultHomeFollowList.value)
- },
- )
- }
-
- val liveServerList: StateFlow> by lazy {
- combine(getFileServersListFlow(), getBlossomServersListFlow()) { nip96, blossom ->
- mergeServerList(nip96.note.event as? FileServersEvent, blossom.note.event as? BlossomServersEvent)
- }.flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- runBlocking {
- mergeServerList(getFileServersList(), getBlossomServersList())
- },
- )
- }
-
- suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
- val flows = loadFlowsFor(listName)
- return mapIntoFollowLists(
- flows.listName,
- flows.kind3.value,
- flows.peopleList.value,
- flows.location.value,
- )
- }
-
- /**
- * filter onion and local host from write relays
- * for each user pubkey, a list of valid relays.
- */
- private fun assembleAuthorsPerWriteRelay(
- userList: Map>,
- hasOnionConnection: Boolean = false,
- ): Map> {
- checkNotInMainThread()
-
- val authorsPerRelayUrl = mutableMapOf>()
- val relayUrlsPerAuthor = mutableMapOf>()
-
- userList.forEach { userWriteRelayListPair ->
- userWriteRelayListPair.value.forEach { relayUrl ->
- if (!RelayUrlFormatter.isLocalHost(relayUrl) && (hasOnionConnection || !RelayUrlFormatter.isOnion(relayUrl))) {
- RelayUrlFormatter.normalizeOrNull(relayUrl)?.let { normRelayUrl ->
- val userSet = authorsPerRelayUrl[normRelayUrl]
- if (userSet != null) {
- userSet.add(userWriteRelayListPair.key)
- } else {
- authorsPerRelayUrl[normRelayUrl] = mutableSetOf(userWriteRelayListPair.key)
- }
-
- val relaySet = authorsPerRelayUrl[userWriteRelayListPair.key]
- if (relaySet != null) {
- relaySet.add(normRelayUrl)
- } else {
- relayUrlsPerAuthor[userWriteRelayListPair.key] = mutableSetOf(normRelayUrl)
- }
- }
- }
- }
- }
-
- // for each relay, authors that only use this relay go first.
- // then keeps order by pubkey asc
- val comparator = compareByDescending { relayUrlsPerAuthor[it]?.size ?: 0 }.thenBy { it }
-
- return authorsPerRelayUrl.mapValues {
- it.value.sortedWith(comparator)
- }
- }
-
- fun authorsPerRelay(
- followsNIP65RelayLists: List,
- defaultRelayList: List,
- torType: TorType,
- ): Map> = authorsPerRelay(followsNIP65RelayLists, defaultRelayList, torType != TorType.OFF)
-
- fun authorsPerRelay(
- followsNIP65RelayLists: List,
- defaultRelayList: List,
- acceptOnion: Boolean,
- ): Map> {
- checkNotInMainThread()
-
- val defaultSet = defaultRelayList.toSet()
-
- return assembleAuthorsPerWriteRelay(
- followsNIP65RelayLists
- .mapNotNull
- {
- val author = (it as? AddressableNote)?.address?.pubKeyHex
- val event = (it.event as? AdvertisedRelayListEvent)
-
- if (event != null) {
- val authorWriteRelays =
- event.writeRelays().map {
- RelayUrlFormatter.normalize(it)
- }
-
- val commonRelaysToMe = authorWriteRelays.filter { it in defaultSet }
- if (commonRelaysToMe.isNotEmpty()) {
- event.pubKey to commonRelaysToMe
- } else {
- event.pubKey to defaultRelayList
- }
- } else {
- if (author != null) {
- author to defaultRelayList
- } else {
- Log.e("Account", "This author should NEVER be null. Note: ${it.idHex}")
- null
- }
- }
- }.toMap(),
- hasOnionConnection = acceptOnion,
- )
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- val liveHomeFollowListAdvertizedRelayListFlow: Flow?> =
- liveHomeFollowLists
- .transformLatest { followList ->
- if (followList != null) {
- emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
- } else {
- emit(null)
- }
- }
-
- val liveHomeListAuthorsPerRelayFlow: Flow