mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.1.0" />
|
<option name="version" value="2.1.0" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ Lastly, the user's account information (private key/pub key) is stored in the An
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Make sure to have the following pre-requisites installed:
|
Make sure to have the following pre-requisites installed:
|
||||||
1. Java 17+
|
1. Java 21+
|
||||||
2. Android Studio
|
2. Android Studio
|
||||||
3. Android 8.0+ Phone or Emulation setup
|
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.DualHttpClientManager
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
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.ots.OtsBlockHeightCache
|
||||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
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.RelaySpeedLogger
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
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 com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -89,18 +91,26 @@ class Amethyst : Application() {
|
|||||||
scope = applicationIOScope,
|
scope = applicationIOScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connects the NostrClient class with okHttp
|
val torProxySettingsAnchor = ProxySettingsAnchor()
|
||||||
val factory =
|
|
||||||
OkHttpWebSocket.BuilderFactory { _, useProxy ->
|
|
||||||
okHttpClients.getHttpClient(useProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provides a relay pool
|
// Connects the NostrClient class with okHttp
|
||||||
val client: NostrClient = NostrClient(factory)
|
val websocketBuilder =
|
||||||
|
OkHttpWebSocket.Builder { url ->
|
||||||
|
okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url))
|
||||||
|
}
|
||||||
|
|
||||||
// Caches all events in Memory
|
// Caches all events in Memory
|
||||||
val cache: LocalCache = LocalCache
|
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
|
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||||
|
|
||||||
@@ -112,15 +122,9 @@ class Amethyst : Application() {
|
|||||||
|
|
||||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||||
|
|
||||||
// Organizes cache clearing
|
|
||||||
val trimmingService = MemoryTrimmingService(cache)
|
|
||||||
|
|
||||||
// Coordinates all subscriptions for the Nostr Client
|
// Coordinates all subscriptions for the Nostr Client
|
||||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
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
|
// saves the .content of NIP-95 blobs in disk to save memory
|
||||||
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ import kotlin.time.measureTimedValue
|
|||||||
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||||
|
|
||||||
fun debugState(context: Context) {
|
fun debugState(context: Context) {
|
||||||
Amethyst.instance.client
|
// Amethyst.instance.client
|
||||||
.allSubscriptions()
|
// .allSubscriptions()
|
||||||
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
|
// .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") }
|
||||||
|
|
||||||
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
|
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
|
||||||
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
|
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
|
||||||
|
|||||||
@@ -27,16 +27,15 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
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.AccountSettings
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
|
||||||
import com.vitorpamplona.amethyst.model.Settings
|
import com.vitorpamplona.amethyst.model.Settings
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
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.ServerName
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
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.edits.PrivateOutboxRelayListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
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.nip47WalletConnect.Nip47WalletConnect
|
||||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
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.nip65RelayList.AdvertisedRelayListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -82,7 +84,6 @@ private object PrefKeys {
|
|||||||
const val SAVED_ACCOUNTS = "all_saved_accounts"
|
const val SAVED_ACCOUNTS = "all_saved_accounts"
|
||||||
const val NOSTR_PRIVKEY = "nostr_privkey"
|
const val NOSTR_PRIVKEY = "nostr_privkey"
|
||||||
const val NOSTR_PUBKEY = "nostr_pubkey"
|
const val NOSTR_PUBKEY = "nostr_pubkey"
|
||||||
const val RELAYS = "relays"
|
|
||||||
const val LOCAL_RELAY_SERVERS = "localRelayServers"
|
const val LOCAL_RELAY_SERVERS = "localRelayServers"
|
||||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
||||||
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
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_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
|
||||||
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
|
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
|
||||||
const val LATEST_CHANNEL_LIST = "latestChannelList"
|
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 LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
||||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||||
@@ -120,8 +124,8 @@ object LocalPreferences {
|
|||||||
private const val COMMA = ","
|
private const val COMMA = ","
|
||||||
|
|
||||||
private var currentAccount: String? = null
|
private var currentAccount: String? = null
|
||||||
private var savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
|
private val savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
|
||||||
private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
private val cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
||||||
|
|
||||||
suspend fun currentAccount(): String? {
|
suspend fun currentAccount(): String? {
|
||||||
if (currentAccount == null) {
|
if (currentAccount == null) {
|
||||||
@@ -249,7 +253,7 @@ object LocalPreferences {
|
|||||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||||
} else {
|
} 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.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||||
}
|
}
|
||||||
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||||
putString(PrefKeys.RELAYS, EventMapper.mapper.writeValueAsString(settings.localRelays))
|
|
||||||
|
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.DEFAULT_FILE_SERVER,
|
PrefKeys.DEFAULT_FILE_SERVER,
|
||||||
@@ -364,8 +367,8 @@ object LocalPreferences {
|
|||||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.localRelayServers.isNotEmpty()) {
|
if (settings.localRelayServers.value.isNotEmpty()) {
|
||||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
|
||||||
} else {
|
} else {
|
||||||
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
||||||
}
|
}
|
||||||
@@ -406,6 +409,33 @@ object LocalPreferences {
|
|||||||
remove(PrefKeys.LATEST_CHANNEL_LIST)
|
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) {
|
if (settings.backupEphemeralChatList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.LATEST_EPHEMERAL_LIST,
|
PrefKeys.LATEST_EPHEMERAL_LIST,
|
||||||
@@ -453,9 +483,8 @@ object LocalPreferences {
|
|||||||
prefs: SharedPreferences = encryptedPreferences(),
|
prefs: SharedPreferences = encryptedPreferences(),
|
||||||
) {
|
) {
|
||||||
Log.d("LocalPreferences", "Saving to shared settings")
|
Log.d("LocalPreferences", "Saving to shared settings")
|
||||||
with(prefs.edit()) {
|
prefs.edit {
|
||||||
putString(PrefKeys.SHARED_SETTINGS, EventMapper.mapper.writeValueAsString(sharedSettings))
|
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
|
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||||
|
|
||||||
val defaultHomeFollowList =
|
val defaultHomeFollowList =
|
||||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS
|
||||||
val defaultStoriesFollowList =
|
val defaultStoriesFollowList =
|
||||||
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
val defaultNotificationFollowList =
|
val defaultNotificationFollowList =
|
||||||
@@ -522,8 +551,6 @@ object LocalPreferences {
|
|||||||
val defaultDiscoveryFollowList =
|
val defaultDiscoveryFollowList =
|
||||||
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
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 zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||||
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
|
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 latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
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 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 hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||||
@@ -559,14 +589,13 @@ object LocalPreferences {
|
|||||||
keyPair = keyPair,
|
keyPair = keyPair,
|
||||||
transientAccount = false,
|
transientAccount = false,
|
||||||
externalSignerPackageName = externalSignerPackageName,
|
externalSignerPackageName = externalSignerPackageName,
|
||||||
localRelays = localRelays,
|
localRelayServers = MutableStateFlow(localRelayServers),
|
||||||
localRelayServers = localRelayServers,
|
|
||||||
defaultFileServer = defaultFileServer,
|
defaultFileServer = defaultFileServer,
|
||||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||||
zapPaymentRequest = zapPaymentRequestServer,
|
zapPaymentRequest = zapPaymentRequestServer?.normalize(),
|
||||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||||
@@ -579,6 +608,9 @@ object LocalPreferences {
|
|||||||
backupMuteList = latestMuteList,
|
backupMuteList = latestMuteList,
|
||||||
backupAppSpecificData = latestAppSpecificData,
|
backupAppSpecificData = latestAppSpecificData,
|
||||||
backupChannelList = latestChannelList,
|
backupChannelList = latestChannelList,
|
||||||
|
backupCommunityList = latestCommunityList,
|
||||||
|
backupHashtagList = latestHashtagList,
|
||||||
|
backupGeohashList = latestGeohashList,
|
||||||
backupEphemeralChatList = latestEphemeralList,
|
backupEphemeralChatList = latestEphemeralList,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
torSettings = TorSettingsFlow.build(torSettings),
|
||||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
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.actions.mediaServers.ServerName
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
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.edits.PrivateOutboxRelayListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.nip47WalletConnect.Nip47WalletConnect
|
||||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
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.ExternalSignerLauncher
|
||||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
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 com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -67,38 +69,29 @@ val DefaultChannelSet =
|
|||||||
val DefaultChannels =
|
val DefaultChannels =
|
||||||
listOf(
|
listOf(
|
||||||
// Anigma's Nostr
|
// Anigma's Nostr
|
||||||
EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", "wss://nos.lol"),
|
EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
|
||||||
// Amethyst's Group
|
// Amethyst's Group
|
||||||
EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "wss://nos.lol"),
|
EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
|
||||||
|
|
||||||
val DefaultNIP65List =
|
val DefaultNIP65List =
|
||||||
listOf(
|
listOf(
|
||||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH),
|
||||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nos.lol/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH),
|
||||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH),
|
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
|
||||||
)
|
)
|
||||||
|
|
||||||
val DefaultDMRelayList =
|
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
|
||||||
listOf(
|
|
||||||
RelayUrlFormatter.normalize("wss://auth.nostr1.com"),
|
|
||||||
RelayUrlFormatter.normalize("wss://relay.0xchat.com"),
|
|
||||||
RelayUrlFormatter.normalize("wss://nos.lol"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val DefaultSearchRelayList =
|
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
|
||||||
listOf(
|
|
||||||
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
|
|
||||||
RelayUrlFormatter.normalize("wss://nostr.wine"),
|
|
||||||
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
|
|
||||||
RelayUrlFormatter.normalize("wss://search.nos.today"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val GLOBAL_FOLLOWS = " Global "
|
val GLOBAL_FOLLOWS = " Global "
|
||||||
|
|
||||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
// 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.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val AROUND_ME = " Around Me "
|
val AROUND_ME = " Around Me "
|
||||||
@@ -108,14 +101,13 @@ class AccountSettings(
|
|||||||
val keyPair: KeyPair,
|
val keyPair: KeyPair,
|
||||||
val transientAccount: Boolean = false,
|
val transientAccount: Boolean = false,
|
||||||
var externalSignerPackageName: String? = null,
|
var externalSignerPackageName: String? = null,
|
||||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
var localRelayServers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
|
||||||
var localRelayServers: Set<String> = setOf(),
|
|
||||||
var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0],
|
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 defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
val defaultDiscoveryFollowList: 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 hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
var hideNIP17WarningDialog: Boolean = false,
|
var hideNIP17WarningDialog: Boolean = false,
|
||||||
@@ -128,6 +120,9 @@ class AccountSettings(
|
|||||||
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
||||||
var backupAppSpecificData: AppSpecificDataEvent? = null,
|
var backupAppSpecificData: AppSpecificDataEvent? = null,
|
||||||
var backupChannelList: ChannelListEvent? = null,
|
var backupChannelList: ChannelListEvent? = null,
|
||||||
|
var backupCommunityList: CommunityListEvent? = null,
|
||||||
|
var backupHashtagList: HashtagListEvent? = null,
|
||||||
|
var backupGeohashList: GeohashListEvent? = null,
|
||||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
||||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||||
@@ -200,7 +195,7 @@ class AccountSettings(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?): Boolean {
|
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
|
||||||
if (zapPaymentRequest != newServer) {
|
if (zapPaymentRequest != newServer) {
|
||||||
zapPaymentRequest = newServer
|
zapPaymentRequest = newServer
|
||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
@@ -256,11 +251,11 @@ class AccountSettings(
|
|||||||
// proxy settings
|
// proxy settings
|
||||||
// ---
|
// ---
|
||||||
fun setTorSettings(newTorSettings: TorSettings): Boolean {
|
fun setTorSettings(newTorSettings: TorSettings): Boolean {
|
||||||
if (torSettings.update(newTorSettings)) {
|
return if (torSettings.update(newTorSettings)) {
|
||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
return true
|
true
|
||||||
} else {
|
} else {
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +296,8 @@ class AccountSettings(
|
|||||||
// ----
|
// ----
|
||||||
|
|
||||||
fun updateLocalRelayServers(servers: Set<String>) {
|
fun updateLocalRelayServers(servers: Set<String>) {
|
||||||
if (localRelayServers != servers) {
|
if (localRelayServers.value != servers) {
|
||||||
localRelayServers = servers
|
localRelayServers.update { servers }
|
||||||
saveAccountSettings()
|
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?) {
|
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
|
||||||
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
|
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
|
// attestations
|
||||||
// ---
|
// ---
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import android.util.Log
|
|||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
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.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.Nip19Parser
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -45,7 +45,7 @@ class AntiSpamFilter {
|
|||||||
|
|
||||||
fun isSpam(
|
fun isSpam(
|
||||||
event: Event,
|
event: Event,
|
||||||
relay: RelayBriefInfoCache.RelayBriefInfo?,
|
relay: NormalizedRelayUrl?,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
@@ -74,14 +74,14 @@ class AntiSpamFilter {
|
|||||||
) {
|
) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"Potential SPAM Message",
|
"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
|
// Log down offenders
|
||||||
logOffender(hash, event)
|
logOffender(hash, event)
|
||||||
|
|
||||||
if (relay != null) {
|
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))
|
flowSpam.tryEmit(AntiSpamState(this))
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.model
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
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.experimental.ephemChat.chat.RoomId
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
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.ATag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
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.nip19Bech32.toNEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
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.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.utils.Hex
|
import com.vitorpamplona.quartz.utils.Hex
|
||||||
import com.vitorpamplona.quartz.utils.LargeCache
|
import com.vitorpamplona.quartz.utils.LargeCache
|
||||||
@@ -51,7 +51,7 @@ class EphemeralChatChannel(
|
|||||||
|
|
||||||
override fun idDisplayNote() = idNote().toShortenHex()
|
override fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
|
||||||
override fun relays() = listOf(roomId.relayUrl)
|
override fun relays() = setOf(roomId.relayUrl)
|
||||||
|
|
||||||
override fun toBestDisplayName() = roomId.toDisplayKey()
|
override fun toBestDisplayName() = roomId.toDisplayKey()
|
||||||
|
|
||||||
@@ -68,17 +68,21 @@ class PublicChatChannel(
|
|||||||
) : Channel(idHex) {
|
) : Channel(idHex) {
|
||||||
var event: ChannelCreateEvent? = null
|
var event: ChannelCreateEvent? = null
|
||||||
var infoTags = EmptyTagList
|
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 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(
|
fun updateChannelInfo(
|
||||||
creator: User,
|
creator: User,
|
||||||
@@ -99,7 +103,7 @@ class PublicChatChannel(
|
|||||||
|
|
||||||
fun updateChannelInfo(
|
fun updateChannelInfo(
|
||||||
creator: User,
|
creator: User,
|
||||||
channelInfo: ChannelData,
|
channelInfo: ChannelDataNorm,
|
||||||
updatedAt: Long,
|
updatedAt: Long,
|
||||||
) {
|
) {
|
||||||
this.info = channelInfo
|
this.info = channelInfo
|
||||||
@@ -130,10 +134,12 @@ class LiveActivitiesChannel(
|
|||||||
|
|
||||||
fun address() = address
|
fun address() = address
|
||||||
|
|
||||||
override fun relays() = info?.allRelayUrls() ?: super.relays()
|
override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays()
|
||||||
|
|
||||||
fun relayHintUrl() = relays().firstOrNull()
|
fun relayHintUrl() = relays().firstOrNull()
|
||||||
|
|
||||||
|
fun relayHintUrls() = relays().take(3)
|
||||||
|
|
||||||
fun updateChannelInfo(
|
fun updateChannelInfo(
|
||||||
creator: User,
|
creator: User,
|
||||||
channelInfo: LiveActivitiesEvent,
|
channelInfo: LiveActivitiesEvent,
|
||||||
@@ -150,11 +156,10 @@ class LiveActivitiesChannel(
|
|||||||
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
|
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
|
||||||
|
|
||||||
override fun anyNameStartsWith(prefix: String): Boolean =
|
override fun anyNameStartsWith(prefix: String): Boolean =
|
||||||
listOfNotNull(info?.title(), info?.summary())
|
info?.title()?.contains(prefix, true) == true ||
|
||||||
.filter { it.contains(prefix, true) }
|
info?.summary()?.contains(prefix, true) == true
|
||||||
.isNotEmpty()
|
|
||||||
|
|
||||||
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())
|
fun toATag() = ATag(address, relayHintUrl())
|
||||||
|
|
||||||
@@ -173,7 +178,8 @@ abstract class Channel(
|
|||||||
var updatedMetadataAt: Long = 0
|
var updatedMetadataAt: Long = 0
|
||||||
val notes = LargeCache<HexKey, Note>()
|
val notes = LargeCache<HexKey, Note>()
|
||||||
var lastNoteCreatedAt: Long = 0
|
var lastNoteCreatedAt: Long = 0
|
||||||
private var relays = mapOf<RelayBriefInfoCache.RelayBriefInfo, Counter>()
|
|
||||||
|
private var relays = mapOf<NormalizedRelayUrl, Counter>()
|
||||||
|
|
||||||
open fun idNote() = Hex.decode(idHex).toNEvent()
|
open fun idNote() = Hex.decode(idHex).toNEvent()
|
||||||
|
|
||||||
@@ -187,13 +193,13 @@ abstract class Channel(
|
|||||||
|
|
||||||
open fun profilePicture(): String? = creator?.info?.banner
|
open fun profilePicture(): String? = creator?.info?.banner
|
||||||
|
|
||||||
open fun relays() =
|
open fun relays(): Set<NormalizedRelayUrl> =
|
||||||
relays.keys
|
relays.keys
|
||||||
.toSortedSet { o1, o2 ->
|
.toSortedSet { o1, o2 ->
|
||||||
val o1Count = relays[o1]?.number ?: 0
|
val o1Count = relays[o1]?.number ?: 0
|
||||||
val o2Count = relays[o2]?.number ?: 0
|
val o2Count = relays[o2]?.number ?: 0
|
||||||
o2Count.compareTo(o1Count) // descending
|
o2Count.compareTo(o1Count) // descending
|
||||||
}.map { it.url }
|
}
|
||||||
|
|
||||||
open fun updateChannelInfo(
|
open fun updateChannelInfo(
|
||||||
creator: User,
|
creator: User,
|
||||||
@@ -206,13 +212,13 @@ abstract class Channel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
|
fun addRelaySync(briefInfo: NormalizedRelayUrl) {
|
||||||
if (briefInfo !in relays) {
|
if (briefInfo !in relays) {
|
||||||
relays = relays + Pair(briefInfo, Counter(1))
|
relays = relays + Pair(briefInfo, Counter(1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRelay(relay: RelayBriefInfoCache.RelayBriefInfo) {
|
fun addRelay(relay: NormalizedRelayUrl) {
|
||||||
val counter = relays[relay]
|
val counter = relays[relay]
|
||||||
if (counter != null) {
|
if (counter != null) {
|
||||||
counter.number++
|
counter.number++
|
||||||
@@ -223,7 +229,7 @@ abstract class Channel(
|
|||||||
|
|
||||||
fun addNote(
|
fun addNote(
|
||||||
note: Note,
|
note: Note,
|
||||||
relay: RelayBriefInfoCache.RelayBriefInfo? = null,
|
relay: NormalizedRelayUrl? = null,
|
||||||
) {
|
) {
|
||||||
notes.put(note.idHex, note)
|
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.HashTag
|
||||||
import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
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.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderHashTagIconsPreview() {
|
fun RenderHashTagIconsPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
RenderRegular(
|
RenderRegular(
|
||||||
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
||||||
EmptyTagList,
|
EmptyTagList,
|
||||||
) { word, state ->
|
) { word, state ->
|
||||||
when (word) {
|
when (word) {
|
||||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav)
|
||||||
is RegularTextSegment -> Text(word.segmentText)
|
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.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
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.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||||
import com.vitorpamplona.amethyst.service.replace
|
import com.vitorpamplona.amethyst.service.replace
|
||||||
import com.vitorpamplona.amethyst.tryAndWait
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
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.addedRewardValue
|
||||||
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
|
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
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.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
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.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
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.Hex
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.containsAny
|
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.CancellationException
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -157,7 +158,7 @@ open class Note(
|
|||||||
var zapPayments = mapOf<Note, Note?>()
|
var zapPayments = mapOf<Note, Note?>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
var relays = listOf<NormalizedRelayUrl>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun id() = Hex.decode(idHex)
|
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
|
val authorRelay = author?.latestMetadataRelay
|
||||||
|
|
||||||
return if (relays.isNotEmpty()) {
|
return if (relays.isNotEmpty()) {
|
||||||
if (authorRelay != null && relays.any { it.url == authorRelay }) {
|
if (authorRelay != null && relays.any { it == authorRelay }) {
|
||||||
authorRelay
|
authorRelay
|
||||||
} else {
|
} else {
|
||||||
relays.firstOrNull()?.url
|
relays.firstOrNull()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -293,7 +306,7 @@ open class Note(
|
|||||||
zaps = mapOf<Note, Note?>()
|
zaps = mapOf<Note, Note?>()
|
||||||
zapPayments = mapOf<Note, Note?>()
|
zapPayments = mapOf<Note, Note?>()
|
||||||
zapsAmount = BigDecimal.ZERO
|
zapsAmount = BigDecimal.ZERO
|
||||||
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
relays = listOf<NormalizedRelayUrl>()
|
||||||
|
|
||||||
if (repliesChanged) flowSet?.replies?.invalidateData()
|
if (repliesChanged) flowSet?.replies?.invalidateData()
|
||||||
if (reactionsChanged) flowSet?.reactions?.invalidateData()
|
if (reactionsChanged) flowSet?.reactions?.invalidateData()
|
||||||
@@ -445,17 +458,17 @@ open class Note(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) {
|
fun addRelaySync(relay: NormalizedRelayUrl) {
|
||||||
if (briefInfo !in relays) {
|
if (relay !in relays) {
|
||||||
relays = relays + briefInfo
|
relays = relays + relay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasRelay(relay: RelayBriefInfoCache.RelayBriefInfo) = relay !in relays
|
fun hasRelay(relay: NormalizedRelayUrl) = relay !in relays
|
||||||
|
|
||||||
fun addRelay(brief: RelayBriefInfoCache.RelayBriefInfo) {
|
fun addRelay(relay: NormalizedRelayUrl) {
|
||||||
if (brief !in relays) {
|
if (relay !in relays) {
|
||||||
addRelaySync(brief)
|
addRelaySync(relay)
|
||||||
flowSet?.relays?.invalidateData()
|
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 reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
|
||||||
|
|
||||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||||
return boosts.firstOrNull {
|
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
|
||||||
it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo()
|
return boosts.any {
|
||||||
} != null // 5 minute protection
|
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 }
|
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
|
||||||
@@ -823,7 +844,7 @@ open class Note(
|
|||||||
zapsAmount = BigDecimal.ZERO
|
zapsAmount = BigDecimal.ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
fun isHiddenFor(accountChoices: HiddenUsersState.LiveHiddenUsers): Boolean {
|
||||||
val thisEvent = event ?: return false
|
val thisEvent = event ?: return false
|
||||||
val hash = thisEvent.pubKey.hashCode()
|
val hash = thisEvent.pubKey.hashCode()
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache.RelayBriefInfo
|
|
||||||
import com.vitorpamplona.quartz.lightning.Lud06
|
import com.vitorpamplona.quartz.lightning.Lud06
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
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.geohash.isTaggedGeoHash
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
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.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
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.entities.NProfile
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||||
@@ -57,7 +58,7 @@ class User(
|
|||||||
var info: UserMetadata? = null
|
var info: UserMetadata? = null
|
||||||
|
|
||||||
var latestMetadata: MetadataEvent? = null
|
var latestMetadata: MetadataEvent? = null
|
||||||
var latestMetadataRelay: String? = null
|
var latestMetadataRelay: NormalizedRelayUrl? = null
|
||||||
var latestContactList: ContactListEvent? = null
|
var latestContactList: ContactListEvent? = null
|
||||||
var latestBookmarkList: BookmarkListEvent? = null
|
var latestBookmarkList: BookmarkListEvent? = null
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ class User(
|
|||||||
var zaps = mapOf<Note, Note?>()
|
var zaps = mapOf<Note, Note?>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var relaysBeingUsed = mapOf<String, RelayInfo>()
|
var relaysBeingUsed = mapOf<NormalizedRelayUrl, RelayInfo>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var privateChatrooms = mapOf<ChatroomKey, Chatroom>()
|
var privateChatrooms = mapOf<ChatroomKey, Chatroom>()
|
||||||
@@ -79,13 +80,21 @@ class User(
|
|||||||
|
|
||||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
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 authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
|
||||||
|
|
||||||
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
|
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())
|
fun toPTag() = PTag(pubkeyHex, bestRelayHint())
|
||||||
|
|
||||||
@@ -289,12 +298,12 @@ class User(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addRelayBeingUsed(
|
fun addRelayBeingUsed(
|
||||||
relay: RelayBriefInfo,
|
relay: NormalizedRelayUrl,
|
||||||
eventTime: Long,
|
eventTime: Long,
|
||||||
) {
|
) {
|
||||||
val here = relaysBeingUsed[relay.url]
|
val here = relaysBeingUsed[relay]
|
||||||
if (here == null) {
|
if (here == null) {
|
||||||
relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1))
|
relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1))
|
||||||
} else {
|
} else {
|
||||||
if (eventTime > here.lastEvent) {
|
if (eventTime > here.lastEvent) {
|
||||||
here.lastEvent = eventTime
|
here.lastEvent = eventTime
|
||||||
@@ -345,8 +354,7 @@ class User(
|
|||||||
type: ReportType,
|
type: ReportType,
|
||||||
): Boolean =
|
): Boolean =
|
||||||
reports[loggedIn]?.firstOrNull {
|
reports[loggedIn]?.firstOrNull {
|
||||||
it.event is ReportEvent &&
|
(it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false
|
||||||
(it.event as ReportEvent).reportedAuthor().any { it.type == type }
|
|
||||||
} != null
|
} != null
|
||||||
|
|
||||||
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||||
@@ -445,7 +453,7 @@ class UserFlowSet(
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RelayInfo(
|
data class RelayInfo(
|
||||||
val url: String,
|
val url: NormalizedRelayUrl,
|
||||||
var lastEvent: Long,
|
var lastEvent: Long,
|
||||||
var counter: 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
|
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.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.NoteState
|
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.chat.RoomId
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class EphemeralChatListState(
|
class EphemeralChatListState(
|
||||||
val signer: NostrSigner,
|
val signer: NostrSigner,
|
||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
|
val settings: AccountSettings,
|
||||||
) {
|
) {
|
||||||
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
|
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
|
||||||
|
|
||||||
@@ -52,31 +59,27 @@ class EphemeralChatListState(
|
|||||||
|
|
||||||
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveEphemeralChatList: StateFlow<Set<RoomId>> by lazy {
|
val liveEphemeralChatList: StateFlow<Set<RoomId>> by lazy {
|
||||||
getEphemeralChatListFlow()
|
getEphemeralChatListFlow()
|
||||||
.transformLatest { noteState ->
|
.transformLatest { noteState ->
|
||||||
val set =
|
emit(ephemeralChatListWithBackup(noteState.note))
|
||||||
tryAndWait { continuation ->
|
|
||||||
(noteState.note.event as? EphemeralChatListEvent)?.publicAndPrivateRoomIds(signer) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (set != null) {
|
|
||||||
emit(set)
|
|
||||||
}
|
|
||||||
}.onStart {
|
}.onStart {
|
||||||
val set =
|
emit(ephemeralChatListWithBackup(getEphemeralChatListNote()))
|
||||||
tryAndWait { continuation ->
|
|
||||||
getEphemeralChatList()?.publicAndPrivateRoomIds(signer) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (set != null) {
|
|
||||||
emit(set)
|
|
||||||
}
|
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
@@ -89,6 +92,7 @@ class EphemeralChatListState(
|
|||||||
channel: EphemeralChatChannel,
|
channel: EphemeralChatChannel,
|
||||||
onDone: (EphemeralChatListEvent) -> Unit,
|
onDone: (EphemeralChatListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (!signer.isWriteable()) return
|
||||||
val ephemeralChatList = getEphemeralChatList()
|
val ephemeralChatList = getEphemeralChatList()
|
||||||
|
|
||||||
if (ephemeralChatList == null) {
|
if (ephemeralChatList == null) {
|
||||||
@@ -113,6 +117,7 @@ class EphemeralChatListState(
|
|||||||
channel: EphemeralChatChannel,
|
channel: EphemeralChatChannel,
|
||||||
onDone: (EphemeralChatListEvent) -> Unit,
|
onDone: (EphemeralChatListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (!signer.isWriteable()) return
|
||||||
val ephemeralChatList = getEphemeralChatList()
|
val ephemeralChatList = getEphemeralChatList()
|
||||||
|
|
||||||
if (ephemeralChatList != null) {
|
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
|
* 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.
|
* 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.model.localRelays.LocalRelayListState
|
||||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
/**
|
class NotificationInboxRelayState(
|
||||||
* There should be only one instance of the Tor binding per app.
|
nip65RelayList: Nip65RelayListState,
|
||||||
*
|
localRelayList: LocalRelayListState,
|
||||||
* Tor will connect as soon as status is listened to.
|
|
||||||
*/
|
|
||||||
class RelayManager(
|
|
||||||
app: Application,
|
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
torManager: TorManager,
|
|
||||||
connManager: ConnectivityManager,
|
|
||||||
) {
|
) {
|
||||||
val relayService =
|
val flow =
|
||||||
combine(
|
combine(
|
||||||
torManager.status,
|
nip65RelayList.inboxFlow,
|
||||||
connManager.status,
|
localRelayList.flow,
|
||||||
) { torStatus, connManager ->
|
) { nip65Inbox, localRelays ->
|
||||||
}
|
nip65Inbox + localRelays
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
val status: StateFlow<RelayServiceStatus> =
|
.stateIn(
|
||||||
RelayService(app).status.stateIn(
|
scope,
|
||||||
scope,
|
SharingStarted.Eagerly,
|
||||||
SharingStarted.WhileSubscribed(30000),
|
nip65RelayList.inboxFlow.value + localRelayList.flow.value,
|
||||||
RelayServiceStatus.Off,
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
+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
|
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.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.NoteState
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.tryAndWait
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
@@ -39,12 +44,14 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class PublicChatListState(
|
class PublicChatListState(
|
||||||
val signer: NostrSigner,
|
val signer: NostrSigner,
|
||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
|
val settings: AccountSettings,
|
||||||
) {
|
) {
|
||||||
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
|
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
|
||||||
|
|
||||||
@@ -54,31 +61,27 @@ class PublicChatListState(
|
|||||||
|
|
||||||
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val livePublicChatList: StateFlow<Set<EventIdHint>> by lazy {
|
val flow: StateFlow<Set<EventIdHint>> by lazy {
|
||||||
getChannelListFlow()
|
getChannelListFlow()
|
||||||
.transformLatest { noteState ->
|
.transformLatest { noteState ->
|
||||||
val set =
|
emit(publicChatListWithBackup(noteState.note))
|
||||||
tryAndWait { continuation ->
|
|
||||||
(noteState.note.event as? ChannelListEvent)?.publicAndPrivateChannels(signer) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (set != null) {
|
|
||||||
emit(set)
|
|
||||||
}
|
|
||||||
}.onStart {
|
}.onStart {
|
||||||
val set =
|
emit(publicChatListWithBackup(getChannelListNote()))
|
||||||
tryAndWait { continuation ->
|
|
||||||
getChannelList()?.publicAndPrivateChannels(signer) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (set != null) {
|
|
||||||
emit(set)
|
|
||||||
}
|
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
@@ -88,8 +91,8 @@ class PublicChatListState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val livePublicChatEventIdSet: StateFlow<Set<HexKey>> by lazy {
|
val flowSet: StateFlow<Set<HexKey>> by lazy {
|
||||||
livePublicChatList
|
flow
|
||||||
.map {
|
.map {
|
||||||
it.mapTo(mutableSetOf()) { it.eventId }
|
it.mapTo(mutableSetOf()) { it.eventId }
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
@@ -104,6 +107,7 @@ class PublicChatListState(
|
|||||||
channel: PublicChatChannel,
|
channel: PublicChatChannel,
|
||||||
onDone: (ChannelListEvent) -> Unit,
|
onDone: (ChannelListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (!signer.isWriteable()) return
|
||||||
val publicChatList = getChannelList()
|
val publicChatList = getChannelList()
|
||||||
|
|
||||||
val fullHint = channel.toEventHint()
|
val fullHint = channel.toEventHint()
|
||||||
@@ -127,6 +131,7 @@ class PublicChatListState(
|
|||||||
channels: List<PublicChatChannel>,
|
channels: List<PublicChatChannel>,
|
||||||
onDone: (ChannelListEvent) -> Unit,
|
onDone: (ChannelListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (!signer.isWriteable()) return
|
||||||
val publicChatList = getChannelList()
|
val publicChatList = getChannelList()
|
||||||
|
|
||||||
val partialHint = channels.map { it.toEventId() }
|
val partialHint = channels.map { it.toEventId() }
|
||||||
@@ -141,10 +146,33 @@ class PublicChatListState(
|
|||||||
channel: PublicChatChannel,
|
channel: PublicChatChannel,
|
||||||
onDone: (ChannelListEvent) -> Unit,
|
onDone: (ChannelListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (!signer.isWriteable()) return
|
||||||
val publicChatList = getChannelList()
|
val publicChatList = getChannelList()
|
||||||
|
|
||||||
if (publicChatList != null) {
|
if (publicChatList != null) {
|
||||||
ChannelListEvent.removeChannel(publicChatList, channel.idHex, signer, onReady = onDone)
|
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.commons.richtext.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.NoteState
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||||
@@ -67,7 +68,7 @@ class EmojiPackState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveEmojiSelectionPack: StateFlow<List<StateFlow<NoteState>>?> by lazy {
|
val flow: StateFlow<List<StateFlow<NoteState>>?> by lazy {
|
||||||
getEmojiPackSelectionFlow()
|
getEmojiPackSelectionFlow()
|
||||||
.transformLatest {
|
.transformLatest {
|
||||||
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
|
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
|
||||||
@@ -98,7 +99,7 @@ class EmojiPackState(
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val myEmojis by lazy {
|
val myEmojis by lazy {
|
||||||
liveEmojiSelectionPack
|
flow
|
||||||
.transformLatest { emojiList ->
|
.transformLatest { emojiList ->
|
||||||
if (emojiList != null) {
|
if (emojiList != null) {
|
||||||
emitAll(
|
emitAll(
|
||||||
@@ -116,4 +117,40 @@ class EmojiPackState(
|
|||||||
mergePack(convertEmojiSelectionPack(getEmojiPackSelection())?.map { it.value }?.toTypedArray() ?: emptyArray()),
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
@@ -51,10 +52,8 @@ class LatestByKindAndAuthor<T : Event>(
|
|||||||
if ((kind in 10000..19999) || (kind in 30000..39999)) {
|
if ((kind in 10000..19999) || (kind in 30000..39999)) {
|
||||||
LocalCache.addressables
|
LocalCache.addressables
|
||||||
.maxOrNullOf(
|
.maxOrNullOf(
|
||||||
filter = { idHex: String, note: AddressableNote ->
|
filter = { address: Address, note: AddressableNote ->
|
||||||
note.event?.let {
|
address.kind == kind && address.pubKeyHex == pubkey
|
||||||
it.kind == kind && it.pubKey == pubkey
|
|
||||||
} == true
|
|
||||||
},
|
},
|
||||||
comparator = CreatedAtComparatorAddresses,
|
comparator = CreatedAtComparatorAddresses,
|
||||||
)?.event as? T
|
)?.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
|
* 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.
|
* 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 {
|
interface IFeedFlowsType {
|
||||||
data class Active(
|
fun flow(): Flow<IFeedTopNavFilter>
|
||||||
val client: NostrClient,
|
|
||||||
) : RelayServiceStatus()
|
|
||||||
|
|
||||||
object Off : RelayServiceStatus()
|
suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>)
|
||||||
|
|
||||||
object Connecting : RelayServiceStatus()
|
|
||||||
}
|
}
|
||||||
+9
-13
@@ -18,23 +18,19 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* 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.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface IPerRelayFilter {
|
interface IFeedTopNavFilter {
|
||||||
fun toRelay(forRelay: String): Filter
|
fun matchAuthor(pubkey: HexKey): Boolean
|
||||||
|
|
||||||
fun toJson(forRelay: String): String
|
fun match(noteEvent: Event): Boolean
|
||||||
|
|
||||||
fun match(
|
fun toPerRelayFlow(cache: LocalCache): Flow<IFeedTopNavPerRelayFilterSet>
|
||||||
event: Event,
|
|
||||||
forRelay: String,
|
|
||||||
): Boolean
|
|
||||||
|
|
||||||
fun toDebugJson(): String
|
fun startValue(cache: LocalCache): IFeedTopNavPerRelayFilterSet
|
||||||
|
|
||||||
// This only exists because some relays confuse empty lists with null lists
|
|
||||||
fun isValidFor(url: String): Boolean
|
|
||||||
}
|
}
|
||||||
+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
|
* 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.
|
* 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 androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.ammolite.relays.FeedType
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a big OR filter.
|
||||||
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Kind3BasicRelaySetupInfo(
|
class AllFollowsByOutboxTopNavPerRelayFilter(
|
||||||
val url: String,
|
val authors: Set<String>? = null,
|
||||||
val read: Boolean,
|
val hashtags: Set<String>? = null,
|
||||||
val write: Boolean,
|
val geotags: Set<String>? = null,
|
||||||
val feedTypes: Set<FeedType>,
|
val communities: Set<String>? = null,
|
||||||
val relayStat: RelayStat,
|
) : IFeedTopNavPerRelayFilter {
|
||||||
val paidRelay: Boolean = false,
|
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
|
||||||
) {
|
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
|
||||||
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
|
|
||||||
}
|
}
|
||||||
+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
|
* 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.
|
* 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 com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||||
import androidx.compose.runtime.Immutable
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
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 {
|
class GlobalFeedFlow(
|
||||||
val cache = LruCache<String, RelayBriefInfo?>(50)
|
val relays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||||
|
) : IFeedFlowsType {
|
||||||
|
val default = GlobalTopNavFilter(relays)
|
||||||
|
|
||||||
@Immutable
|
override fun flow() = MutableStateFlow(default)
|
||||||
class RelayBriefInfo(
|
|
||||||
val url: String,
|
|
||||||
) {
|
|
||||||
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern()
|
|
||||||
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(url: String): RelayBriefInfo {
|
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||||
val info = cache[url]
|
collector.emit(default)
|
||||||
if (info != null) return info
|
|
||||||
|
|
||||||
val newInfo = RelayBriefInfo(url)
|
|
||||||
cache.put(url, newInfo)
|
|
||||||
return newInfo
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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
|
* 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.
|
* 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(
|
@Immutable
|
||||||
val types: Set<FeedType>,
|
object GlobalTopNavPerRelayFilter : IFeedTopNavPerRelayFilter
|
||||||
val filter: IPerRelayFilter,
|
|
||||||
) {
|
|
||||||
// This only exists because some relays confuse empty lists with null lists
|
|
||||||
fun isValidFor(url: String) = filter.isValidFor(url)
|
|
||||||
}
|
|
||||||
+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
|
* 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.
|
* 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 androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RelaySetupInfo(
|
class HashtagTopNavPerRelayFilter(
|
||||||
val url: String,
|
val hashtags: Set<String>,
|
||||||
val read: Boolean,
|
) : IFeedTopNavPerRelayFilter {
|
||||||
val write: Boolean,
|
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
|
||||||
val feedTypes: Set<FeedType>,
|
}
|
||||||
)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class RelaySetupInfoToConnect(
|
|
||||||
val url: String,
|
|
||||||
val forceProxy: Boolean,
|
|
||||||
val read: Boolean,
|
|
||||||
val write: Boolean,
|
|
||||||
val feedTypes: Set<FeedType>,
|
|
||||||
)
|
|
||||||
+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
|
* 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.
|
* 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 androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.ammolite.relays.FeedType
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
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.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Kind3RelayProposalSetupInfo(
|
class UnknownTopNavFilter(
|
||||||
val url: String,
|
val feedName: String,
|
||||||
val read: Boolean,
|
) : IFeedTopNavFilter {
|
||||||
val write: Boolean,
|
override fun matchAuthor(pubkey: HexKey) = false
|
||||||
val feedTypes: Set<FeedType>,
|
|
||||||
val relayStat: RelayStat,
|
override fun match(noteEvent: Event) = false
|
||||||
val paidRelay: Boolean = false,
|
|
||||||
val users: List<HexKey>,
|
override fun toPerRelayFlow(cache: LocalCache): Flow<UnknownTopNavPerRelayFilterSet> {
|
||||||
) {
|
return MutableStateFlow(UnknownTopNavPerRelayFilterSet)
|
||||||
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
|
}
|
||||||
|
|
||||||
|
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
|
* 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.
|
* 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 {
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||||
// Websocket connected
|
|
||||||
CONNECTED,
|
|
||||||
|
|
||||||
// Websocket disconnecting
|
object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet
|
||||||
DISCONNECTING,
|
|
||||||
|
|
||||||
// Websocket disconnected
|
|
||||||
DISCONNECTED,
|
|
||||||
}
|
|
||||||
+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.Log
|
||||||
import android.util.LruCache
|
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.nip11RelayInfo.Nip11RelayInformation
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
@@ -49,25 +50,23 @@ object Nip11CachedRetriever {
|
|||||||
|
|
||||||
class RetrieveResultLoading : RetrieveResult(TimeUtils.now())
|
class RetrieveResultLoading : RetrieveResult(TimeUtils.now())
|
||||||
|
|
||||||
private val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
|
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(100)
|
||||||
private val retriever = Nip11Retriever()
|
private val retriever = Nip11Retriever()
|
||||||
|
|
||||||
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
|
fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation? {
|
||||||
val result = relayInformationDocumentCache.get(RelayUrlFormatter.getHttpsUrl(dirtyUrl)) ?: return null
|
val result = relayInformationDocumentCache.get(relay) ?: return null
|
||||||
if (result is RetrieveResultSuccess) return result.data
|
if (result is RetrieveResultSuccess) return result.data
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
dirtyUrl: String,
|
relay: NormalizedRelayUrl,
|
||||||
okHttpClient: (String) -> OkHttpClient,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
val url = RelayUrlFormatter.getHttpsUrl(dirtyUrl)
|
val doc = relayInformationDocumentCache.get(relay)
|
||||||
val doc = relayInformationDocumentCache.get(url)
|
|
||||||
|
|
||||||
if (doc != null) {
|
if (doc != null) {
|
||||||
if (doc is RetrieveResultSuccess) {
|
if (doc is RetrieveResultSuccess) {
|
||||||
onInfo(doc.data)
|
onInfo(doc.data)
|
||||||
@@ -75,41 +74,37 @@ object Nip11CachedRetriever {
|
|||||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
||||||
// just wait.
|
// just wait.
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
retrieve(relay, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
} else if (doc is RetrieveResultError) {
|
} else if (doc is RetrieveResultError) {
|
||||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
||||||
onError(dirtyUrl, doc.error, null)
|
onError(relay, doc.error, null)
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
retrieve(relay, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
retrieve(relay, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun retrieve(
|
private suspend fun retrieve(
|
||||||
url: String,
|
relay: NormalizedRelayUrl,
|
||||||
dirtyUrl: String,
|
|
||||||
okHttpClient: (String) -> OkHttpClient,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
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(
|
retriever.loadRelayInfo(
|
||||||
url = url,
|
relay = relay,
|
||||||
dirtyUrl = dirtyUrl,
|
|
||||||
okHttpClient = okHttpClient,
|
okHttpClient = okHttpClient,
|
||||||
onInfo = {
|
onInfo = {
|
||||||
checkNotInMainThread()
|
relayInformationDocumentCache.put(relay, RetrieveResultSuccess(it))
|
||||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
|
||||||
onInfo(it)
|
onInfo(it)
|
||||||
},
|
},
|
||||||
onError = { dirtyUrl, code, errorMsg ->
|
onError = { relay, code, errorMsg ->
|
||||||
checkNotInMainThread()
|
relayInformationDocumentCache.put(relay, RetrieveResultError(code, errorMsg))
|
||||||
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
onError(relay, code, errorMsg)
|
||||||
onError(url, code, errorMsg)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -124,13 +119,13 @@ class Nip11Retriever {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
url: String,
|
relay: NormalizedRelayUrl,
|
||||||
dirtyUrl: String,
|
|
||||||
okHttpClient: (String) -> OkHttpClient,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, ErrorCode, String?) -> Unit,
|
onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
val url = relay.toHttp()
|
||||||
try {
|
try {
|
||||||
val request: Request =
|
val request: Request =
|
||||||
Request
|
Request
|
||||||
@@ -154,16 +149,16 @@ class Nip11Retriever {
|
|||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
onInfo(Nip11RelayInformation.fromJson(body))
|
onInfo(Nip11RelayInformation.fromJson(body))
|
||||||
} else {
|
} else {
|
||||||
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.e(
|
Log.e(
|
||||||
"RelayInfoFail",
|
"RelayInfoFail",
|
||||||
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
"Resulting Message from Relay ${relay.url} in not parseable: $body",
|
||||||
e,
|
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,
|
call: Call,
|
||||||
e: IOException,
|
e: IOException,
|
||||||
) {
|
) {
|
||||||
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
|
Log.e("RelayInfoFail", "${relay.url} unavailable", e)
|
||||||
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
|
onError(relay, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e)
|
||||||
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.collectSuccessfulOperations
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.nip47WalletConnect.PayInvoiceErrorResponse
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
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.nip57Zaps.splits.zapSplitSetup
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.collectSuccessfulOperations
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -173,6 +175,16 @@ class ZapPaymentHandler(
|
|||||||
val user: User? = null,
|
val user: User? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun receivingRelaySet(userHex: HexKey): Set<NormalizedRelayUrl>? {
|
||||||
|
return (
|
||||||
|
LocalCache
|
||||||
|
.getAddressableNoteIfExists(
|
||||||
|
AdvertisedRelayListEvent.createAddressTag(userHex),
|
||||||
|
)?.event as? AdvertisedRelayListEvent
|
||||||
|
)
|
||||||
|
?.readRelaysNorm()?.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun signAllZapRequests(
|
suspend fun signAllZapRequests(
|
||||||
note: Note,
|
note: Note,
|
||||||
pollOption: Int?,
|
pollOption: Int?,
|
||||||
@@ -181,18 +193,6 @@ class ZapPaymentHandler(
|
|||||||
zapsToSend: List<BaseZapSplitSetup>,
|
zapsToSend: List<BaseZapSplitSetup>,
|
||||||
onAllDone: suspend (List<ZapRequestReady>) -> Unit,
|
onAllDone: suspend (List<ZapRequestReady>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val authorRelayList =
|
|
||||||
note.author
|
|
||||||
?.pubkeyHex
|
|
||||||
?.let {
|
|
||||||
(
|
|
||||||
LocalCache
|
|
||||||
.getAddressableNoteIfExists(
|
|
||||||
AdvertisedRelayListEvent.createAddressTag(it),
|
|
||||||
)?.event as? AdvertisedRelayListEvent?
|
|
||||||
)?.readRelays()
|
|
||||||
}?.toSet()
|
|
||||||
|
|
||||||
collectSuccessfulOperations<BaseZapSplitSetup, ZapRequestReady>(
|
collectSuccessfulOperations<BaseZapSplitSetup, ZapRequestReady>(
|
||||||
items = zapsToSend,
|
items = zapsToSend,
|
||||||
runRequestFor = { next: BaseZapSplitSetup, onReady ->
|
runRequestFor = { next: BaseZapSplitSetup, onReady ->
|
||||||
@@ -203,18 +203,12 @@ class ZapPaymentHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (next is ZapSplitSetup) {
|
} else if (next is ZapSplitSetup) {
|
||||||
val user = LocalCache.getUserIfExists(next.pubKeyHex)
|
val authorRelayList = note.author?.let { receivingRelaySet(it.pubkeyHex) } ?: emptySet()
|
||||||
val userRelayList =
|
val userRelayList = receivingRelaySet(next.pubKeyHex) ?: emptySet()
|
||||||
(
|
|
||||||
(
|
|
||||||
LocalCache
|
|
||||||
.getAddressableNoteIfExists(
|
|
||||||
AdvertisedRelayListEvent.createAddressTag(next.pubKeyHex),
|
|
||||||
)?.event as? AdvertisedRelayListEvent?
|
|
||||||
)?.readRelays()?.toSet() ?: emptySet()
|
|
||||||
) + (authorRelayList ?: 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))
|
onReady(ZapRequestReady(next, zapRequestJson, user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +381,7 @@ class ZapPaymentHandler(
|
|||||||
message: String,
|
message: String,
|
||||||
zapType: LnZapEvent.ZapType,
|
zapType: LnZapEvent.ZapType,
|
||||||
overrideUser: User? = null,
|
overrideUser: User? = null,
|
||||||
additionalRelays: Set<String>? = null,
|
additionalRelays: Set<NormalizedRelayUrl>? = null,
|
||||||
onReady: (String?) -> Unit,
|
onReady: (String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
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.Amethyst
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.launchAndWaitAll
|
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
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.nip42RelayAuth.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -51,7 +52,7 @@ class RegisterAccounts(
|
|||||||
|
|
||||||
private suspend fun signAllAuths(
|
private suspend fun signAllAuths(
|
||||||
notificationToken: String,
|
notificationToken: String,
|
||||||
remainingTos: List<Pair<AccountSettings, List<String>>>,
|
remainingTos: List<Pair<AccountSettings, List<NormalizedRelayUrl>>>,
|
||||||
output: MutableList<RelayAuthEvent>,
|
output: MutableList<RelayAuthEvent>,
|
||||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -99,15 +100,15 @@ class RegisterAccounts(
|
|||||||
|
|
||||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||||
if (acc != null && acc.isWriteable()) {
|
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(", ")}")
|
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(", ")}")
|
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(", ")}")
|
Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -39,7 +39,7 @@ class DualHttpClientManager(
|
|||||||
) {
|
) {
|
||||||
val factory = OkHttpClientFactory(keyCache)
|
val factory = OkHttpClientFactory(keyCache)
|
||||||
|
|
||||||
private val defaultHttpClient: StateFlow<OkHttpClient> =
|
val defaultHttpClient: StateFlow<OkHttpClient> =
|
||||||
combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile ->
|
combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile ->
|
||||||
factory.buildHttpClient(proxy, mobile, userAgent)
|
factory.buildHttpClient(proxy, mobile, userAgent)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
@@ -48,7 +48,7 @@ class DualHttpClientManager(
|
|||||||
factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent),
|
factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val defaultHttpClientWithoutProxy: StateFlow<OkHttpClient> =
|
val defaultHttpClientWithoutProxy: StateFlow<OkHttpClient> =
|
||||||
isMobileDataProvider
|
isMobileDataProvider
|
||||||
.map { mobile ->
|
.map { mobile ->
|
||||||
factory.buildHttpClient(mobile, userAgent)
|
factory.buildHttpClient(mobile, userAgent)
|
||||||
|
|||||||
+36
-23
@@ -20,27 +20,50 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.okhttp
|
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.WebSocket
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class OkHttpWebSocket(
|
class OkHttpWebSocket(
|
||||||
val url: String,
|
val url: NormalizedRelayUrl,
|
||||||
val forceProxy: Boolean,
|
val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient,
|
||||||
val httpClient: (url: String, forceProxy: Boolean) -> OkHttpClient,
|
|
||||||
val out: WebSocketListener,
|
val out: WebSocketListener,
|
||||||
) : WebSocket {
|
) : WebSocket {
|
||||||
private val listener = OkHttpWebsocketListener()
|
private val listener = OkHttpWebsocketListener()
|
||||||
|
private var usingOkHttp: OkHttpClient? = null
|
||||||
private var socket: okhttp3.WebSocket? = 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() {
|
override fun connect() {
|
||||||
socket = httpClient(url, forceProxy).newWebSocket(buildRequest(), listener)
|
usingOkHttp = httpClient(url)
|
||||||
|
socket = usingOkHttp?.newWebSocket(buildRequest(), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
||||||
@@ -49,7 +72,7 @@ class OkHttpWebSocket(
|
|||||||
response: Response,
|
response: Response,
|
||||||
) = out.onOpen(
|
) = out.onOpen(
|
||||||
response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
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(
|
override fun onMessage(
|
||||||
@@ -73,31 +96,21 @@ class OkHttpWebSocket(
|
|||||||
webSocket: okhttp3.WebSocket,
|
webSocket: okhttp3.WebSocket,
|
||||||
t: Throwable,
|
t: Throwable,
|
||||||
response: Response?,
|
response: Response?,
|
||||||
) = out.onFailure(t, response?.message)
|
) = out.onFailure(t, response?.code, response?.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Builder(
|
class Builder(
|
||||||
val forceProxy: Boolean,
|
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||||
val httpClient: (String, Boolean) -> OkHttpClient,
|
|
||||||
) : WebsocketBuilder {
|
) : WebsocketBuilder {
|
||||||
// Called when connecting.
|
// Called when connecting.
|
||||||
override fun build(
|
override fun build(
|
||||||
url: String,
|
url: NormalizedRelayUrl,
|
||||||
out: WebSocketListener,
|
out: WebSocketListener,
|
||||||
) = OkHttpWebSocket(url, forceProxy, httpClient, out)
|
) = OkHttpWebSocket(url, httpClient, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BuilderFactory(
|
override fun disconnect() {
|
||||||
val httpClient: (String, Boolean) -> OkHttpClient,
|
socket?.close(1000, "Normal closure")
|
||||||
) : WebsocketBuilderFactory {
|
|
||||||
override fun build(
|
|
||||||
url: String,
|
|
||||||
forceProxy: Boolean,
|
|
||||||
) = Builder(forceProxy, httpClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
socket?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
|
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