Early support for Blocked and Trusted Relay Lists
This commit is contained in:
@@ -51,7 +51,9 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
@@ -96,6 +98,8 @@ private object PrefKeys {
|
||||
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
|
||||
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
|
||||
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_BLOCKED_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_TRUSTED_RELAY_LIST = "latestSearchRelayList"
|
||||
const val LATEST_MUTE_LIST = "latestMuteList"
|
||||
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
|
||||
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
|
||||
@@ -367,6 +371,24 @@ object LocalPreferences {
|
||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupBlockedRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_BLOCKED_RELAY_LIST,
|
||||
EventMapper.mapper.writeValueAsString(settings.backupBlockedRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.backupTrustedRelayList != null) {
|
||||
putString(
|
||||
PrefKeys.LATEST_TRUSTED_RELAY_LIST,
|
||||
EventMapper.mapper.writeValueAsString(settings.backupTrustedRelayList),
|
||||
)
|
||||
} else {
|
||||
remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
|
||||
}
|
||||
|
||||
if (settings.localRelayServers.value.isNotEmpty()) {
|
||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
|
||||
} else {
|
||||
@@ -562,6 +584,8 @@ object LocalPreferences {
|
||||
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||
val latestBlockedRelayList = parseEventOrNull<BlockedRelayListEvent>(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
|
||||
val latestTrustedRelayList = parseEventOrNull<TrustedRelayListEvent>(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
|
||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||
@@ -604,6 +628,8 @@ object LocalPreferences {
|
||||
backupNIP65RelayList = latestNip65RelayList,
|
||||
backupDMRelayList = latestDmRelayList,
|
||||
backupSearchRelayList = latestSearchRelayList,
|
||||
backupBlockedRelayList = latestBlockedRelayList,
|
||||
backupTrustedRelayList = latestTrustedRelayList,
|
||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||
backupMuteList = latestMuteList,
|
||||
backupAppSpecificData = latestAppSpecificData,
|
||||
|
||||
@@ -43,10 +43,12 @@ import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction
|
||||
import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.MuteListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
@@ -239,6 +241,8 @@ class Account(
|
||||
val searchRelayList = SearchRelayListState(signer, cache, scope, settings)
|
||||
val privateStorageRelayList = PrivateStorageRelayListState(signer, cache, scope, settings)
|
||||
val localRelayList = LocalRelayListState(signer, cache, scope, settings)
|
||||
val trustedRelayList = TrustedRelayListState(signer, cache, scope, settings)
|
||||
val blockedRelayList = BlockedRelayListState(signer, cache, scope, settings)
|
||||
|
||||
val kind3FollowList = FollowListState(signer, cache, scope, settings)
|
||||
val ephemeralChatList = EphemeralChatListState(signer, cache, scope, settings)
|
||||
@@ -264,14 +268,14 @@ class Account(
|
||||
val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||
val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope)
|
||||
|
||||
val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, scope)
|
||||
val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, scope)
|
||||
|
||||
// Follows Relays
|
||||
val followOutboxes = FollowListOutboxRelays(kind3FollowList, cache, scope)
|
||||
val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope)
|
||||
val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||
|
||||
// keeps a cache of the outbox relays for each author
|
||||
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, cache, scope).flow
|
||||
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow
|
||||
|
||||
// Merges all follow lists to create a single All Follows feed.
|
||||
val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope)
|
||||
@@ -283,6 +287,7 @@ class Account(
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
@@ -295,6 +300,7 @@ class Account(
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
@@ -307,6 +313,7 @@ class Account(
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
@@ -319,6 +326,7 @@ class Account(
|
||||
allFollows = allFollows.flow,
|
||||
locationFlow = geolocationFlow,
|
||||
followsRelays = followPlusAllMine.flow,
|
||||
blockedRelays = blockedRelayList.flow,
|
||||
signer = signer,
|
||||
scope = scope,
|
||||
).flow
|
||||
@@ -2100,6 +2108,16 @@ class Account(
|
||||
searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox)
|
||||
}
|
||||
|
||||
fun saveTrustedRelayList(trustedRelays: List<NormalizedRelayUrl>) {
|
||||
if (!isWriteable()) return
|
||||
trustedRelayList.saveRelayList(trustedRelays, ::sendMyPublicAndPrivateOutbox)
|
||||
}
|
||||
|
||||
fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) {
|
||||
if (!isWriteable()) return
|
||||
blockedRelayList.saveRelayList(blockedRelays, ::sendMyPublicAndPrivateOutbox)
|
||||
}
|
||||
|
||||
fun sendNip65RelayList(relays: List<AdvertisedRelayInfo>) {
|
||||
if (!isWriteable()) return
|
||||
nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox)
|
||||
|
||||
@@ -40,7 +40,9 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
|
||||
@@ -116,6 +118,8 @@ class AccountSettings(
|
||||
var backupDMRelayList: ChatMessageRelayListEvent? = null,
|
||||
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
|
||||
var backupSearchRelayList: SearchRelayListEvent? = null,
|
||||
var backupBlockedRelayList: BlockedRelayListEvent? = null,
|
||||
var backupTrustedRelayList: TrustedRelayListEvent? = null,
|
||||
var backupMuteList: MuteListEvent? = null,
|
||||
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
|
||||
var backupAppSpecificData: AppSpecificDataEvent? = null,
|
||||
@@ -351,6 +355,26 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
|
||||
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupBlockedRelayList?.id != newBlockedRelayList.id) {
|
||||
backupBlockedRelayList = newBlockedRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrustedRelayList(newTrustedRelayList: TrustedRelayListEvent?) {
|
||||
if (newTrustedRelayList == null || newTrustedRelayList.tags.isEmpty()) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupTrustedRelayList?.id != newTrustedRelayList.id) {
|
||||
backupTrustedRelayList = newTrustedRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePrivateHomeRelayList(newPrivateHomeRelayList: PrivateOutboxRelayListEvent?) {
|
||||
if (newPrivateHomeRelayList == null || newPrivateHomeRelayList.tags.isEmpty()) return
|
||||
|
||||
|
||||
@@ -125,12 +125,14 @@ import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
@@ -972,6 +974,18 @@ object LocalCache : ILocalCache {
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
private fun consume(
|
||||
event: BlockedRelayListEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
private fun consume(
|
||||
event: TrustedRelayListEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
private fun consume(
|
||||
event: CommunityDefinitionEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -3045,6 +3059,7 @@ object LocalCache : ILocalCache {
|
||||
is BadgeAwardEvent -> consume(event, relay, wasVerified)
|
||||
is BadgeDefinitionEvent -> consume(event, relay, wasVerified)
|
||||
is BadgeProfilesEvent -> consume(event, relay, wasVerified)
|
||||
is BlockedRelayListEvent -> consume(event, relay, wasVerified)
|
||||
is BlossomServersEvent -> consume(event, relay, wasVerified)
|
||||
is BookmarkListEvent -> consume(event, relay, wasVerified)
|
||||
is CalendarEvent -> consume(event, relay, wasVerified)
|
||||
@@ -3126,6 +3141,7 @@ object LocalCache : ILocalCache {
|
||||
is TextNoteModificationEvent -> consume(event, relay, wasVerified)
|
||||
is TorrentEvent -> consume(event, relay, wasVerified)
|
||||
is TorrentCommentEvent -> consume(event, relay, wasVerified)
|
||||
is TrustedRelayListEvent -> consume(event, relay, wasVerified)
|
||||
is VideoHorizontalEvent -> consume(event, relay, wasVerified)
|
||||
is VideoVerticalEvent -> consume(event, relay, wasVerified)
|
||||
is WikiNoteEvent -> consume(event, relay, wasVerified)
|
||||
|
||||
+14
-7
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
@@ -33,17 +34,20 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.flatten
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.minus
|
||||
import kotlin.collections.toSet
|
||||
|
||||
class FollowListOutboxRelays(
|
||||
kind3Follows: FollowListState,
|
||||
blockedRelayList: BlockedRelayListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@@ -57,26 +61,29 @@ class FollowListOutboxRelays(
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
|
||||
fun combineAllFlows(
|
||||
flows: List<StateFlow<NoteState>>,
|
||||
blockedRelays: Set<NormalizedRelayUrl>,
|
||||
): Flow<Set<NormalizedRelayUrl>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
relayListNotes.mapNotNull {
|
||||
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
|
||||
}
|
||||
}.map {
|
||||
it.flatten().toSet()
|
||||
it.flatten().toSet().minus(blockedRelays)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest { followList ->
|
||||
combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedRelays ->
|
||||
val flows: List<StateFlow<NoteState>> = allRelayListFlows(followList.authors)
|
||||
val relayListFlows = combineAllFlows(flows)
|
||||
val relayListFlows = combineAllFlows(flows, blockedRelays)
|
||||
emitAll(relayListFlows)
|
||||
}.onStart {
|
||||
val blocked = blockedRelayList.flow.value.toSet()
|
||||
kind3Follows.flow.value.authors
|
||||
.mapNotNull {
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()
|
||||
getNIP65RelayList(it)?.writeRelaysNorm()?.minus(blocked)
|
||||
}.flatten()
|
||||
.toSet()
|
||||
}.flowOn(Dispatchers.Default)
|
||||
|
||||
+13
-5
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
@@ -34,15 +35,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlin.collections.map
|
||||
|
||||
class FollowsPerOutboxRelay(
|
||||
kind3Follows: FollowListState,
|
||||
blockedRelayList: BlockedRelayListState,
|
||||
val cache: LocalCache,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@@ -56,34 +58,40 @@ class FollowsPerOutboxRelay(
|
||||
|
||||
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
|
||||
|
||||
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
fun combineAllFlows(
|
||||
flows: List<StateFlow<NoteState>>,
|
||||
blockedList: Set<NormalizedRelayUrl>,
|
||||
): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
combine(flows) { relayListNotes: Array<NoteState> ->
|
||||
mapOfSet {
|
||||
relayListNotes.forEach { noteState ->
|
||||
noteState.note.author?.pubkeyHex?.let { authorHex ->
|
||||
(noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay ->
|
||||
if (relay !in blockedList) {
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||
kind3Follows.flow
|
||||
.transformLatest { followList ->
|
||||
combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedList ->
|
||||
val flows: List<StateFlow<NoteState>> = allRelayListFlows(followList.authors)
|
||||
val relayListFlows = combineAllFlows(flows)
|
||||
val relayListFlows = combineAllFlows(flows, blockedList)
|
||||
emitAll(relayListFlows)
|
||||
}.onStart {
|
||||
emit(
|
||||
mapOfSet {
|
||||
kind3Follows.flow.value.authors.map { authorHex ->
|
||||
getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay ->
|
||||
if (relay !in blockedRelayList.flow.value) {
|
||||
add(relay, authorHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BlockedRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress())
|
||||
|
||||
fun getBlockedRelayListFlow(): StateFlow<NoteState> = getBlockedRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent
|
||||
|
||||
fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getBlockedRelayListFlow()
|
||||
.map { normalizeBlockedRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
blockedRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val relayListForBlocked = getBlockedRelayList()
|
||||
|
||||
if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) {
|
||||
BlockedRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForBlocked,
|
||||
relays = blockedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
BlockedRelayListEvent.createFromScratch(
|
||||
relays = blockedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupBlockedRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
|
||||
getBlockedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? BlockedRelayListEvent)?.let {
|
||||
settings.updateBlockedRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TrustedRelayListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: AccountSettings,
|
||||
) {
|
||||
fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getTrustedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getTrustedRelayListAddress())
|
||||
|
||||
fun getTrustedRelayListFlow(): StateFlow<NoteState> = getTrustedRelayListNote().flow().metadata.stateFlow
|
||||
|
||||
fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent
|
||||
|
||||
fun normalizeTrustedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
|
||||
val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList
|
||||
return event?.relays()?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
val flow =
|
||||
getTrustedRelayListFlow()
|
||||
.map { normalizeTrustedRelayListWithBackup(it.note) }
|
||||
.onStart { emit(normalizeTrustedRelayListWithBackup(getTrustedRelayListNote())) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun saveRelayList(
|
||||
trustedRelays: List<NormalizedRelayUrl>,
|
||||
onDone: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val relayListForTrusted = getTrustedRelayList()
|
||||
|
||||
if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) {
|
||||
TrustedRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForTrusted,
|
||||
relays = trustedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
} else {
|
||||
TrustedRelayListEvent.createFromScratch(
|
||||
relays = trustedRelays,
|
||||
signer = signer,
|
||||
onReady = onDone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.backupTrustedRelayList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start")
|
||||
getTrustedRelayListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}")
|
||||
(it.note.event as? TrustedRelayListEvent)?.let {
|
||||
settings.updateTrustedRelayList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-7
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -41,32 +42,33 @@ class TrustedRelayListsState(
|
||||
val localRelayList: LocalRelayListState,
|
||||
val dmRelayList: DmRelayListState,
|
||||
val searchRelayListState: SearchRelayListState,
|
||||
val trustedRelayList: TrustedRelayListState,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun mergeLists(
|
||||
nip65: Set<NormalizedRelayUrl>,
|
||||
private: Set<NormalizedRelayUrl>,
|
||||
local: Set<NormalizedRelayUrl>,
|
||||
dm: Set<NormalizedRelayUrl>,
|
||||
search: Set<NormalizedRelayUrl>,
|
||||
): Set<NormalizedRelayUrl> = nip65 + private + local + dm + search
|
||||
fun mergeLists(lists: Array<Set<NormalizedRelayUrl>>): Set<NormalizedRelayUrl> = lists.reduce { acc, set -> acc + set }
|
||||
|
||||
val flow: StateFlow<Set<NormalizedRelayUrl>> =
|
||||
combine(
|
||||
listOf(
|
||||
nip65RelayList.allFlow,
|
||||
privateOutboxRelayList.flow,
|
||||
localRelayList.flow,
|
||||
dmRelayList.flow,
|
||||
searchRelayListState.flow,
|
||||
trustedRelayList.flow,
|
||||
),
|
||||
::mergeLists,
|
||||
).onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
arrayOf(
|
||||
nip65RelayList.allFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
dmRelayList.flow.value,
|
||||
searchRelayListState.flow.value,
|
||||
trustedRelayList.flow.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
@@ -74,11 +76,14 @@ class TrustedRelayListsState(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
mergeLists(
|
||||
arrayOf(
|
||||
nip65RelayList.allFlow.value,
|
||||
privateOutboxRelayList.flow.value,
|
||||
localRelayList.flow.value,
|
||||
dmRelayList.flow.value,
|
||||
searchRelayListState.flow.value,
|
||||
trustedRelayList.flow.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+3
-2
@@ -50,18 +50,19 @@ class FeedTopNavFilterState(
|
||||
val allFollows: StateFlow<FollowListState.Kind3Follows>,
|
||||
val locationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val signer: NostrSigner,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
fun loadFlowsFor(listName: String): IFeedFlowsType =
|
||||
when (listName) {
|
||||
GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays)
|
||||
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays)
|
||||
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays)
|
||||
AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays)
|
||||
else -> {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
if (note != null) {
|
||||
NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays)
|
||||
NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays)
|
||||
} else {
|
||||
UnknownFeedFlow(listName)
|
||||
}
|
||||
|
||||
+4
-3
@@ -50,6 +50,7 @@ class AllFollowsByOutboxTopNavFilter(
|
||||
val geotags: Set<String>? = null,
|
||||
val communities: Set<String>? = null,
|
||||
val defaultRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.Companion.toScope(it) }
|
||||
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.Companion.toScope(it) }
|
||||
@@ -91,8 +92,8 @@ class AllFollowsByOutboxTopNavFilter(
|
||||
MutableStateFlow(emptyMap())
|
||||
}
|
||||
|
||||
return combine(authorsPerRelay, communitiesPerRelay, defaultRelays) { perRelayAuthors, perRelayCommunities, default ->
|
||||
val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).ifEmpty { default }
|
||||
return combine(authorsPerRelay, communitiesPerRelay, defaultRelays, blockedRelays) { perRelayAuthors, perRelayCommunities, default, blockedRelays ->
|
||||
val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).filter { it !in blockedRelays }.ifEmpty { default }
|
||||
|
||||
AllFollowsByOutboxTopNavPerRelayFilterSet(
|
||||
allRelays.associateWith {
|
||||
@@ -121,7 +122,7 @@ class AllFollowsByOutboxTopNavFilter(
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).ifEmpty { defaultRelays.value }
|
||||
val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).filter { it !in blockedRelays.value }.ifEmpty { defaultRelays.value }
|
||||
|
||||
return AllFollowsByOutboxTopNavPerRelayFilterSet(
|
||||
allRelays.associateWith {
|
||||
|
||||
+3
@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.map
|
||||
class AllFollowsFeedFlow(
|
||||
val allFollows: StateFlow<FollowListState.Kind3Follows?>,
|
||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter =
|
||||
if (kind3 != null) {
|
||||
@@ -40,11 +41,13 @@ class AllFollowsFeedFlow(
|
||||
geotags = kind3.geotags,
|
||||
communities = kind3.communities,
|
||||
defaultRelays = followsRelays,
|
||||
blockedRelays = blockedRelays,
|
||||
)
|
||||
} else {
|
||||
AllFollowsByOutboxTopNavFilter(
|
||||
authors = emptySet(),
|
||||
defaultRelays = followsRelays,
|
||||
blockedRelays = blockedRelays,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+22
-19
@@ -48,24 +48,25 @@ class NoteFeedFlow(
|
||||
val metadataFlow: StateFlow<NoteState?>,
|
||||
val signer: NostrSigner,
|
||||
val allFollowRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedFlowsType {
|
||||
fun process(noteEvent: Event): IFeedTopNavFilter =
|
||||
when (noteEvent) {
|
||||
is PeopleListEvent -> {
|
||||
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)
|
||||
} else {
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)
|
||||
}
|
||||
}
|
||||
is MuteListEvent -> {
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users)
|
||||
MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays)
|
||||
}
|
||||
is FollowListEvent -> {
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet())
|
||||
AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays)
|
||||
}
|
||||
is CommunityListEvent -> {
|
||||
AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet())
|
||||
AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet(), blockedRelays)
|
||||
}
|
||||
is HashtagListEvent -> {
|
||||
HashtagTopNavFilter(noteEvent.publicAndCachedPrivateHashtags(), allFollowRelays)
|
||||
@@ -78,44 +79,45 @@ class NoteFeedFlow(
|
||||
community = noteEvent.addressTag(),
|
||||
authors = noteEvent.moderatorKeys().toSet().ifEmpty { null },
|
||||
relays = noteEvent.relayUrls().toSet(),
|
||||
blockedRelays = blockedRelays,
|
||||
)
|
||||
}
|
||||
else -> AuthorsByOutboxTopNavFilter(emptySet())
|
||||
else -> AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)
|
||||
}
|
||||
|
||||
suspend fun FlowCollector<IFeedTopNavFilter>.process(noteEvent: Event) {
|
||||
when (noteEvent) {
|
||||
is PeopleListEvent -> {
|
||||
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users))
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users))
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
} else {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users))
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(AuthorsByOutboxTopNavFilter(it.users))
|
||||
emit(AuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
}
|
||||
}
|
||||
is MuteListEvent -> {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users))
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateUsersAndWords(signer)?.let {
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users))
|
||||
emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays))
|
||||
}
|
||||
}
|
||||
is FollowListEvent -> {
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet()))
|
||||
emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays))
|
||||
}
|
||||
is CommunityListEvent -> {
|
||||
emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet()))
|
||||
emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet(), blockedRelays))
|
||||
|
||||
noteEvent.publicAndPrivateCommunities(signer)?.let {
|
||||
val communities = it.map { it.addressId }.toSet()
|
||||
emit(AllCommunitiesTopNavFilter(communities))
|
||||
emit(AllCommunitiesTopNavFilter(communities, blockedRelays))
|
||||
}
|
||||
}
|
||||
is HashtagListEvent -> {
|
||||
@@ -138,12 +140,13 @@ class NoteFeedFlow(
|
||||
community = noteEvent.addressTag(),
|
||||
authors = noteEvent.moderatorKeys().toSet().ifEmpty { null },
|
||||
relays = noteEvent.relayUrls().toSet(),
|
||||
blockedRelays = blockedRelays,
|
||||
),
|
||||
)
|
||||
}
|
||||
else ->
|
||||
emit(
|
||||
AuthorsByOutboxTopNavFilter(emptySet()),
|
||||
AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -153,7 +156,7 @@ class NoteFeedFlow(
|
||||
metadataFlow.transformLatest { noteState ->
|
||||
val noteEvent = noteState?.note?.event
|
||||
if (noteEvent == null) {
|
||||
AuthorsByOutboxTopNavFilter(emptySet())
|
||||
AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)
|
||||
} else {
|
||||
process(noteEvent)
|
||||
}
|
||||
@@ -162,7 +165,7 @@ class NoteFeedFlow(
|
||||
override fun startValue(): IFeedTopNavFilter {
|
||||
val noteEvent = metadataFlow.value?.note?.event
|
||||
if (noteEvent == null) {
|
||||
return AuthorsByOutboxTopNavFilter(emptySet())
|
||||
return AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)
|
||||
} else {
|
||||
return process(noteEvent)
|
||||
}
|
||||
@@ -171,7 +174,7 @@ class NoteFeedFlow(
|
||||
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||
val noteEvent = metadataFlow.value?.note?.event
|
||||
if (noteEvent == null) {
|
||||
collector.emit(AuthorsByOutboxTopNavFilter(emptySet()))
|
||||
collector.emit(AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays))
|
||||
} else {
|
||||
collector.process(noteEvent)
|
||||
}
|
||||
|
||||
+14
-6
@@ -29,10 +29,14 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlin.collections.minus
|
||||
|
||||
@Immutable
|
||||
class AllCommunitiesTopNavFilter(
|
||||
val communities: Set<String>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey): Boolean = true
|
||||
|
||||
@@ -43,13 +47,17 @@ class AllCommunitiesTopNavFilter(
|
||||
map.mapValues { AllCommunitiesTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AllCommunitiesTopNavPerRelayFilterSet> =
|
||||
CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) {
|
||||
convert(it)
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AllCommunitiesTopNavPerRelayFilterSet> {
|
||||
val communitiesPerRelay = CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it }
|
||||
|
||||
return combine(communitiesPerRelay, blockedRelays) { communitiesPerRelay, blockedRelays ->
|
||||
convert(communitiesPerRelay.minus(blockedRelays))
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet =
|
||||
CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) {
|
||||
convert(it)
|
||||
override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet {
|
||||
val communitiesPerRelay = CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it }
|
||||
|
||||
return convert(communitiesPerRelay.minus(blockedRelays.value))
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -29,10 +29,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@Immutable
|
||||
class AuthorsByOutboxTopNavFilter(
|
||||
val authors: Set<String>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = pubkey in authors
|
||||
|
||||
@@ -48,7 +51,17 @@ class AuthorsByOutboxTopNavFilter(
|
||||
map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsByOutboxTopNavPerRelayFilterSet> = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert)
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<AuthorsByOutboxTopNavPerRelayFilterSet> {
|
||||
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it }
|
||||
|
||||
override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert)
|
||||
return combine(authorsPerRelay, blockedRelays) { authors, blocked ->
|
||||
convert(authors.minus(blocked))
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet {
|
||||
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it }
|
||||
|
||||
return convert(authorsPerRelay.minus(blockedRelays.value))
|
||||
}
|
||||
}
|
||||
|
||||
+20
-15
@@ -31,13 +31,16 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableN
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Immutable
|
||||
class SingleCommunityTopNavFilter(
|
||||
val community: String,
|
||||
val authors: Set<String>?,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = authors == null || pubkey in authors
|
||||
|
||||
@@ -53,20 +56,22 @@ class SingleCommunityTopNavFilter(
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<SingleCommunityTopNavPerRelayFilterSet> {
|
||||
// relay field takes priority
|
||||
if (relays.isNotEmpty()) {
|
||||
return MutableStateFlow(
|
||||
return blockedRelays.map { blocked ->
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
relays.associateWith {
|
||||
relays.minus(blocked).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (authors != null) {
|
||||
// go by authors
|
||||
return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) {
|
||||
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it }
|
||||
|
||||
return combine(authorsPerRelay, blockedRelays) { authorsPerRelay, blocked ->
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
it.mapValues {
|
||||
authorsPerRelay.minus(blocked).mapValues {
|
||||
SingleCommunityTopNavPerRelayFilter(community, it.value)
|
||||
},
|
||||
)
|
||||
@@ -74,20 +79,20 @@ class SingleCommunityTopNavFilter(
|
||||
}
|
||||
|
||||
// go by hints
|
||||
return MutableStateFlow(
|
||||
return blockedRelays.map { blocked ->
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
cache.relayHints.hintsForAddress(community).associateWith {
|
||||
cache.relayHints.hintsForAddress(community).minus(blocked).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): SingleCommunityTopNavPerRelayFilterSet {
|
||||
// relay field takes priority
|
||||
if (relays.isNotEmpty()) {
|
||||
return SingleCommunityTopNavPerRelayFilterSet(
|
||||
relays.associateWith {
|
||||
relays.minus(blockedRelays.value).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
)
|
||||
@@ -95,18 +100,18 @@ class SingleCommunityTopNavFilter(
|
||||
|
||||
if (authors != null) {
|
||||
// go by authors
|
||||
return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) {
|
||||
SingleCommunityTopNavPerRelayFilterSet(
|
||||
it.mapValues {
|
||||
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it }
|
||||
|
||||
return SingleCommunityTopNavPerRelayFilterSet(
|
||||
authorsPerRelay.minus(blockedRelays.value).mapValues {
|
||||
SingleCommunityTopNavPerRelayFilter(community, it.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// go by hints
|
||||
return SingleCommunityTopNavPerRelayFilterSet(
|
||||
cache.relayHints.hintsForAddress(community).associateWith {
|
||||
cache.relayHints.hintsForAddress(community).minus(blockedRelays.value).associateWith {
|
||||
SingleCommunityTopNavPerRelayFilter(community, authors)
|
||||
},
|
||||
)
|
||||
|
||||
+15
-2
@@ -29,10 +29,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@Immutable
|
||||
class MutedAuthorsByOutboxTopNavFilter(
|
||||
val authors: Set<String>,
|
||||
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||
) : IFeedTopNavFilter {
|
||||
override fun matchAuthor(pubkey: HexKey) = pubkey in authors
|
||||
|
||||
@@ -48,7 +51,17 @@ class MutedAuthorsByOutboxTopNavFilter(
|
||||
map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) },
|
||||
)
|
||||
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<MutedAuthorsByOutboxTopNavPerRelayFilterSet> = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert)
|
||||
override fun toPerRelayFlow(cache: LocalCache): Flow<MutedAuthorsByOutboxTopNavPerRelayFilterSet> {
|
||||
val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it }
|
||||
|
||||
override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert)
|
||||
return combine(authorsPerRelay, blockedRelays) { authors, blocked ->
|
||||
convert(authors.minus(blocked))
|
||||
}
|
||||
}
|
||||
|
||||
override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet {
|
||||
val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it }
|
||||
|
||||
return convert(authorsPerRelay.minus(blockedRelays.value))
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -31,6 +31,8 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
@@ -49,6 +51,12 @@ val AccountInfoAndListsFromKeyKinds =
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
)
|
||||
|
||||
val AccountInfoAndListsFromKeyKinds2 =
|
||||
listOf(
|
||||
BlockedRelayListEvent.KIND,
|
||||
TrustedRelayListEvent.KIND,
|
||||
)
|
||||
|
||||
val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND)
|
||||
val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG))
|
||||
|
||||
@@ -70,6 +78,16 @@ fun filterAccountInfoAndListsFromKey(
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = AccountInfoAndListsFromKeyKinds2,
|
||||
authors = listOf(pubkey),
|
||||
limit = 20,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
|
||||
+4
@@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
@@ -41,6 +43,8 @@ val BasicAccountInfoKinds =
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
BlossomServersEvent.KIND,
|
||||
BlockedRelayListEvent.KIND,
|
||||
TrustedRelayListEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterBasicAccountInfoFromKeys(
|
||||
|
||||
@@ -127,7 +127,7 @@ fun AppNavigation(
|
||||
composableFromEnd<Route.Settings> { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
||||
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(it.toAdd, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, nav) }
|
||||
|
||||
@@ -455,7 +455,7 @@ fun ListContent(
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = {
|
||||
nav.closeDrawer()
|
||||
nav.nav(Route.EditRelays())
|
||||
nav.nav(Route.EditRelays)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -53,9 +53,7 @@ sealed class Route {
|
||||
|
||||
@Serializable object EditProfile : Route()
|
||||
|
||||
@Serializable data class EditRelays(
|
||||
val toAdd: String? = null,
|
||||
) : Route()
|
||||
@Serializable object EditRelays : Route()
|
||||
|
||||
@Serializable data class Nip47NWCSetup(
|
||||
val nip47: String? = null,
|
||||
|
||||
@@ -78,12 +78,14 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward
|
||||
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.DisplayDMRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EmptyState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
|
||||
@@ -181,10 +183,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
@@ -635,6 +639,8 @@ private fun RenderNoteRow(
|
||||
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
||||
is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||
|
||||
@@ -37,6 +37,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -57,7 +59,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -188,6 +192,58 @@ fun DisplaySearchRelayList(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayBlockedRelayList(
|
||||
baseNote: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? BlockedRelayListEvent ?: return
|
||||
|
||||
val relays by
|
||||
remember(baseNote) {
|
||||
mutableStateOf(
|
||||
noteEvent.relays().toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
DisplayRelaySet(
|
||||
relays,
|
||||
stringRes(id = R.string.blocked_relays_title),
|
||||
null,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayTrustedRelayList(
|
||||
baseNote: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? TrustedRelayListEvent ?: return
|
||||
|
||||
val relays by
|
||||
remember(baseNote) {
|
||||
mutableStateOf(
|
||||
noteEvent.relays().toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
DisplayRelaySet(
|
||||
relays,
|
||||
stringRes(id = R.string.trusted_relays_title),
|
||||
null,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayRelaySet(
|
||||
relays: ImmutableList<NormalizedRelayUrl>,
|
||||
@@ -280,14 +336,16 @@ private fun RelayOptionsAction(
|
||||
nav: INav,
|
||||
) {
|
||||
val isCurrentlyOnTheUsersList by observeUserRelayIntoList(accountViewModel.userProfile(), relay, accountViewModel)
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
if (isCurrentlyOnTheUsersList) {
|
||||
AddRelayButton {
|
||||
nav.nav(Route.EditRelays(relay.url))
|
||||
clipboardManager.setText(AnnotatedString(relay.url))
|
||||
nav.nav(Route.EditRelays)
|
||||
}
|
||||
} else {
|
||||
RemoveRelayButton {
|
||||
nav.nav(Route.EditRelays(relay.url))
|
||||
nav.nav(Route.EditRelays)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class FollowingEphemeralChatSubAssembler(
|
||||
userJobMap[key.account.userProfile()] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.Default) {
|
||||
key.account.ephemeralChatList.liveEphemeralChatList.sample(5000).collectLatest {
|
||||
key.account.ephemeralChatList.liveEphemeralChatList.sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ class FollowingPublicChatSubAssembler(
|
||||
userJobMap[key.account.userProfile()] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.Default) {
|
||||
key.account.publicChatList.flowSet.sample(5000).collectLatest {
|
||||
key.account.publicChatList.flowSet.sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ class DiscoveryFollowsDiscoverySubAssembler1(
|
||||
}
|
||||
},
|
||||
key.scope.launch(Dispatchers.Default) {
|
||||
key.followsPerRelayFlow().sample(5000).collectLatest {
|
||||
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class DiscoveryFollowsDiscoverySubAssembler2(
|
||||
}
|
||||
},
|
||||
key.scope.launch(Dispatchers.Default) {
|
||||
key.followsPerRelayFlow().sample(5000).collectLatest {
|
||||
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class DiscoveryFollowsDiscoverySubAssembler3(
|
||||
}
|
||||
},
|
||||
key.scope.launch(Dispatchers.Default) {
|
||||
key.followsPerRelayFlow().sample(5000).collectLatest {
|
||||
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+6
-2
@@ -26,6 +26,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@@ -46,6 +48,7 @@ fun RelayFeedView(
|
||||
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
val listState = rememberLazyListState()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
@@ -56,10 +59,11 @@ fun RelayFeedView(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
onAddRelay = {
|
||||
nav.nav(Route.EditRelays(item.url.url))
|
||||
clipboardManager.setText(AnnotatedString(item.url.url))
|
||||
nav.nav(Route.EditRelays)
|
||||
},
|
||||
onRemoveRelay = {
|
||||
nav.nav(Route.EditRelays(item.url.url))
|
||||
nav.nav(Route.EditRelays)
|
||||
},
|
||||
)
|
||||
HorizontalDivider(
|
||||
|
||||
+35
-3
@@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton
|
||||
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.common.relaySetupInfoBuilder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems
|
||||
@@ -68,6 +70,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65Hom
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer
|
||||
@@ -79,17 +83,15 @@ import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
fun AllRelayListScreen(
|
||||
relayToAdd: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
MappedAllRelayListView(relayToAdd ?: "", accountViewModel, nav)
|
||||
MappedAllRelayListView(accountViewModel, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MappedAllRelayListView(
|
||||
relayToAdd: String = "",
|
||||
accountViewModel: AccountViewModel,
|
||||
newNav: INav,
|
||||
) {
|
||||
@@ -106,6 +108,12 @@ fun MappedAllRelayListView(
|
||||
val searchViewModel: SearchRelayListViewModel = viewModel()
|
||||
val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val blockedViewModel: BlockedRelayListViewModel = viewModel()
|
||||
val blockedFeedState by blockedViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val trustedViewModel: TrustedRelayListViewModel = viewModel()
|
||||
val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val localViewModel: LocalRelayListViewModel = viewModel()
|
||||
val localFeedState by localViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -119,6 +127,8 @@ fun MappedAllRelayListView(
|
||||
localViewModel.load(accountViewModel.account)
|
||||
privateOutboxViewModel.load(accountViewModel.account)
|
||||
connectedViewModel.load(accountViewModel.account)
|
||||
blockedViewModel.load(accountViewModel.account)
|
||||
trustedViewModel.load(accountViewModel.account)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -148,6 +158,8 @@ fun MappedAllRelayListView(
|
||||
searchViewModel.create()
|
||||
localViewModel.create()
|
||||
privateOutboxViewModel.create()
|
||||
trustedViewModel.create()
|
||||
blockedViewModel.create()
|
||||
newNav.popBack()
|
||||
},
|
||||
true,
|
||||
@@ -164,6 +176,8 @@ fun MappedAllRelayListView(
|
||||
searchViewModel.clear()
|
||||
localViewModel.clear()
|
||||
privateOutboxViewModel.clear()
|
||||
trustedViewModel.clear()
|
||||
blockedViewModel.clear()
|
||||
newNav.popBack()
|
||||
},
|
||||
)
|
||||
@@ -248,6 +262,24 @@ fun MappedAllRelayListView(
|
||||
}
|
||||
renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav)
|
||||
|
||||
item {
|
||||
SettingsCategory(
|
||||
stringRes(R.string.trusted_section),
|
||||
stringRes(R.string.trusted_section_explainer),
|
||||
SettingsCategorySpacingModifier,
|
||||
)
|
||||
}
|
||||
renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, newNav)
|
||||
|
||||
item {
|
||||
SettingsCategory(
|
||||
stringRes(R.string.blocked_section),
|
||||
stringRes(R.string.blocked_section_explainer),
|
||||
SettingsCategorySpacingModifier,
|
||||
)
|
||||
}
|
||||
renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, newNav)
|
||||
|
||||
item {
|
||||
SettingsCategory(
|
||||
stringRes(R.string.connected_section),
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked
|
||||
|
||||
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.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.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 BlockedRelayList(
|
||||
postViewModel: BlockedRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val newNav = rememberExtendedNav(nav, onClose)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
renderBlockedItems(feedState, postViewModel, accountViewModel, newNav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderBlockedItems(
|
||||
feedState: List<BasicRelaySetupInfo>,
|
||||
postViewModel: BlockedRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
itemsIndexed(feedState, key = { _, item -> "Trusted" + item.relay }) { index, item ->
|
||||
BasicRelaySetupInfoDialog(
|
||||
item,
|
||||
onDelete = { postViewModel.deleteRelay(item) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class BlockedRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||
override fun getRelayList(): List<NormalizedRelayUrl>? = account.blockedRelayList.getBlockedRelayList()?.relays()
|
||||
|
||||
override fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||
account.saveBlockedRelayList(urlList)
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted
|
||||
|
||||
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.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.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 TrustedRelayList(
|
||||
postViewModel: TrustedRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val newNav = rememberExtendedNav(nav, onClose)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
renderTrustedItems(feedState, postViewModel, accountViewModel, newNav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderTrustedItems(
|
||||
feedState: List<BasicRelaySetupInfo>,
|
||||
postViewModel: TrustedRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
itemsIndexed(feedState, key = { _, item -> "Trusted" + item.relay }) { index, item ->
|
||||
BasicRelaySetupInfoDialog(
|
||||
item,
|
||||
onDelete = { postViewModel.deleteRelay(item) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class TrustedRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||
override fun getRelayList(): List<NormalizedRelayUrl>? = account.trustedRelayList.getTrustedRelayList()?.relays()
|
||||
|
||||
override fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||
account.saveTrustedRelayList(urlList)
|
||||
}
|
||||
}
|
||||
+8
@@ -117,12 +117,14 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
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.DisplayDMRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
|
||||
@@ -201,10 +203,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup
|
||||
@@ -589,6 +593,10 @@ private fun FullBleedNoteCompose(
|
||||
DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is SearchRelayListEvent) {
|
||||
DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is BlockedRelayListEvent) {
|
||||
DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is TrustedRelayListEvent) {
|
||||
DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is FhirResourceEvent) {
|
||||
RenderFhirResource(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is GitRepositoryEvent) {
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ class VideoOutboxEventsFilterSubAssembler(
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.scope.launch(Dispatchers.Default) {
|
||||
key.followsPerRelayFlow().sample(5000).collectLatest {
|
||||
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1052,6 +1052,9 @@
|
||||
<string name="search_relays_not_found_editing">Insert between 1–3 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="blocked_relays_title">Blocked Relays</string>
|
||||
<string name="trusted_relays_title">Trusted Relays</string>
|
||||
|
||||
<string name="dm_upload">DM Upload</string>
|
||||
<string name="relay_settings">Relay Settings</string>
|
||||
<string name="public_home_section">Public Outbox/Home Relays</string>
|
||||
@@ -1076,6 +1079,12 @@
|
||||
<string name="local_section">Local Relays</string>
|
||||
<string name="local_section_explainer">List of relays that are running in this device.</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="blocked_section">Blocked Relays</string>
|
||||
<string name="blocked_section_explainer">Amethyst will never connect to these relays</string>
|
||||
|
||||
<string name="zap_the_devs_title">Zap the Devs!</string>
|
||||
<string name="zap_the_devs_description">Your donation helps us make a difference. Every sat counts!</string>
|
||||
<string name="donate_now">Donate Now</string>
|
||||
|
||||
@@ -74,12 +74,14 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
|
||||
@@ -145,6 +147,7 @@ class EventFactory {
|
||||
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
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)
|
||||
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -257,6 +260,7 @@ class EventFactory {
|
||||
TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -70,7 +71,8 @@ class NostrClient(
|
||||
reqs + counts + outbox
|
||||
}.onStart {
|
||||
activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value
|
||||
}.onEach {
|
||||
}.debounce(300)
|
||||
.onEach {
|
||||
relayPool.updatePool(it)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
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.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip50Search.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class BlockedRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10006
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(AltTag.assemble("Relay list to use for trusted connections"))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: BlockedRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BlockedRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BlockedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
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.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip50Search.tags.RelayTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class TrustedRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun relays(): List<NormalizedRelayUrl> = tags.mapNotNull(RelayTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10089
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createTagArray(relays: List<NormalizedRelayUrl>): Array<Array<String>> =
|
||||
relays
|
||||
.map {
|
||||
RelayTag.assemble(it)
|
||||
}.plusElement(AltTag.assemble("Relay list to use for trusted connections"))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: TrustedRelayListEvent,
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.tags
|
||||
.filter(RelayTag::notMatch)
|
||||
.plus(
|
||||
relays.map {
|
||||
RelayTag.assemble(it)
|
||||
},
|
||||
).toTypedArray()
|
||||
|
||||
signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady)
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TrustedRelayListEvent) -> Unit,
|
||||
) {
|
||||
signer.sign(createdAt, KIND, createTagArray(relays), "", onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): TrustedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user