Merge branch 'vitorpamplona:main' into labeled-bookmarks
This commit is contained in:
@@ -33,6 +33,7 @@ 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.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
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.core.JsonMapper
|
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||||
@@ -110,6 +111,7 @@ private object PrefKeys {
|
|||||||
const val LATEST_HASHTAG_LIST = "latestHashtagList"
|
const val LATEST_HASHTAG_LIST = "latestHashtagList"
|
||||||
const val LATEST_GEOHASH_LIST = "latestGeohashList"
|
const val LATEST_GEOHASH_LIST = "latestGeohashList"
|
||||||
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
|
||||||
|
const val LATEST_TRUST_PROVIDER_LIST = "latestTrustProviderList"
|
||||||
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"
|
||||||
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
||||||
@@ -354,6 +356,7 @@ object LocalPreferences {
|
|||||||
putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupHashtagList)
|
putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupHashtagList)
|
||||||
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
|
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
|
||||||
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
|
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
|
||||||
|
putOrRemove(PrefKeys.LATEST_TRUST_PROVIDER_LIST, settings.backupTrustProviderList)
|
||||||
|
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
||||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
||||||
@@ -482,6 +485,7 @@ object LocalPreferences {
|
|||||||
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
|
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
|
||||||
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
|
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
|
||||||
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
|
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
|
||||||
|
val latestTrustProviderList = parseEventOrNull<TrustProviderListEvent>(PrefKeys.LATEST_TRUST_PROVIDER_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)
|
||||||
@@ -525,6 +529,7 @@ object LocalPreferences {
|
|||||||
backupHashtagList = latestHashtagList,
|
backupHashtagList = latestHashtagList,
|
||||||
backupGeohashList = latestGeohashList,
|
backupGeohashList = latestGeohashList,
|
||||||
backupEphemeralChatList = latestEphemeralList,
|
backupEphemeralChatList = latestEphemeralList,
|
||||||
|
backupTrustProviderList = latestTrustProviderList,
|
||||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
|
|||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
|
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||||
|
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListDecryptionCache
|
||||||
|
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||||
@@ -284,6 +286,9 @@ class Account(
|
|||||||
val muteListDecryptionCache = MuteListDecryptionCache(signer)
|
val muteListDecryptionCache = MuteListDecryptionCache(signer)
|
||||||
val muteList = MuteListState(signer, cache, muteListDecryptionCache, scope, settings)
|
val muteList = MuteListState(signer, cache, muteListDecryptionCache, scope, settings)
|
||||||
|
|
||||||
|
val trustProviderListDecryptionCache = TrustProviderListDecryptionCache(signer)
|
||||||
|
val trustProviderList = TrustProviderListState(signer, cache, trustProviderListDecryptionCache, scope, settings)
|
||||||
|
|
||||||
val peopleListDecryptionCache = PeopleListDecryptionCache(signer)
|
val peopleListDecryptionCache = PeopleListDecryptionCache(signer)
|
||||||
val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope)
|
val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope)
|
||||||
val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope)
|
val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.screen.FeedDefinition
|
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
@@ -138,6 +139,7 @@ class AccountSettings(
|
|||||||
var backupHashtagList: HashtagListEvent? = null,
|
var backupHashtagList: HashtagListEvent? = null,
|
||||||
var backupGeohashList: GeohashListEvent? = null,
|
var backupGeohashList: GeohashListEvent? = null,
|
||||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||||
|
var backupTrustProviderList: TrustProviderListEvent? = null,
|
||||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||||
@@ -437,6 +439,16 @@ class AccountSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTrustProviderListTo(trustProviderList: TrustProviderListEvent?) {
|
||||||
|
if (trustProviderList == null || trustProviderList.tags.isEmpty()) return
|
||||||
|
|
||||||
|
// Events might be different objects, we have to compare their ids.
|
||||||
|
if (backupTrustProviderList?.id != trustProviderList.id) {
|
||||||
|
backupTrustProviderList = trustProviderList
|
||||||
|
saveAccountSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateMuteList(newMuteList: MuteListEvent?) {
|
fun updateMuteList(newMuteList: MuteListEvent?) {
|
||||||
if (newMuteList == null || newMuteList.tags.isEmpty()) return
|
if (newMuteList == null || newMuteList.tags.isEmpty()) return
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
|||||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
@@ -971,6 +972,12 @@ object LocalCache : ILocalCache {
|
|||||||
wasVerified: Boolean,
|
wasVerified: Boolean,
|
||||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||||
|
|
||||||
|
private fun consume(
|
||||||
|
event: TrustProviderListEvent,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
wasVerified: Boolean,
|
||||||
|
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||||
|
|
||||||
private fun consume(
|
private fun consume(
|
||||||
event: ProxyRelayListEvent,
|
event: ProxyRelayListEvent,
|
||||||
relay: NormalizedRelayUrl?,
|
relay: NormalizedRelayUrl?,
|
||||||
@@ -2856,6 +2863,7 @@ object LocalCache : ILocalCache {
|
|||||||
is TorrentEvent -> consume(event, relay, wasVerified)
|
is TorrentEvent -> consume(event, relay, wasVerified)
|
||||||
is TorrentCommentEvent -> consume(event, relay, wasVerified)
|
is TorrentCommentEvent -> consume(event, relay, wasVerified)
|
||||||
is TrustedRelayListEvent -> consume(event, relay, wasVerified)
|
is TrustedRelayListEvent -> consume(event, relay, wasVerified)
|
||||||
|
is TrustProviderListEvent -> consume(event, relay, wasVerified)
|
||||||
is VideoHorizontalEvent -> consume(event, relay, wasVerified)
|
is VideoHorizontalEvent -> consume(event, relay, wasVerified)
|
||||||
is VideoNormalEvent -> consume(event, relay, wasVerified)
|
is VideoNormalEvent -> consume(event, relay, wasVerified)
|
||||||
is VideoVerticalEvent -> consume(event, relay, wasVerified)
|
is VideoVerticalEvent -> consume(event, relay, wasVerified)
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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.trustedAssertions
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviderSet
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviders
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||||
|
|
||||||
|
class TrustProviderListDecryptionCache(
|
||||||
|
val signer: NostrSigner,
|
||||||
|
) {
|
||||||
|
val cachedPrivateLists = PrivateTagArrayEventCache<TrustProviderListEvent>(signer)
|
||||||
|
|
||||||
|
fun cachedServiceProviders(event: TrustProviderListEvent) = cachedPrivateLists.mergeTagListPrecached(event).serviceProviders()
|
||||||
|
|
||||||
|
fun cachedServiceProviderSet(event: TrustProviderListEvent) = cachedPrivateLists.mergeTagListPrecached(event).serviceProviderSet()
|
||||||
|
|
||||||
|
suspend fun serviceProviders(event: TrustProviderListEvent) = cachedPrivateLists.mergeTagList(event).serviceProviders()
|
||||||
|
|
||||||
|
suspend fun serviceProviderSet(event: TrustProviderListEvent) = cachedPrivateLists.mergeTagList(event).serviceProviderSet()
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* 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.trustedAssertions
|
||||||
|
|
||||||
|
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.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
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
|
||||||
|
|
||||||
|
class TrustProviderListState(
|
||||||
|
val signer: NostrSigner,
|
||||||
|
val cache: LocalCache,
|
||||||
|
val decryptionCache: TrustProviderListDecryptionCache,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val settings: AccountSettings,
|
||||||
|
) {
|
||||||
|
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||||
|
val trustProviderListNote = cache.getOrCreateAddressableNote(getTrustProviderListAddress())
|
||||||
|
|
||||||
|
fun getTrustProviderListAddress() = TrustProviderListEvent.createAddress(signer.pubKey)
|
||||||
|
|
||||||
|
fun getTrustProviderListFlow(): StateFlow<NoteState> = trustProviderListNote.flow().metadata.stateFlow
|
||||||
|
|
||||||
|
fun getTrustProviderList(): TrustProviderListEvent? = trustProviderListNote.event as? TrustProviderListEvent
|
||||||
|
|
||||||
|
suspend fun trustProviderListWithBackup(note: Note): Set<ServiceProviderTag> {
|
||||||
|
val event = note.event as? TrustProviderListEvent ?: settings.backupTrustProviderList
|
||||||
|
return event?.let { decryptionCache.serviceProviderSet(it) } ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val liveTrustProviderList: StateFlow<Set<ServiceProviderTag>> =
|
||||||
|
getTrustProviderListFlow()
|
||||||
|
.transformLatest { noteState ->
|
||||||
|
emit(trustProviderListWithBackup(noteState.note))
|
||||||
|
}.onStart {
|
||||||
|
emit(trustProviderListWithBackup(trustProviderListNote))
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
settings.backupTrustProviderList?.let { event ->
|
||||||
|
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.justConsumeMyOwnEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
Log.d("AccountRegisterObservers", "TrustProviderList Collector Start")
|
||||||
|
getTrustProviderListFlow().collect { noteState ->
|
||||||
|
Log.d("AccountRegisterObservers", "TrustProviderList List for ${signer.pubKey}")
|
||||||
|
(noteState.note.event as? TrustProviderListEvent)?.let {
|
||||||
|
settings.updateTrustProviderListTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -37,7 +37,9 @@ class AuthCoordinator(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private val authWithAccounts = ListWithUniqueSetCache<ScreenAuthAccount, Account> { it.account }
|
private val authWithAccounts = ListWithUniqueSetCache<ScreenAuthAccount, Account> { it.account }
|
||||||
private val tempAccount = NostrSignerSync()
|
private val tempAccount by lazy {
|
||||||
|
NostrSignerSync()
|
||||||
|
}
|
||||||
|
|
||||||
val receiver =
|
val receiver =
|
||||||
RelayAuthenticator(
|
RelayAuthenticator(
|
||||||
|
|||||||
@@ -123,12 +123,17 @@ fun WatchAndRenderRelay(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val relay by baseNote
|
val flow =
|
||||||
.flow()
|
remember(baseNote, relayIndex) {
|
||||||
.relays.stateFlow
|
baseNote
|
||||||
.mapNotNull {
|
.flow()
|
||||||
it.note.relays.getOrNull(relayIndex)
|
.relays.stateFlow
|
||||||
}.collectAsStateWithLifecycle(baseNote.relays.getOrNull(relayIndex))
|
.mapNotNull {
|
||||||
|
it.note.relays.getOrNull(relayIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val relay by flow.collectAsStateWithLifecycle(baseNote.relays.getOrNull(relayIndex))
|
||||||
|
|
||||||
CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) {
|
CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
|
||||||
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.relayClient.reqCommand.event.observeNote
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||||
@@ -113,7 +112,7 @@ fun DisplayHighlightPreview() {
|
|||||||
makeItShort = false,
|
makeItShort = false,
|
||||||
canPreview = true,
|
canPreview = true,
|
||||||
quotesLeft = 3,
|
quotesLeft = 3,
|
||||||
backgroundColor = mutableStateOf(Color.White),
|
backgroundColor = remember { mutableStateOf(Color.White) },
|
||||||
accountViewModel = mockAccountViewModel(),
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = EmptyNav(),
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
@@ -137,7 +136,7 @@ fun DisplayHighlightPreviewNewLine() {
|
|||||||
makeItShort = false,
|
makeItShort = false,
|
||||||
canPreview = true,
|
canPreview = true,
|
||||||
quotesLeft = 3,
|
quotesLeft = 3,
|
||||||
backgroundColor = mutableStateOf(Color.White),
|
backgroundColor = remember { mutableStateOf(Color.White) },
|
||||||
accountViewModel = mockAccountViewModel(),
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = EmptyNav(),
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
@@ -239,7 +238,7 @@ private fun DisplayQuoteAuthor(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
var userBase by remember { mutableStateOf<User?>(authorHex?.let { accountViewModel.getUserIfExists(it) }) }
|
var userBase by remember { mutableStateOf(authorHex?.let { accountViewModel.getUserIfExists(it) }) }
|
||||||
|
|
||||||
if (userBase == null && authorHex != null) {
|
if (userBase == null && authorHex != null) {
|
||||||
LaunchedEffect(authorHex) {
|
LaunchedEffect(authorHex) {
|
||||||
@@ -248,7 +247,7 @@ private fun DisplayQuoteAuthor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var addressable by remember {
|
var addressable by remember {
|
||||||
mutableStateOf<AddressableNote?>(postAddress?.let { accountViewModel.getAddressableNoteIfExists(it) })
|
mutableStateOf(postAddress?.let { accountViewModel.getAddressableNoteIfExists(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addressable == null && postAddress != null) {
|
if (addressable == null && postAddress != null) {
|
||||||
@@ -264,7 +263,7 @@ private fun DisplayQuoteAuthor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var version by remember {
|
var version by remember {
|
||||||
mutableStateOf<Note?>(postVersion?.let { accountViewModel.getNoteIfExists(it.eventId) })
|
mutableStateOf(postVersion?.let { accountViewModel.getNoteIfExists(it.eventId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version == null && postVersion != null) {
|
if (version == null && postVersion != null) {
|
||||||
@@ -324,7 +323,7 @@ fun DisplayEntryForNote(
|
|||||||
) {
|
) {
|
||||||
val noteState by observeNote(note, accountViewModel)
|
val noteState by observeNote(note, accountViewModel)
|
||||||
|
|
||||||
val author = userBase ?: noteState?.note?.author
|
val author = userBase ?: noteState.note.author
|
||||||
|
|
||||||
if (author != null) {
|
if (author != null) {
|
||||||
RenderUserAsClickableText(author, null, accountViewModel, nav)
|
RenderUserAsClickableText(author, null, accountViewModel, nav)
|
||||||
@@ -361,7 +360,7 @@ fun DisplayEntryForAUrl(
|
|||||||
remember {
|
remember {
|
||||||
try {
|
try {
|
||||||
URL(url)
|
URL(url)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Log.w("Note Compose", "Invalid URI: $url")
|
Log.w("Note Compose", "Invalid URI: $url")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-13
@@ -65,7 +65,6 @@ import com.vitorpamplona.quartz.utils.Log
|
|||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -85,17 +84,14 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
fun loginWithDefaultAccountIfLoggedOff() {
|
fun loginWithDefaultAccountIfLoggedOff() {
|
||||||
// pulls account from storage.
|
// pulls account from storage.
|
||||||
if (_accountContent.value !is AccountState.LoggedIn) {
|
if (_accountContent.value !is AccountState.LoggedIn) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
loginWithDefaultAccount()
|
loginWithDefaultAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loginWithDefaultAccount(route: Route? = null) {
|
private suspend fun loginWithDefaultAccount(route: Route? = null) {
|
||||||
val accountSettings =
|
val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage()
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
LocalPreferences.loadAccountConfigFromEncryptedStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountSettings != null) {
|
if (accountSettings != null) {
|
||||||
startUI(accountSettings, route)
|
startUI(accountSettings, route)
|
||||||
@@ -135,7 +131,7 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
throw Exception("Invalid key while trying to login with external signer")
|
throw Exception("Invalid key while trying to login with external signer")
|
||||||
}
|
}
|
||||||
|
|
||||||
val account =
|
val accountSettings =
|
||||||
if (loginWithExternalSigner) {
|
if (loginWithExternalSigner) {
|
||||||
AccountSettings(
|
AccountSettings(
|
||||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||||
@@ -164,18 +160,18 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalPreferences.setDefaultAccount(account)
|
LocalPreferences.setDefaultAccount(accountSettings)
|
||||||
|
|
||||||
startUI(account)
|
startUI(accountSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
fun startUI(
|
||||||
suspend fun startUI(
|
|
||||||
accountSettings: AccountSettings,
|
accountSettings: AccountSettings,
|
||||||
route: Route? = null,
|
route: Route? = null,
|
||||||
) = withContext(Dispatchers.Main) {
|
) {
|
||||||
|
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
|
||||||
_accountContent.update {
|
_accountContent.update {
|
||||||
AccountState.LoggedIn(Amethyst.instance.accountsCache.loadAccount(accountSettings), route)
|
AccountState.LoggedIn(account, route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanistAdaptive = "0.37.3"
|
accompanistAdaptive = "0.37.3"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.12.0"
|
||||||
agp = "8.13.1"
|
agp = "8.13.1"
|
||||||
android-compileSdk = "36"
|
android-compileSdk = "36"
|
||||||
android-minSdk = "26"
|
android-minSdk = "26"
|
||||||
@@ -13,11 +13,11 @@ benchmark = "1.4.1"
|
|||||||
benchmarkJunit4 = "1.4.1"
|
benchmarkJunit4 = "1.4.1"
|
||||||
biometricKtx = "1.2.0-alpha05"
|
biometricKtx = "1.2.0-alpha05"
|
||||||
coil = "3.3.0"
|
coil = "3.3.0"
|
||||||
composeBom = "2025.11.00"
|
composeBom = "2025.11.01"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
datastore = "1.1.7"
|
datastore = "1.2.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
firebaseBom = "34.5.0"
|
firebaseBom = "34.6.0"
|
||||||
fragmentKtx = "1.8.9"
|
fragmentKtx = "1.8.9"
|
||||||
gms = "4.4.4"
|
gms = "4.4.4"
|
||||||
jacksonModuleKotlin = "2.20.1"
|
jacksonModuleKotlin = "2.20.1"
|
||||||
@@ -32,35 +32,35 @@ kotlinxSerializationPlugin = "2.2.21"
|
|||||||
languageId = "17.0.6"
|
languageId = "17.0.6"
|
||||||
lazysodiumAndroid = "5.2.0"
|
lazysodiumAndroid = "5.2.0"
|
||||||
lazysodiumJava = "5.2.0"
|
lazysodiumJava = "5.2.0"
|
||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
lightcompressor-enhanced = "1.6.0"
|
lightcompressor-enhanced = "1.6.0"
|
||||||
markdown = "f92ef49c9d"
|
markdown = "f92ef49c9d"
|
||||||
media3 = "1.8.0"
|
media3 = "1.8.0"
|
||||||
mockk = "1.14.6"
|
mockk = "1.14.6"
|
||||||
kotlinx-coroutines-test = "1.10.2"
|
kotlinx-coroutines-test = "1.10.2"
|
||||||
navigationCompose = "2.9.6"
|
navigationCompose = "2.9.6"
|
||||||
okhttp = "5.3.0"
|
okhttp = "5.3.2"
|
||||||
runner = "1.7.0"
|
runner = "1.7.0"
|
||||||
rfc3986 = "0.1.2"
|
rfc3986 = "0.1.2"
|
||||||
secp256k1KmpJniAndroid = "0.21.0"
|
secp256k1KmpJniAndroid = "0.21.0"
|
||||||
securityCryptoKtx = "1.1.0"
|
securityCryptoKtx = "1.1.0"
|
||||||
spotless = "8.0.0"
|
spotless = "8.1.0"
|
||||||
torAndroid = "0.4.8.19"
|
torAndroid = "0.4.8.19"
|
||||||
translate = "17.0.3"
|
translate = "17.0.3"
|
||||||
unifiedpush = "3.0.10"
|
unifiedpush = "3.0.10"
|
||||||
urlDetector = "0.1.23"
|
urlDetector = "0.1.23"
|
||||||
vico-charts = "2.3.4"
|
vico-charts = "2.3.6"
|
||||||
zelory = "3.0.1"
|
zelory = "3.0.1"
|
||||||
zoomable = "2.9.0"
|
zoomable = "2.9.0"
|
||||||
zxing = "3.5.4"
|
zxing = "3.5.4"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
windowCoreAndroid = "1.5.0"
|
windowCoreAndroid = "1.5.1"
|
||||||
androidxCamera = "1.5.1"
|
androidxCamera = "1.5.1"
|
||||||
androidxCollection = "1.5.0"
|
androidxCollection = "1.5.0"
|
||||||
kotlinStdlib = "2.2.20"
|
kotlinStdlib = "2.2.21"
|
||||||
kotlinTest = "2.2.20"
|
kotlinTest = "2.2.20"
|
||||||
core = "1.7.0"
|
core = "1.7.0"
|
||||||
mavenPublish = "0.34.0"
|
mavenPublish = "0.35.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" }
|
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" }
|
||||||
|
|||||||
+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.quartz.experimental.trustedAssertions.list
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceType
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
|
fun TagArrayBuilder<TrustProviderListEvent>.serviceProvider(
|
||||||
|
service: ServiceType,
|
||||||
|
pubkey: HexKey,
|
||||||
|
relayUrl: NormalizedRelayUrl,
|
||||||
|
) = addUnique(ServiceProviderTag.assemble(service, pubkey, relayUrl))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<TrustProviderListEvent>.serviceProvider(provider: ServiceProviderTag) = addUnique(provider.toTagArray())
|
||||||
|
|
||||||
|
fun TagArrayBuilder<TrustProviderListEvent>.serviceProviders(providers: List<ServiceProviderTag>) = addAll(providers.map { it.toTagArray() })
|
||||||
+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.quartz.experimental.trustedAssertions.list
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag.Companion.parse
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
|
||||||
|
fun TagArray.serviceProviders() = mapNotNull(ServiceProviderTag::parse)
|
||||||
|
|
||||||
|
fun TagArray.serviceProviderSet() = mapNotNullTo(mutableSetOf(), ServiceProviderTag::parse)
|
||||||
+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.quartz.experimental.trustedAssertions.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.fastAny
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.removeParsing
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class TrustProviderListEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun serviceProviders() = tags.serviceProviders()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 10040
|
||||||
|
const val ALT = "Trusted Service Providers"
|
||||||
|
const val FIXED_D_TAG = ""
|
||||||
|
|
||||||
|
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
|
||||||
|
|
||||||
|
suspend fun create(
|
||||||
|
provider: ServiceProviderTag,
|
||||||
|
isPrivate: Boolean,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): TrustProviderListEvent =
|
||||||
|
if (isPrivate) {
|
||||||
|
create(
|
||||||
|
publicProviders = emptyList(),
|
||||||
|
privateProviders = listOf(provider),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
create(
|
||||||
|
publicProviders = listOf(provider),
|
||||||
|
privateProviders = emptyList(),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun add(
|
||||||
|
earlierVersion: TrustProviderListEvent,
|
||||||
|
provider: ServiceProviderTag,
|
||||||
|
isPrivate: Boolean,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): TrustProviderListEvent =
|
||||||
|
if (isPrivate) {
|
||||||
|
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||||
|
resign(
|
||||||
|
tags = earlierVersion.tags,
|
||||||
|
privateTags = privateTags.plus(provider.toTagArray()),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resign(
|
||||||
|
content = earlierVersion.content,
|
||||||
|
tags = earlierVersion.tags.plus(provider.toTagArray()),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(
|
||||||
|
earlierVersion: TrustProviderListEvent,
|
||||||
|
provider: ServiceProviderTag,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): TrustProviderListEvent {
|
||||||
|
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||||
|
return resign(
|
||||||
|
privateTags = privateTags.removeParsing(ServiceProviderTag::parse, provider),
|
||||||
|
tags = earlierVersion.tags.removeParsing(ServiceProviderTag::parse, provider),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resign(
|
||||||
|
tags: TagArray,
|
||||||
|
privateTags: TagArray,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
) = resign(
|
||||||
|
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
|
||||||
|
tags = tags,
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun resign(
|
||||||
|
content: String,
|
||||||
|
tags: TagArray,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): TrustProviderListEvent {
|
||||||
|
val newTags =
|
||||||
|
if (tags.fastAny(AltTag::match)) {
|
||||||
|
tags
|
||||||
|
} else {
|
||||||
|
tags + AltTag.assemble(ALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer.sign(createdAt, KIND, newTags, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun create(
|
||||||
|
publicProviders: List<ServiceProviderTag> = emptyList(),
|
||||||
|
privateProviders: List<ServiceProviderTag> = emptyList(),
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
) = signer.sign(build(publicProviders, privateProviders, signer, createdAt))
|
||||||
|
|
||||||
|
suspend fun build(
|
||||||
|
publicProviders: List<ServiceProviderTag> = emptyList(),
|
||||||
|
privateProviders: List<ServiceProviderTag> = emptyList(),
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<TrustProviderListEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(
|
||||||
|
kind = KIND,
|
||||||
|
description = PrivateTagsInContent.encryptNip44(privateProviders.map { it.toTagArray() }.toTypedArray(), signer),
|
||||||
|
createdAt = createdAt,
|
||||||
|
) {
|
||||||
|
alt(ALT)
|
||||||
|
serviceProviders(publicProviders)
|
||||||
|
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.quartz.experimental.trustedAssertions.list.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
data class ServiceProviderTag(
|
||||||
|
val service: ServiceType,
|
||||||
|
val pubkey: HexKey,
|
||||||
|
val relayUrl: NormalizedRelayUrl,
|
||||||
|
) {
|
||||||
|
fun toTagArray() = assemble(service, pubkey, relayUrl)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(tag: Array<String>): ServiceProviderTag? {
|
||||||
|
ensure(tag.has(2)) { return null }
|
||||||
|
ensure(tag[0].isNotEmpty()) { return null }
|
||||||
|
ensure(tag[1].length == 64) { return null }
|
||||||
|
ensure(tag[2].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
val service = ServiceType.parse(tag[0]) ?: return null
|
||||||
|
val relay = RelayUrlNormalizer.normalizeOrNull(tag[2]) ?: return null
|
||||||
|
|
||||||
|
return ServiceProviderTag(service, tag[1], relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(
|
||||||
|
serviceType: String,
|
||||||
|
pubkey: HexKey,
|
||||||
|
relayUrl: String,
|
||||||
|
) = arrayOf(serviceType, pubkey, relayUrl)
|
||||||
|
|
||||||
|
fun assemble(
|
||||||
|
service: ServiceType,
|
||||||
|
pubkey: HexKey,
|
||||||
|
relayUrl: NormalizedRelayUrl,
|
||||||
|
) = assemble(service.toValue(), pubkey, relayUrl.url)
|
||||||
|
|
||||||
|
fun assemble(id: ServiceProviderTag) = arrayOf(id.service, id.pubkey, id.relayUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ServiceType(
|
||||||
|
val kind: Int,
|
||||||
|
val type: String,
|
||||||
|
) {
|
||||||
|
fun toValue() = assemble(kind, type)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun assemble(
|
||||||
|
kind: Int,
|
||||||
|
type: String,
|
||||||
|
) = "$kind:$type"
|
||||||
|
|
||||||
|
fun parse(serviceType: String): ServiceType? {
|
||||||
|
val (kindStr, type) = serviceType.split(":", limit = 2)
|
||||||
|
val kind = kindStr.toIntOrNull() ?: return null
|
||||||
|
return ServiceType(kind, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isOfKind(
|
||||||
|
serviceType: String,
|
||||||
|
kind: String,
|
||||||
|
) = serviceType.startsWith(kind) && serviceType[kind.length] == ':'
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -103,7 +103,7 @@ suspend fun INostrClient.sendAndWaitForResponse(
|
|||||||
val currentResult = receivedResults[result.relay]
|
val currentResult = receivedResults[result.relay]
|
||||||
// do not override a successful result.
|
// do not override a successful result.
|
||||||
if (currentResult == null || !currentResult) {
|
if (currentResult == null || !currentResult) {
|
||||||
receivedResults.put(result.relay, result.success)
|
receivedResults[result.relay] = result.success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.experimental.nns.NNSEvent
|
|||||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
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
|
||||||
@@ -294,6 +295,7 @@ class EventFactory {
|
|||||||
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
|
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
TrustProviderListEvent.KIND -> TrustProviderListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
|
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
VideoNormalEvent.KIND -> VideoNormalEvent(id, pubKey, createdAt, tags, content, sig)
|
VideoNormalEvent.KIND -> VideoNormalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
+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.quartz.experimental.trustedAssertions
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
|
||||||
|
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceType
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class ServiceParser {
|
||||||
|
val brainstorm =
|
||||||
|
TrustProviderListEvent(
|
||||||
|
id = "d74ce2e62a152e787001f81b51f7f18b69816cbafc064e38af99f1ee55895424",
|
||||||
|
pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||||
|
createdAt = 1762635473,
|
||||||
|
tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("30382:rank", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:followers", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:personalizedGrapeRank_influence", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:personalizedGrapeRank_average", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:personalizedGrapeRank_confidence", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:personalizedGrapeRank_input", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:personalizedPageRank", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:verifiedFollowersCount", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:verifiedMutersCount", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:verifiedReportersCount", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
arrayOf("30382:hops", "a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98", "wss://nip85.brainstorm.world"),
|
||||||
|
),
|
||||||
|
content = "",
|
||||||
|
sig = "8947d7b83b9483d241aba1b3af09c5b95b4b053d3c9e4832e1b39eb805ba33a156ea62150e99416a38ff07026f8f99f6a0bbdedb928413ba404c8b9631415c21",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseService() {
|
||||||
|
assertEquals(
|
||||||
|
ServiceProviderTag(
|
||||||
|
ServiceType(30382, "rank"),
|
||||||
|
"a49e1bdd16cb0d720cd04c4c4cd9e04c21ea2292121fbbe99f03f4a6c1fd2a98",
|
||||||
|
RelayUrlNormalizer.normalize("wss://nip85.brainstorm.world"),
|
||||||
|
),
|
||||||
|
brainstorm.serviceProviders()[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -30,14 +30,14 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientSendAndWaitTest : BaseNostrClientTest() {
|
class NostrClientSendAndWaitTest : BaseNostrClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testSendAndWaitForResponse() =
|
fun testSendAndWaitForResponse() =
|
||||||
runTest {
|
runBlocking {
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
val client = NostrClient(socketBuilder, appScope)
|
||||||
|
|
||||||
@@ -61,6 +61,6 @@ class NostrClientSendAndWaitTest : BaseNostrClientTest() {
|
|||||||
appScope.cancel()
|
appScope.cancel()
|
||||||
|
|
||||||
assertEquals(true, resultDamus)
|
assertEquals(true, resultDamus)
|
||||||
assertEquals(false, resultNos)
|
assertEquals(true, resultNos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user