Adds broadcasting relay section

This commit is contained in:
Vitor Pamplona
2025-07-23 11:35:25 -04:00
parent 5ac4314e2a
commit 57e4d76da0
19 changed files with 465 additions and 10 deletions
@@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleLi
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
@@ -242,6 +244,9 @@ class Account(
val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer) val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer)
val trustedRelayList = TrustedRelayListState(signer, cache, trustedRelayListDecryptionCache, scope, settings) val trustedRelayList = TrustedRelayListState(signer, cache, trustedRelayListDecryptionCache, scope, settings)
val broadcastRelayListDecryptionCache = BroadcastRelayListDecryptionCache(signer)
val broadcastRelayList = BroadcastRelayListState(signer, cache, broadcastRelayListDecryptionCache, scope, settings)
val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer)
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
@@ -280,15 +285,15 @@ class Account(
val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope)
// Relay settings // Relay settings
val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope)
val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope)
val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope)
val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, scope) val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, broadcastRelayList, scope)
// Follows Relays // Follows Relays
val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope)
val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, scope) val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, scope)
// keeps a cache of the outbox relays for each author // keeps a cache of the outbox relays for each author
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow
@@ -1714,6 +1719,8 @@ class Account(
suspend fun saveSearchRelayList(searchRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) suspend fun saveSearchRelayList(searchRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays))
suspend fun saveBroadcastRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays))
suspend fun saveTrustedRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) suspend fun saveTrustedRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays))
suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays))
@@ -138,6 +138,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
@@ -957,6 +958,12 @@ object LocalCache : ILocalCache {
wasVerified: Boolean, wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified) ) = consumeBaseReplaceable(event, relay, wasVerified)
private fun consume(
event: BroadcastRelayListEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
private fun consume( private fun consume(
event: CommunityDefinitionEvent, event: CommunityDefinitionEvent,
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
@@ -2709,6 +2716,7 @@ object LocalCache : ILocalCache {
is BadgeProfilesEvent -> consume(event, relay, wasVerified) is BadgeProfilesEvent -> consume(event, relay, wasVerified)
is BlockedRelayListEvent -> consume(event, relay, wasVerified) is BlockedRelayListEvent -> consume(event, relay, wasVerified)
is BlossomServersEvent -> consume(event, relay, wasVerified) is BlossomServersEvent -> consume(event, relay, wasVerified)
is BroadcastRelayListEvent -> consume(event, relay, wasVerified)
is BookmarkListEvent -> consume(event, relay, wasVerified) is BookmarkListEvent -> consume(event, relay, wasVerified)
is CalendarEvent -> consume(event, relay, wasVerified) is CalendarEvent -> consume(event, relay, wasVerified)
is CalendarDateSlotEvent -> consume(event, relay, wasVerified) is CalendarDateSlotEvent -> consume(event, relay, wasVerified)
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -34,6 +35,7 @@ class AccountOutboxRelayState(
nip65: Nip65RelayListState, nip65: Nip65RelayListState,
privateStorage: PrivateStorageRelayListState, privateStorage: PrivateStorageRelayListState,
local: LocalRelayListState, local: LocalRelayListState,
broadcast: BroadcastRelayListState,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
val flow = val flow =
@@ -41,14 +43,16 @@ class AccountOutboxRelayState(
nip65.outboxFlow, nip65.outboxFlow,
privateStorage.flow, privateStorage.flow,
local.flow, local.flow,
) { nip65Inbox, privateOutBox, localRelays -> broadcast.flow,
nip65Inbox + privateOutBox + localRelays ) { nip65Inbox, privateOutBox, localRelays, broadcastRelays ->
nip65Inbox + privateOutBox + localRelays + broadcastRelays
}.flowOn(Dispatchers.Default) }.flowOn(Dispatchers.Default)
.stateIn( .stateIn(
scope, scope,
SharingStarted.Eagerly, SharingStarted.Eagerly,
nip65.outboxFlow.value + nip65.outboxFlow.value +
privateStorage.flow.value + privateStorage.flow.value +
local.flow.value, local.flow.value +
broadcast.flow.value,
) )
} }
@@ -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.nip51Lists.broadcastRelays
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
class BroadcastRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<BroadcastRelayListEvent>(signer)
@@ -0,0 +1,87 @@
/**
* 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.broadcastRelays
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.nip51Lists.relayLists.BroadcastRelayListEvent
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 BroadcastRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: BroadcastRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getBroadcastRelayListAddress() = BroadcastRelayListEvent.createAddress(signer.pubKey)
fun getBroadcastRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = getBroadcastRelayListNote().flow().metadata.stateFlow
fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent
suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? BroadcastRelayListEvent
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
val flow =
getBroadcastRelayListFlow()
.map { normalizeBroadcastRelayListWithBackup(it.note) }
.onStart { emit(normalizeBroadcastRelayListWithBackup(getBroadcastRelayListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(broadcastRelays: List<NormalizedRelayUrl>): BroadcastRelayListEvent {
val relayListForBroadcast = getBroadcastRelayList()
return if (relayListForBroadcast != null && relayListForBroadcast.tags.isNotEmpty()) {
BroadcastRelayListEvent.updateRelayList(
earlierVersion = relayListForBroadcast,
relays = broadcastRelays,
signer = signer,
)
} else {
BroadcastRelayListEvent.create(
relays = broadcastRelays,
signer = signer,
)
}
}
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -41,6 +42,7 @@ class MergedFollowPlusMineRelayListsState(
val privateOutboxRelayList: PrivateStorageRelayListState, val privateOutboxRelayList: PrivateStorageRelayListState,
val localRelayList: LocalRelayListState, val localRelayList: LocalRelayListState,
val trustedRelayList: TrustedRelayListState, val trustedRelayList: TrustedRelayListState,
val broadcastRelayList: BroadcastRelayListState,
val scope: CoroutineScope, val scope: CoroutineScope,
) { ) {
fun mergeLists(lists: Array<Set<NormalizedRelayUrl>>): Set<NormalizedRelayUrl> = lists.reduce { acc, set -> acc + set } fun mergeLists(lists: Array<Set<NormalizedRelayUrl>>): Set<NormalizedRelayUrl> = lists.reduce { acc, set -> acc + set }
@@ -54,6 +56,7 @@ class MergedFollowPlusMineRelayListsState(
privateOutboxRelayList.flow, privateOutboxRelayList.flow,
localRelayList.flow, localRelayList.flow,
trustedRelayList.flow, trustedRelayList.flow,
broadcastRelayList.flow,
), ),
::mergeLists, ::mergeLists,
).onStart { ).onStart {
@@ -66,6 +69,7 @@ class MergedFollowPlusMineRelayListsState(
privateOutboxRelayList.flow.value, privateOutboxRelayList.flow.value,
localRelayList.flow.value, localRelayList.flow.value,
trustedRelayList.flow.value, trustedRelayList.flow.value,
broadcastRelayList.flow.value,
), ),
), ),
) )
@@ -81,6 +85,7 @@ class MergedFollowPlusMineRelayListsState(
privateOutboxRelayList.flow.value, privateOutboxRelayList.flow.value,
localRelayList.flow.value, localRelayList.flow.value,
trustedRelayList.flow.value, trustedRelayList.flow.value,
broadcastRelayList.flow.value,
), ),
), ),
) )
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
@@ -43,6 +44,7 @@ class TrustedRelayListsState(
val dmRelayList: DmRelayListState, val dmRelayList: DmRelayListState,
val searchRelayListState: SearchRelayListState, val searchRelayListState: SearchRelayListState,
val trustedRelayList: TrustedRelayListState, val trustedRelayList: TrustedRelayListState,
val broadcastRelayList: BroadcastRelayListState,
val scope: CoroutineScope, val scope: CoroutineScope,
) { ) {
fun mergeLists(lists: Array<Set<NormalizedRelayUrl>>): Set<NormalizedRelayUrl> = lists.reduce { acc, set -> acc + set } fun mergeLists(lists: Array<Set<NormalizedRelayUrl>>): Set<NormalizedRelayUrl> = lists.reduce { acc, set -> acc + set }
@@ -56,6 +58,7 @@ class TrustedRelayListsState(
dmRelayList.flow, dmRelayList.flow,
searchRelayListState.flow, searchRelayListState.flow,
trustedRelayList.flow, trustedRelayList.flow,
broadcastRelayList.flow,
), ),
::mergeLists, ::mergeLists,
).onStart { ).onStart {
@@ -68,6 +71,7 @@ class TrustedRelayListsState(
dmRelayList.flow.value, dmRelayList.flow.value,
searchRelayListState.flow.value, searchRelayListState.flow.value,
trustedRelayList.flow.value, trustedRelayList.flow.value,
broadcastRelayList.flow.value,
), ),
), ),
) )
@@ -83,6 +87,7 @@ class TrustedRelayListsState(
dmRelayList.flow.value, dmRelayList.flow.value,
searchRelayListState.flow.value, searchRelayListState.flow.value,
trustedRelayList.flow.value, trustedRelayList.flow.value,
broadcastRelayList.flow.value,
), ),
), ),
) )
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
@@ -55,6 +56,7 @@ val AccountInfoAndListsFromKeyKinds2 =
listOf( listOf(
BlockedRelayListEvent.KIND, BlockedRelayListEvent.KIND,
TrustedRelayListEvent.KIND, TrustedRelayListEvent.KIND,
BroadcastRelayListEvent.KIND,
) )
val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND)
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
@@ -43,8 +44,13 @@ val BasicAccountInfoKinds =
SearchRelayListEvent.KIND, SearchRelayListEvent.KIND,
FileServersEvent.KIND, FileServersEvent.KIND,
BlossomServersEvent.KIND, BlossomServersEvent.KIND,
)
val BasicAccountInfoKinds2 =
listOf(
BlockedRelayListEvent.KIND, BlockedRelayListEvent.KIND,
TrustedRelayListEvent.KIND, TrustedRelayListEvent.KIND,
BroadcastRelayListEvent.KIND,
) )
fun filterBasicAccountInfoFromKeys( fun filterBasicAccountInfoFromKeys(
@@ -61,7 +67,17 @@ fun filterBasicAccountInfoFromKeys(
Filter( Filter(
kinds = BasicAccountInfoKinds, kinds = BasicAccountInfoKinds,
authors = otherAccounts.toList(), authors = otherAccounts.toList(),
limit = otherAccounts.size * 8, limit = otherAccounts.size * BasicAccountInfoKinds.size,
since = since,
),
),
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = BasicAccountInfoKinds2,
authors = otherAccounts.toList(),
limit = otherAccounts.size * BasicAccountInfoKinds2.size,
since = since, since = since,
), ),
), ),
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
@@ -187,6 +188,7 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
@@ -654,6 +656,7 @@ private fun RenderNoteRow(
is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav)
is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav)
is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav)
is BroadcastRelayListEvent -> DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav)
is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav) is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav)
@@ -234,6 +234,27 @@ fun DisplayTrustedRelayList(
) )
} }
@Composable
fun DisplayBroadcastRelayList(
baseNote: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val relays by accountViewModel.account.broadcastRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle(
accountViewModel.account.broadcastRelayListDecryptionCache.fastStartValueForRelayList(baseNote),
)
DisplayRelaySet(
relays,
stringRes(id = R.string.broadcast_relays_title),
null,
backgroundColor,
accountViewModel,
nav,
)
}
@Composable @Composable
fun DisplayRelaySet( fun DisplayRelaySet(
relay: RelayListCard, relay: RelayListCard,
@@ -47,6 +47,8 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems
@@ -83,6 +85,7 @@ fun AllRelayListScreen(
val trustedViewModel: TrustedRelayListViewModel = viewModel() val trustedViewModel: TrustedRelayListViewModel = viewModel()
val localViewModel: LocalRelayListViewModel = viewModel() val localViewModel: LocalRelayListViewModel = viewModel()
val connectedViewModel: ConnectedRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel()
val broadcastViewModel: BroadcastRelayListViewModel = viewModel()
dmViewModel.init(accountViewModel) dmViewModel.init(accountViewModel)
nip65ViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel)
@@ -92,6 +95,7 @@ fun AllRelayListScreen(
connectedViewModel.init(accountViewModel) connectedViewModel.init(accountViewModel)
blockedViewModel.init(accountViewModel) blockedViewModel.init(accountViewModel)
trustedViewModel.init(accountViewModel) trustedViewModel.init(accountViewModel)
broadcastViewModel.init(accountViewModel)
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
dmViewModel.load() dmViewModel.load()
@@ -102,6 +106,7 @@ fun AllRelayListScreen(
connectedViewModel.load() connectedViewModel.load()
blockedViewModel.load() blockedViewModel.load()
trustedViewModel.load() trustedViewModel.load()
broadcastViewModel.load()
} }
MappedAllRelayListView( MappedAllRelayListView(
@@ -113,6 +118,7 @@ fun AllRelayListScreen(
connectedViewModel, connectedViewModel,
blockedViewModel, blockedViewModel,
trustedViewModel, trustedViewModel,
broadcastViewModel,
accountViewModel, accountViewModel,
nav, nav,
) )
@@ -129,6 +135,7 @@ fun MappedAllRelayListView(
connectedViewModel: ConnectedRelayListViewModel, connectedViewModel: ConnectedRelayListViewModel,
blockedViewModel: BlockedRelayListViewModel, blockedViewModel: BlockedRelayListViewModel,
trustedViewModel: TrustedRelayListViewModel, trustedViewModel: TrustedRelayListViewModel,
broadcastViewModel: BroadcastRelayListViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newNav: INav, newNav: INav,
) { ) {
@@ -141,6 +148,7 @@ fun MappedAllRelayListView(
val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle() val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle()
val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle()
val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle()
val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -154,6 +162,7 @@ fun MappedAllRelayListView(
privateOutboxViewModel.clear() privateOutboxViewModel.clear()
trustedViewModel.clear() trustedViewModel.clear()
blockedViewModel.clear() blockedViewModel.clear()
broadcastViewModel.clear()
newNav.popBack() newNav.popBack()
}, },
onPost = { onPost = {
@@ -164,6 +173,7 @@ fun MappedAllRelayListView(
privateOutboxViewModel.create() privateOutboxViewModel.create()
trustedViewModel.create() trustedViewModel.create()
blockedViewModel.create() blockedViewModel.create()
broadcastViewModel.create()
newNav.popBack() newNav.popBack()
}, },
) )
@@ -221,6 +231,15 @@ fun MappedAllRelayListView(
} }
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav) renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav)
item {
SettingsCategory(
stringRes(R.string.broadcast_section),
stringRes(R.string.broadcast_section_explainer),
SettingsCategorySpacingModifier,
)
}
renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav)
item { item {
SettingsCategoryWithButton( SettingsCategoryWithButton(
stringRes(R.string.search_section), stringRes(R.string.search_section),
@@ -0,0 +1,35 @@
/**
* 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.ui.screen.loggedIn.relays.broadcast
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import java.util.stream.Collectors.toList
class BroadcastRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<NormalizedRelayUrl>? =
account.broadcastRelayList.flow.value
.toList()
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.saveBroadcastRelayList(urlList)
}
}
@@ -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.ui.screen.loggedIn.relays.broadcast
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable
fun BroadcastRelayList(
postViewModel: BroadcastRelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
nav: INav,
) {
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
val newNav = rememberExtendedNav(nav, onClose)
Row(verticalAlignment = Alignment.CenterVertically) {
LazyColumn(
contentPadding = FeedPadding,
) {
renderBroadcastItems(feedState, postViewModel, accountViewModel, newNav)
}
}
}
fun LazyListScope.renderBroadcastItems(
feedState: List<BasicRelaySetupInfo>,
postViewModel: BroadcastRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
itemsIndexed(feedState, key = { _, item -> "Broadcast" + item.relay }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
accountViewModel = accountViewModel,
nav,
)
}
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -119,6 +119,7 @@ import com.vitorpamplona.amethyst.ui.note.types.AudioHeader
import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
@@ -208,6 +209,7 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
@@ -603,6 +605,8 @@ private fun FullBleedNoteCompose(
DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is TrustedRelayListEvent) { } else if (noteEvent is TrustedRelayListEvent) {
DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is BroadcastRelayListEvent) {
DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is FhirResourceEvent) { } else if (noteEvent is FhirResourceEvent) {
RenderFhirResource(baseNote, accountViewModel, nav) RenderFhirResource(baseNote, accountViewModel, nav)
} else if (noteEvent is GitRepositoryEvent) { } else if (noteEvent is GitRepositoryEvent) {
+6 -3
View File
@@ -1064,9 +1064,6 @@
<string name="search_relays_not_found_editing">Insert between 13 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50</string> <string name="search_relays_not_found_editing">Insert between 13 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50</string>
<string name="search_relays_not_found_examples">Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string> <string name="search_relays_not_found_examples">Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com</string>
<string name="blocked_relays_title">Blocked Relays</string>
<string name="trusted_relays_title">Trusted Relays</string>
<string name="dm_upload">DM Upload</string> <string name="dm_upload">DM Upload</string>
<string name="relay_settings">Relay Settings</string> <string name="relay_settings">Relay Settings</string>
<string name="public_home_section">Public Outbox/Home Relays</string> <string name="public_home_section">Public Outbox/Home Relays</string>
@@ -1091,9 +1088,15 @@
<string name="local_section">Local Relays</string> <string name="local_section">Local Relays</string>
<string name="local_section_explainer">List of relays that are running in this device.</string> <string name="local_section_explainer">List of relays that are running in this device.</string>
<string name="trusted_relays_title">Trusted Relays</string>
<string name="trusted_section">Trusted Relays</string> <string name="trusted_section">Trusted Relays</string>
<string name="trusted_section_explainer">Relays you trust to not need a Tor connection for</string> <string name="trusted_section_explainer">Relays you trust to not need a Tor connection for</string>
<string name="broadcast_relays_title">Broadcast Relays</string>
<string name="broadcast_section">Broadcast Relays</string>
<string name="broadcast_section_explainer">Relays that specialize in pushing your notes to all of the other relays. Amethyst will add this relay to all new events made by you</string>
<string name="blocked_relays_title">Blocked Relays</string>
<string name="blocked_section">Blocked Relays</string> <string name="blocked_section">Blocked Relays</string>
<string name="blocked_section_explainer">Amethyst will never connect to these relays</string> <string name="blocked_section_explainer">Amethyst will never connect to these relays</string>
@@ -82,6 +82,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
@@ -161,6 +162,7 @@ class EventFactory {
BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
BlossomServersEvent.KIND -> BlossomServersEvent(id, pubKey, createdAt, tags, content, sig) BlossomServersEvent.KIND -> BlossomServersEvent(id, pubKey, createdAt, tags, content, sig)
BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
BroadcastRelayListEvent.KIND -> BroadcastRelayListEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig) CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,121 @@
/**
* 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.nip51Lists.relayLists
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
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.encryption.signNip51List
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.broadcastRelays
import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.plus
@Immutable
class BroadcastRelayListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun publicRelays() = tags.relays()
suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays()
suspend fun decryptRelays(signer: NostrSigner): List<NormalizedRelayUrl> = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList())
companion object {
const val KIND = 10088
const val ALT = "Broadcasting relays from this author"
val TAGS = arrayOf(AltTag.assemble(ALT))
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "")
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null)
fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "")
suspend fun updateRelayList(
earlierVersion: BroadcastRelayListEvent,
relays: List<NormalizedRelayUrl>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): BroadcastRelayListEvent {
val newRelayList = relays.map { RelayTag.assemble(it) }
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val publicTags = earlierVersion.tags.remove(RelayTag::match)
val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList)
return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags)
}
suspend fun create(
relays: List<NormalizedRelayUrl>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): BroadcastRelayListEvent {
val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray()
return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray)
}
suspend fun create(
relays: List<NormalizedRelayUrl>,
signer: NostrSignerSync,
createdAt: Long = TimeUtils.now(),
): BroadcastRelayListEvent {
val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray()
return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray)
}
suspend fun build(
publicRelays: List<NormalizedRelayUrl> = emptyList(),
privateRelays: List<NormalizedRelayUrl> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<BroadcastRelayListEvent>.() -> Unit = {},
) = eventTemplate<BroadcastRelayListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
broadcastRelays(publicRelays)
initializer()
}
}
}
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
@@ -39,6 +40,8 @@ fun TagArrayBuilder<BlockedRelayListEvent>.blockedRelays(relays: List<Normalized
fun TagArrayBuilder<TrustedRelayListEvent>.trustedRelays(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder<TrustedRelayListEvent>.trustedRelays(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) })
fun TagArrayBuilder<BroadcastRelayListEvent>.broadcastRelays(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) })
fun TagArrayBuilder<PrivateOutboxRelayListEvent>.privateRelays(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder<PrivateOutboxRelayListEvent>.privateRelays(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) })
fun TagArrayBuilder<RelaySetEvent>.relaySet(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder<RelaySetEvent>.relaySet(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) })