diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 46367f35e..ad3c409d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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.blockedRelays.BlockedRelayListDecryptionCache 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.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache @@ -242,6 +244,9 @@ class Account( val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer) 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 blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) @@ -280,15 +285,15 @@ class Account( val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) // 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 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 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 val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow @@ -1714,6 +1719,8 @@ class Account( suspend fun saveSearchRelayList(searchRelays: List) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) + suspend fun saveBroadcastRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays)) + suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a5504a4a3..5ce63e658 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -138,6 +138,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent 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.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -957,6 +958,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: BroadcastRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: CommunityDefinitionEvent, relay: NormalizedRelayUrl?, @@ -2709,6 +2716,7 @@ object LocalCache : ILocalCache { is BadgeProfilesEvent -> consume(event, relay, wasVerified) is BlockedRelayListEvent -> consume(event, relay, wasVerified) is BlossomServersEvent -> consume(event, relay, wasVerified) + is BroadcastRelayListEvent -> consume(event, relay, wasVerified) is BookmarkListEvent -> consume(event, relay, wasVerified) is CalendarEvent -> consume(event, relay, wasVerified) is CalendarDateSlotEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 001e086ac..58de33ab4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -34,6 +35,7 @@ class AccountOutboxRelayState( nip65: Nip65RelayListState, privateStorage: PrivateStorageRelayListState, local: LocalRelayListState, + broadcast: BroadcastRelayListState, scope: CoroutineScope, ) { val flow = @@ -41,14 +43,16 @@ class AccountOutboxRelayState( nip65.outboxFlow, privateStorage.flow, local.flow, - ) { nip65Inbox, privateOutBox, localRelays -> - nip65Inbox + privateOutBox + localRelays + broadcast.flow, + ) { nip65Inbox, privateOutBox, localRelays, broadcastRelays -> + nip65Inbox + privateOutBox + localRelays + broadcastRelays }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, nip65.outboxFlow.value + privateStorage.flow.value + - local.flow.value, + local.flow.value + + broadcast.flow.value, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt new file mode 100644 index 000000000..73ba12906 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt @@ -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(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt new file mode 100644 index 000000000..8cada24fe --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt @@ -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 = getBroadcastRelayListNote().flow().metadata.stateFlow + + fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent + + suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set { + 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): 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, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index b016b1eae..1f2a6fa7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +42,7 @@ class MergedFollowPlusMineRelayListsState( val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, val trustedRelayList: TrustedRelayListState, + val broadcastRelayList: BroadcastRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -54,6 +56,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow, localRelayList.flow, trustedRelayList.flow, + broadcastRelayList.flow, ), ::mergeLists, ).onStart { @@ -66,6 +69,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow.value, localRelayList.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) @@ -81,6 +85,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow.value, localRelayList.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index 77e279742..743481c34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState @@ -43,6 +44,7 @@ class TrustedRelayListsState( val dmRelayList: DmRelayListState, val searchRelayListState: SearchRelayListState, val trustedRelayList: TrustedRelayListState, + val broadcastRelayList: BroadcastRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -56,6 +58,7 @@ class TrustedRelayListsState( dmRelayList.flow, searchRelayListState.flow, trustedRelayList.flow, + broadcastRelayList.flow, ), ::mergeLists, ).onStart { @@ -68,6 +71,7 @@ class TrustedRelayListsState( dmRelayList.flow.value, searchRelayListState.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) @@ -83,6 +87,7 @@ class TrustedRelayListsState( dmRelayList.flow.value, searchRelayListState.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 774bb3bde..51a97c89c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent 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.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -55,6 +56,7 @@ val AccountInfoAndListsFromKeyKinds2 = listOf( BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, + BroadcastRelayListEvent.KIND, ) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index 34dff46d7..d988c7b12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent 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.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -43,8 +44,13 @@ val BasicAccountInfoKinds = SearchRelayListEvent.KIND, FileServersEvent.KIND, BlossomServersEvent.KIND, + ) + +val BasicAccountInfoKinds2 = + listOf( BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, + BroadcastRelayListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( @@ -61,7 +67,17 @@ fun filterBasicAccountInfoFromKeys( Filter( kinds = BasicAccountInfoKinds, 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, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 42fcd2704..675860072 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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.types.BadgeDisplay 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.DisplayFollowList 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.peopleList.PeopleListEvent 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.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -654,6 +656,7 @@ private fun RenderNoteRow( is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) is BlockedRelayListEvent -> DisplayBlockedRelayList(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 EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index d8382e305..2cb155e28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -234,6 +234,27 @@ fun DisplayTrustedRelayList( ) } +@Composable +fun DisplayBroadcastRelayList( + baseNote: Note, + backgroundColor: MutableState, + 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 fun DisplayRelaySet( relay: RelayListCard, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index ab81f80bc..35a189701 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -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.relays.blocked.BlockedRelayListViewModel 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.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems @@ -83,6 +85,7 @@ fun AllRelayListScreen( val trustedViewModel: TrustedRelayListViewModel = viewModel() val localViewModel: LocalRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel() + val broadcastViewModel: BroadcastRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -92,6 +95,7 @@ fun AllRelayListScreen( connectedViewModel.init(accountViewModel) blockedViewModel.init(accountViewModel) trustedViewModel.init(accountViewModel) + broadcastViewModel.init(accountViewModel) LaunchedEffect(accountViewModel) { dmViewModel.load() @@ -102,6 +106,7 @@ fun AllRelayListScreen( connectedViewModel.load() blockedViewModel.load() trustedViewModel.load() + broadcastViewModel.load() } MappedAllRelayListView( @@ -113,6 +118,7 @@ fun AllRelayListScreen( connectedViewModel, blockedViewModel, trustedViewModel, + broadcastViewModel, accountViewModel, nav, ) @@ -129,6 +135,7 @@ fun MappedAllRelayListView( connectedViewModel: ConnectedRelayListViewModel, blockedViewModel: BlockedRelayListViewModel, trustedViewModel: TrustedRelayListViewModel, + broadcastViewModel: BroadcastRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { @@ -141,6 +148,7 @@ fun MappedAllRelayListView( val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() + val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -154,6 +162,7 @@ fun MappedAllRelayListView( privateOutboxViewModel.clear() trustedViewModel.clear() blockedViewModel.clear() + broadcastViewModel.clear() newNav.popBack() }, onPost = { @@ -164,6 +173,7 @@ fun MappedAllRelayListView( privateOutboxViewModel.create() trustedViewModel.create() blockedViewModel.create() + broadcastViewModel.create() newNav.popBack() }, ) @@ -221,6 +231,15 @@ fun MappedAllRelayListView( } 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 { SettingsCategoryWithButton( stringRes(R.string.search_section), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt new file mode 100644 index 000000000..fb36e2080 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt @@ -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? = + account.broadcastRelayList.flow.value + .toList() + + override suspend fun saveRelayList(urlList: List) { + account.saveBroadcastRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt new file mode 100644 index 000000000..5fd691e58 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt @@ -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, + 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)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index c04fd1310..e22eb57ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -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.BadgeDisplay 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.DisplayFollowList 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.peopleList.PeopleListEvent 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.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -603,6 +605,8 @@ private fun FullBleedNoteCompose( DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is BroadcastRelayListEvent) { + DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FhirResourceEvent) { RenderFhirResource(baseNote, accountViewModel, nav) } else if (noteEvent is GitRepositoryEvent) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a9cdda2ec..89d142a5f 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1064,9 +1064,6 @@ Insert between 1–3 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50 Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com - Blocked Relays - Trusted Relays - DM Upload Relay Settings Public Outbox/Home Relays @@ -1091,9 +1088,15 @@ Local Relays List of relays that are running in this device. + Trusted Relays Trusted Relays Relays you trust to not need a Tor connection for + Broadcast Relays + Broadcast Relays + 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 + + Blocked Relays Blocked Relays Amethyst will never connect to these relays diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 0f14d4abc..6715c0368 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent 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.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -161,6 +162,7 @@ class EventFactory { BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) BlossomServersEvent.KIND -> BlossomServersEvent(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) CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig) CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt new file mode 100644 index 000000000..f09930df0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt @@ -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>, + 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 = 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, + 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, + 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, + 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 = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + broadcastRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index b8626bf5f..c6cb3e2e4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent 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.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -39,6 +40,8 @@ fun TagArrayBuilder.blockedRelays(relays: List.trustedRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.broadcastRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) })