Breaking filters down by feed type: Home, DMs, Public Chats and Global.

This commit is contained in:
Vitor Pamplona
2023-02-05 18:14:41 -05:00
parent 72aad26c03
commit bd94544c9b
27 changed files with 575 additions and 306 deletions
@@ -1,17 +1,23 @@
package com.vitorpamplona.amethyst package com.vitorpamplona.amethyst
import android.content.Context import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.DefaultChannels import com.vitorpamplona.amethyst.model.DefaultChannels
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.events.ContactListEvent
import nostr.postr.events.Event
import nostr.postr.toHex import nostr.postr.toHex
class LocalPreferences(context: Context) { class LocalPreferences(context: Context) {
val encryptedPreferences = EncryptedStorage().preferences(context) val encryptedPreferences = EncryptedStorage().preferences(context)
val gson = GsonBuilder().create()
fun clearEncryptedStorage() { fun clearEncryptedStorage() {
encryptedPreferences.edit().apply { encryptedPreferences.edit().apply {
@@ -19,6 +25,7 @@ class LocalPreferences(context: Context) {
remove("nostr_pubkey") remove("nostr_pubkey")
remove("following_channels") remove("following_channels")
remove("hidden_users") remove("hidden_users")
remove("relays")
}.apply() }.apply()
} }
@@ -28,6 +35,7 @@ class LocalPreferences(context: Context) {
account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) } account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) }
account.followingChannels.let { putStringSet("following_channels", it) } account.followingChannels.let { putStringSet("following_channels", it) }
account.hiddenUsers.let { putStringSet("hidden_users", it) } account.hiddenUsers.let { putStringSet("hidden_users", it) }
account.localRelays.let { putString("relays", gson.toJson(it)) }
}.apply() }.apply()
} }
@@ -37,12 +45,17 @@ class LocalPreferences(context: Context) {
val pubKey = getString("nostr_pubkey", null) val pubKey = getString("nostr_pubkey", null)
val followingChannels = getStringSet("following_channels", null)?.toMutableSet() ?: mutableSetOf() val followingChannels = getStringSet("following_channels", null)?.toMutableSet() ?: mutableSetOf()
val hiddenUsers = getStringSet("hidden_users", emptySet())?.toMutableSet() ?: mutableSetOf() val hiddenUsers = getStringSet("hidden_users", emptySet())?.toMutableSet() ?: mutableSetOf()
val localRelays = gson.fromJson(
getString("relays", "[]"),
object : TypeToken<Set<NewRelayListViewModel.Relay>>() {}.type
) ?: setOf<NewRelayListViewModel.Relay>()
if (pubKey != null) { if (pubKey != null) {
return Account( return Account(
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
followingChannels, followingChannels,
hiddenUsers hiddenUsers,
localRelays
) )
} else { } else {
return null return null
@@ -1,7 +1,7 @@
package com.vitorpamplona.amethyst package com.vitorpamplona.amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
@@ -29,7 +29,7 @@ object ServiceManager {
val myAccount = account val myAccount = account
if (myAccount != null) { if (myAccount != null) {
Client.connect(myAccount.activeRelays() ?: Constants.defaultRelays) Client.connect(myAccount.convertLocalRelays())
// start services // start services
NostrAccountDataSource.account = myAccount NostrAccountDataSource.account = myAccount
@@ -55,7 +55,7 @@ object ServiceManager {
NostrChatroomListDataSource.start() NostrChatroomListDataSource.start()
} else { } else {
// if not logged in yet, start a basic service wit default relays // if not logged in yet, start a basic service wit default relays
Client.connect() Client.connect(Constants.convertDefaultRelays())
} }
} }
@@ -1,7 +1,7 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
@@ -9,8 +9,10 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
import java.util.Date import java.util.Date
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -34,8 +36,9 @@ val DefaultChannels = setOf(
class Account( class Account(
val loggedIn: Persona, val loggedIn: Persona,
var followingChannels: Set<String> = DefaultChannels.toMutableSet(), var followingChannels: Set<String> = DefaultChannels,
var hiddenUsers: Set<String> = mutableSetOf() var hiddenUsers: Set<String> = setOf(),
var localRelays: Set<NewRelayListViewModel.Relay> = Constants.defaultRelays.toSet()
) { ) {
fun userProfile(): User { fun userProfile(): User {
@@ -330,14 +333,23 @@ class Account(
} }
fun activeRelays(): Array<Relay>? { fun activeRelays(): Array<Relay>? {
return userProfile().relays?.map { Relay(it.key, it.value.read, it.value.write) }?.toTypedArray() return userProfile().relays?.map {
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
Relay(it.key, it.value.read, it.value.write, localFeedTypes)
}?.toTypedArray()
}
fun convertLocalRelays(): Array<Relay> {
return localRelays.map {
Relay(it.url, it.read, it.write, it.feedTypes)
}.toTypedArray()
} }
init { init {
userProfile().subscribe(object: User.Listener() { userProfile().subscribe(object: User.Listener() {
override fun onRelayChange() { override fun onRelayChange() {
Client.disconnect() Client.disconnect()
Client.connect(activeRelays() ?: Constants.defaultRelays) Client.connect(activeRelays() ?: convertLocalRelays())
RelayPool.requestAndWatch() RelayPool.requestAndWatch()
} }
}) })
@@ -396,6 +408,11 @@ class Account(
innerReports).toSet() innerReports).toSet()
} }
fun saveRelayList(value: List<NewRelayListViewModel.Relay>) {
localRelays = value.toSet()
sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } )
}
} }
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) { class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
@@ -178,6 +179,8 @@ class User(val pubkeyHex: String) {
updatedFollowsAt = updateAt updatedFollowsAt = updateAt
} }
data class RelayMetadata(val read: Boolean, val write: Boolean, val activeTypes: Set<FeedType>)
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) { fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
if (relays != relayUse) { if (relays != relayUse) {
relays = relayUse relays = relayUse
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.UUID import java.util.UUID
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
@@ -7,7 +8,7 @@ data class Channel (
val id: String = UUID.randomUUID().toString().substring(0,4), val id: String = UUID.randomUUID().toString().substring(0,4),
val onEOSE: ((Long) -> Unit)? = null val onEOSE: ((Long) -> Unit)? = null
) { ) {
var filter: List<JsonFilter>? = null // Inactive when null var filter: List<TypedFilter>? = null // Inactive when null
fun updateEOSE(l: Long) { fun updateEOSE(l: Long) {
onEOSE?.let { it(l) } onEOSE?.let { it(l) }
@@ -1,27 +0,0 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.Relay
object Constants {
val defaultRelays = arrayOf(
Relay("wss://nostr.bitcoiner.social", read = true, write = true),
Relay("wss://relay.nostr.bg", read = true, write = true),
Relay("wss://brb.io", read = true, write = true),
Relay("wss://relay.snort.social", read = true, write = true),
Relay("wss://nostr.rocks", read = true, write = true),
Relay("wss://relay.damus.io", read = true, write = true),
Relay("wss://nostr.fmt.wiz.biz", read = true, write = true),
Relay("wss://nostr.oxtr.dev", read = true, write = true),
Relay("wss://eden.nostr.land", read = true, write = true),
Relay("wss://nostr.zebedee.cloud", read = true, write = true),
Relay("wss://nostr-pub.wellorder.net", read = true, write = true),
Relay("wss://nostr.mom", read = true, write = true),
Relay("wss://nostr.orangepill.dev", read = true, write = true),
Relay("wss://nostr-pub.semisol.dev", read = true, write = true),
Relay("wss://nostr.onsats.org", read = true, write = true),
Relay("wss://nostr.sandwich.farm", read = true, write = true),
Relay("wss://relay.nostr.ch", read = true, write = true),
Relay("wss://no.str.cr", read = true, write = true),
Relay("wss://nos.lol", read = true, write = true)
)
}
@@ -6,6 +6,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
@@ -14,32 +16,44 @@ import nostr.postr.events.TextNoteEvent
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") { object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
lateinit var account: Account lateinit var account: Account
fun createAccountContactListFilter(): JsonFilter { fun createAccountContactListFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
kinds = listOf(ContactListEvent.kind), types = FeedType.values().toSet(),
authors = listOf(account.userProfile().pubkeyHex), filter = JsonFilter(
limit = 1 kinds = listOf(ContactListEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1
)
) )
} }
fun createAccountMetadataFilter(): JsonFilter { fun createAccountMetadataFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
kinds = listOf(MetadataEvent.kind), types = FeedType.values().toSet(),
authors = listOf(account.userProfile().pubkeyHex), filter = JsonFilter(
limit = 1 kinds = listOf(MetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1
)
) )
} }
fun createAccountReportsFilter(): JsonFilter { fun createAccountReportsFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
kinds = listOf(ReportEvent.kind), types = FeedType.values().toSet(),
authors = listOf(account.userProfile().pubkeyHex) filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
)
) )
} }
fun createNotificationFilter() = JsonFilter( fun createNotificationFilter() = TypedFilter(
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), types = FeedType.values().toSet(),
limit = 200 filter = JsonFilter(
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 200
)
) )
val accountChannel = requestNewChannel() val accountChannel = requestNewChannel()
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") { object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
@@ -15,12 +17,15 @@ object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
resetFilters() resetFilters()
} }
fun createMessagesToChannelFilter(): JsonFilter? { fun createMessagesToChannelFilter(): TypedFilter? {
if (channel != null) { if (channel != null) {
return JsonFilter( return TypedFilter(
kinds = listOf(ChannelMessageEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
tags = mapOf("e" to listOfNotNull(channel?.idHex)), filter = JsonFilter(
limit = 200 kinds = listOf(ChannelMessageEvent.kind),
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
limit = 200
)
) )
} }
return null return null
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
@@ -16,28 +18,34 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
withUser = LocalCache.users[userId] withUser = LocalCache.users[userId]
} }
fun createMessagesToMeFilter(): JsonFilter? { fun createMessagesToMeFilter(): TypedFilter? {
val myPeer = withUser val myPeer = withUser
return if (myPeer != null) { return if (myPeer != null) {
JsonFilter( TypedFilter(
kinds = listOf(PrivateDmEvent.kind), types = setOf(FeedType.PRIVATE_DMS),
authors = listOf(myPeer.pubkeyHex) , filter = JsonFilter(
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)) kinds = listOf(PrivateDmEvent.kind),
authors = listOf(myPeer.pubkeyHex) ,
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
)
) )
} else { } else {
null null
} }
} }
fun createMessagesFromMeFilter(): JsonFilter? { fun createMessagesFromMeFilter(): TypedFilter? {
val myPeer = withUser val myPeer = withUser
return if (myPeer != null) { return if (myPeer != null) {
JsonFilter( TypedFilter(
kinds = listOf(PrivateDmEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
authors = listOf(account.userProfile().pubkeyHex), filter = JsonFilter(
tags = mapOf("p" to listOf(myPeer.pubkeyHex)) kinds = listOf(PrivateDmEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
tags = mapOf("p" to listOf(myPeer.pubkeyHex))
)
) )
} else { } else {
null null
@@ -6,48 +6,68 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") { object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
lateinit var account: Account lateinit var account: Account
fun createMessagesToMeFilter() = JsonFilter( fun createMessagesToMeFilter() = TypedFilter(
kinds = listOf(PrivateDmEvent.kind), types = setOf(FeedType.PRIVATE_DMS),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)) filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
)
) )
fun createMessagesFromMeFilter() = JsonFilter( fun createMessagesFromMeFilter() = TypedFilter(
kinds = listOf(PrivateDmEvent.kind), types = setOf(FeedType.PRIVATE_DMS),
authors = listOf(account.userProfile().pubkeyHex) filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
)
) )
fun createChannelsCreatedbyMeFilter() = JsonFilter( fun createChannelsCreatedbyMeFilter() = TypedFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
authors = listOf(account.userProfile().pubkeyHex) filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
)
) )
fun createMyChannelsFilter() = JsonFilter( fun createMyChannelsFilter() = TypedFilter(
kinds = listOf(ChannelCreateEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
ids = account.followingChannels.toList() filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = account.followingChannels.toList()
)
) )
fun createLastChannelInfoFilter(): List<JsonFilter> { fun createLastChannelInfoFilter(): List<TypedFilter> {
return account.followingChannels.map { return account.followingChannels.map {
JsonFilter( TypedFilter(
kinds = listOf(ChannelMetadataEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
tags = mapOf("e" to listOf(it)), filter = JsonFilter(
limit = 1 kinds = listOf(ChannelMetadataEvent.kind),
tags = mapOf("e" to listOf(it)),
limit = 1
)
) )
} }
} }
fun createLastMessageOfEachChannelFilter(): List<JsonFilter> { fun createLastMessageOfEachChannelFilter(): List<TypedFilter> {
return account.followingChannels.map { return account.followingChannels.map {
JsonFilter( TypedFilter(
kinds = listOf(ChannelMessageEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
tags = mapOf("e" to listOf(it)), filter = JsonFilter(
limit = 1 kinds = listOf(ChannelMessageEvent.kind),
tags = mapOf("e" to listOf(it)),
limit = 1
)
) )
} }
} }
@@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
@@ -16,11 +13,8 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import java.util.Collections
import java.util.Date import java.util.Date
import java.util.UUID import java.util.UUID
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -181,7 +175,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
// saves the channels that are currently active // saves the channels that are currently active
val activeChannels = channels.values.filter { it.filter != null } val activeChannels = channels.values.filter { it.filter != null }
// saves the current content to only update if it changes // saves the current content to only update if it changes
val currentFilter = activeChannels.associate { it.id to it.filter!!.joinToString("|") { it.toJson() } } val currentFilter = activeChannels.associate { it.id to it.filter!!.joinToString("|") { it.filter.toJson() } }
updateChannelFilters() updateChannelFilters()
@@ -195,7 +189,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
Client.close(channel.id) Client.close(channel.id)
} else { } else {
// was active and is still active, check if it has changed. // was active and is still active, check if it has changed.
if (channelsNewFilter.joinToString("|") { it.toJson() } != currentFilter[channel.id]) { if (channelsNewFilter.joinToString("|") { it.filter.toJson() } != currentFilter[channel.id]) {
Client.close(channel.id) Client.close(channel.id)
Client.sendFilter(channel.id, channelsNewFilter) Client.sendFilter(channel.id, channelsNewFilter)
} else { } else {
@@ -208,7 +202,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
// was not active and is still not active, does nothing // was not active and is still not active, does nothing
} else { } else {
// was not active and becomes active, sends the filter. // was not active and becomes active, sends the filter.
if (channelsNewFilter.joinToString("|") { it.toJson() } != currentFilter[channel.id]) { if (channelsNewFilter.joinToString("|") { it.filter.toJson() } != currentFilter[channel.id]) {
Client.sendFilter(channel.id, channelsNewFilter) Client.sendFilter(channel.id, channelsNewFilter)
} }
} }
@@ -4,14 +4,19 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") { object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
lateinit var account: Account lateinit var account: Account
fun createGlobalFilter() = JsonFilter( fun createGlobalFilter() = TypedFilter(
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind), types = setOf(FeedType.GLOBAL),
limit = 200 filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind),
limit = 200
)
) )
val globalFeedChannel = requestNewChannel() val globalFeedChannel = requestNewChannel()
@@ -5,6 +5,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
@@ -30,7 +32,7 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
account.userProfile().unsubscribe(cacheListener) account.userProfile().unsubscribe(cacheListener)
} }
fun createFollowAccountsFilter(): JsonFilter { fun createFollowAccountsFilter(): TypedFilter {
val follows = account.userProfile().follows val follows = account.userProfile().follows
val followKeys = follows.map { val followKeys = follows.map {
@@ -39,31 +41,18 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6))
return JsonFilter( return TypedFilter(
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind), types = setOf(FeedType.FOLLOWS),
authors = followSet, filter = JsonFilter(
limit = 400 kinds = listOf(TextNoteEvent.kind, RepostEvent.kind),
authors = followSet,
limit = 400
)
) )
} }
val followAccountChannel = requestNewChannel() val followAccountChannel = requestNewChannel()
fun <T> equalsIgnoreOrder(list1:List<T>?, list2:List<T>?): Boolean {
if (list1 == null && list2 == null) return true
if (list1 == null) return false
if (list2 == null) return false
return list1.size == list2.size && list1.toSet() == list2.toSet()
}
fun equalAuthors(list1:JsonFilter?, list2:JsonFilter?): Boolean {
if (list1 == null && list2 == null) return true
if (list1 == null) return false
if (list2 == null) return false
return equalsIgnoreOrder(list1.authors, list2.authors)
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val user = account.userProfile() val user = account.userProfile()
@@ -75,10 +64,6 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
} }
override fun updateChannelFilters() { override fun updateChannelFilters() {
val newFollowAccountsFilter = createFollowAccountsFilter() followAccountChannel.filter = listOf(createFollowAccountsFilter()).ifEmpty { null }
if (!equalAuthors(newFollowAccountsFilter, followAccountChannel.filter?.firstOrNull())) {
followAccountChannel.filter = listOf(newFollowAccountsFilter).ifEmpty { null }
}
} }
} }
@@ -5,6 +5,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Collections import java.util.Collections
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.bechToBytes import nostr.postr.bechToBytes
@@ -14,19 +16,23 @@ import nostr.postr.toHex
object NostrSearchEventOrUserDataSource: NostrDataSource<Note>("SingleEventFeed") { object NostrSearchEventOrUserDataSource: NostrDataSource<Note>("SingleEventFeed") {
private var hexToWatch: String? = null private var hexToWatch: String? = null
private fun createAnythingWithIDFilter(): List<JsonFilter>? { private fun createAnythingWithIDFilter(): List<TypedFilter>? {
if (hexToWatch == null) { if (hexToWatch == null) {
return null return null
} }
// downloads all the reactions to a given event. // downloads all the reactions to a given event.
return listOf( return listOf(
JsonFilter( TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
ids = listOfNotNull(hexToWatch) ids = listOfNotNull(hexToWatch)
), )),
JsonFilter( TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
authors = listOfNotNull(hexToWatch) authors = listOfNotNull(hexToWatch)
) ))
) )
} }
@@ -7,6 +7,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Collections import java.util.Collections
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
@@ -14,7 +16,7 @@ import nostr.postr.events.TextNoteEvent
object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed") { object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed") {
private var channelsToWatch = setOf<String>() private var channelsToWatch = setOf<String>()
private fun createRepliesAndReactionsFilter(): JsonFilter? { private fun createRepliesAndReactionsFilter(): TypedFilter? {
val reactionsToWatch = channelsToWatch.map { it } val reactionsToWatch = channelsToWatch.map { it }
if (reactionsToWatch.isEmpty()) { if (reactionsToWatch.isEmpty()) {
@@ -22,13 +24,16 @@ object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed")
} }
// downloads all the reactions to a given event. // downloads all the reactions to a given event.
return JsonFilter( return TypedFilter(
kinds = listOf(ChannelMetadataEvent.kind), types = setOf(FeedType.PUBLIC_CHATS),
tags = mapOf("e" to reactionsToWatch) filter = JsonFilter(
kinds = listOf(ChannelMetadataEvent.kind),
tags = mapOf("e" to reactionsToWatch)
)
) )
} }
fun createLoadEventsIfNotLoadedFilter(): JsonFilter? { fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val directEventsToLoad = channelsToWatch val directEventsToLoad = channelsToWatch
.map { LocalCache.getOrCreateChannel(it) } .map { LocalCache.getOrCreateChannel(it) }
.filter { it.notes.isEmpty() } .filter { it.notes.isEmpty() }
@@ -40,9 +45,12 @@ object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed")
} }
// downloads linked events to this event. // downloads linked events to this event.
return JsonFilter( return TypedFilter(
kinds = listOf(ChannelCreateEvent.kind), types = FeedType.values().toSet(),
ids = interestedEvents.toList() filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = interestedEvents.toList()
)
) )
} }
@@ -8,6 +8,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Collections import java.util.Collections
import java.util.Date import java.util.Date
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
@@ -17,7 +19,7 @@ import nostr.postr.events.TextNoteEvent
object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") { object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
private var eventsToWatch = setOf<String>() private var eventsToWatch = setOf<String>()
private fun createRepliesAndReactionsFilter(): List<JsonFilter>? { private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) } val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
if (reactionsToWatch.isEmpty()) { if (reactionsToWatch.isEmpty()) {
@@ -30,17 +32,20 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
val lastTime = it.lastReactionsDownloadTime; val lastTime = it.lastReactionsDownloadTime;
lastTime == null || lastTime < (now - 10) lastTime == null || lastTime < (now - 10)
}.map { }.map {
JsonFilter( TypedFilter(
kinds = listOf( types = FeedType.values().toSet(),
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind filter = JsonFilter(
), kinds = listOf(
tags = mapOf("e" to listOf(it.idHex)), TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind
since = it.lastReactionsDownloadTime ),
tags = mapOf("e" to listOf(it.idHex)),
since = it.lastReactionsDownloadTime
)
) )
} }
} }
fun createLoadEventsIfNotLoadedFilter(): List<JsonFilter>? { fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad = eventsToWatch val directEventsToLoad = eventsToWatch
.map { LocalCache.getOrCreateNote(it) } .map { LocalCache.getOrCreateNote(it) }
.filter { it.event == null } .filter { it.event == null }
@@ -60,13 +65,18 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
} }
// downloads linked events to this event. // downloads linked events to this event.
return listOf(JsonFilter( return listOf(
kinds = listOf( TypedFilter(
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, types = FeedType.values().toSet(),
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind filter = JsonFilter(
), kinds = listOf(
ids = interestedEvents.toList() TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind,
)) ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind
),
ids = interestedEvents.toList()
)
)
)
} }
val singleEventChannel = requestNewChannel() { time -> val singleEventChannel = requestNewChannel() { time ->
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Collections import java.util.Collections
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
@@ -11,25 +13,31 @@ import nostr.postr.events.MetadataEvent
object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") { object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
var usersToWatch = setOf<String>() var usersToWatch = setOf<String>()
fun createUserFilter(): List<JsonFilter>? { fun createUserFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null if (usersToWatch.isEmpty()) return null
return usersToWatch.filter { LocalCache.getOrCreateUser(it).latestMetadata == null }.map { return usersToWatch.filter { LocalCache.getOrCreateUser(it).latestMetadata == null }.map {
JsonFilter( TypedFilter(
kinds = listOf(MetadataEvent.kind), types = FeedType.values().toSet(),
authors = listOf(it), filter = JsonFilter(
limit = 1 kinds = listOf(MetadataEvent.kind),
authors = listOf(it),
limit = 1
)
) )
} }
} }
fun createUserReportFilter(): List<JsonFilter>? { fun createUserReportFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null if (usersToWatch.isEmpty()) return null
return usersToWatch.map { return usersToWatch.map {
JsonFilter( TypedFilter(
kinds = listOf(ReportEvent.kind), types = FeedType.values().toSet(),
tags = mapOf("p" to listOf(it)) filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
tags = mapOf("p" to listOf(it))
)
) )
} }
} }
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Collections import java.util.Collections
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
@@ -11,18 +13,21 @@ import nostr.postr.events.TextNoteEvent
object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") { object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
private var eventsToWatch = setOf<String>() private var eventsToWatch = setOf<String>()
fun createRepliesAndReactionsFilter(): JsonFilter? { fun createRepliesAndReactionsFilter(): TypedFilter? {
if (eventsToWatch.isEmpty()) { if (eventsToWatch.isEmpty()) {
return null return null
} }
return JsonFilter( return TypedFilter(
kinds = listOf(TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind), types = FeedType.values().toSet(),
tags = mapOf("e" to eventsToWatch.toList()) filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind),
tags = mapOf("e" to eventsToWatch.toList())
)
) )
} }
fun createLoadEventsIfNotLoadedFilter(): JsonFilter? { fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val nodes = eventsToWatch.map { LocalCache.getOrCreateNote(it) } val nodes = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
val eventsToLoad = nodes val eventsToLoad = nodes
@@ -33,8 +38,11 @@ object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
return null return null
} }
return JsonFilter( return TypedFilter(
ids = eventsToLoad types = FeedType.values().toSet(),
filter = JsonFilter(
ids = eventsToLoad
)
) )
} }
@@ -5,6 +5,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
@@ -19,33 +21,45 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
resetFilters() resetFilters()
} }
fun createUserInfoFilter(): JsonFilter { fun createUserInfoFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
kinds = listOf(MetadataEvent.kind), types = FeedType.values().toSet(),
authors = listOf(user!!.pubkeyHex), filter = JsonFilter(
limit = 1 kinds = listOf(MetadataEvent.kind),
authors = listOf(user!!.pubkeyHex),
limit = 1
)
) )
} }
fun createUserPostsFilter(): JsonFilter { fun createUserPostsFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
kinds = listOf(TextNoteEvent.kind), types = FeedType.values().toSet(),
authors = listOf(user!!.pubkeyHex), filter = JsonFilter(
limit = 100 kinds = listOf(TextNoteEvent.kind),
authors = listOf(user!!.pubkeyHex),
limit = 100
)
) )
} }
fun createFollowFilter(): JsonFilter { fun createFollowFilter(): TypedFilter {
return JsonFilter( return TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(ContactListEvent.kind),
authors = listOf(user!!.pubkeyHex),
limit = 1
)
)
}
fun createFollowersFilter() = TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(ContactListEvent.kind), kinds = listOf(ContactListEvent.kind),
authors = listOf(user!!.pubkeyHex), tags = mapOf("p" to listOf(user!!.pubkeyHex))
limit = 1
) )
}
fun createFollowersFilter() = JsonFilter(
kinds = listOf(ContactListEvent.kind),
tags = mapOf("p" to listOf(user!!.pubkeyHex))
) )
val userInfoChannel = requestNewChannel() val userInfoChannel = requestNewChannel()
@@ -1,10 +1,6 @@
package com.vitorpamplona.amethyst.service.relays package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.service.Constants
import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import nostr.postr.JsonFilter
import nostr.postr.events.Event import nostr.postr.events.Event
/** /**
@@ -24,10 +20,10 @@ object Client: RelayPool.Listener {
**/ **/
var lenient: Boolean = false var lenient: Boolean = false
private var listeners = setOf<Listener>() private var listeners = setOf<Listener>()
private var relays = Constants.defaultRelays private var relays = Constants.convertDefaultRelays()
private var subscriptions = mapOf<String, List<JsonFilter>>() private var subscriptions = mapOf<String, List<TypedFilter>>()
fun connect(relays: Array<Relay> = Constants.defaultRelays) { fun connect(relays: Array<Relay>) {
RelayPool.register(this) RelayPool.register(this)
RelayPool.unloadRelays() RelayPool.unloadRelays()
RelayPool.loadRelays(relays.toList()) RelayPool.loadRelays(relays.toList())
@@ -36,14 +32,15 @@ object Client: RelayPool.Listener {
fun sendFilter( fun sendFilter(
subscriptionId: String = UUID.randomUUID().toString().substring(0..10), subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
filters: List<JsonFilter> = listOf(JsonFilter()) filters: List<TypedFilter> = listOf()
) { ) {
subscriptions = subscriptions + Pair(subscriptionId, filters) subscriptions = subscriptions + Pair(subscriptionId, filters)
RelayPool.sendFilter(subscriptionId) RelayPool.sendFilter(subscriptionId)
} }
fun sendFilterOnlyIfDisconnected( fun sendFilterOnlyIfDisconnected(
subscriptionId: String = UUID.randomUUID().toString().substring(0..10), subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
filters: List<JsonFilter> = listOf(JsonFilter()) filters: List<TypedFilter> = listOf()
) { ) {
subscriptions = subscriptions + Pair(subscriptionId, filters) subscriptions = subscriptions + Pair(subscriptionId, filters)
RelayPool.sendFilterOnlyIfDisconnected() RelayPool.sendFilterOnlyIfDisconnected()
@@ -91,7 +88,7 @@ object Client: RelayPool.Listener {
return subscriptions.keys.toList() return subscriptions.keys.toList()
} }
fun getSubscriptionFilters(subId: String): List<JsonFilter> { fun getSubscriptionFilters(subId: String): List<TypedFilter> {
return subscriptions[subId] ?: emptyList() return subscriptions[subId] ?: emptyList()
} }
@@ -0,0 +1,48 @@
package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
val activeTypesGlobal = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
fun convertDefaultRelays(): Array<Relay> {
return defaultRelays.map {
Relay(it.url, it.read, it.write, it.feedTypes)
}.toTypedArray()
}
val defaultRelays = arrayOf(
// Free relays
NewRelayListViewModel.Relay("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://brb.io", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://relay.snort.social", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://nostr-pub.wellorder.net", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://nostr.mom", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
NewRelayListViewModel.Relay("wss://nos.lol", read = true, write = true, feedTypes = activeTypes),
// Less Reliable
//NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://nostr.onsats.org", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://nostr.sandwich.farm", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://relay.nostr.ch", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://nostr.zebedee.cloud", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://nostr.rocks", read = true, write = true, feedTypes = activeTypes),
//NewRelayListViewModel.Relay("wss://nostr.fmt.wiz.biz", read = true, write = true, feedTypes = activeTypes),
// Paid relays
NewRelayListViewModel.Relay("wss://relay.nostr.com.au", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://eden.nostr.land", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://nostr.milou.lol", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://puravida.nostr.land", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://nostr.wine", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://nostr.inosta.cc", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://nostr-pub.semisol.dev", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://relay.orangepill.dev", read = true, write = false, feedTypes = activeTypesGlobal),
NewRelayListViewModel.Relay("wss://relay.nostrati.com", read = true, write = false, feedTypes = activeTypesGlobal),
)
}
@@ -11,10 +11,15 @@ import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
enum class FeedType {
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL
}
class Relay( class Relay(
var url: String, var url: String,
var read: Boolean = true, var read: Boolean = true,
var write: Boolean = true var write: Boolean = true,
var activeTypes: Set<FeedType> = FeedType.values().toSet(),
) { ) {
private val httpClient = OkHttpClient.Builder() private val httpClient = OkHttpClient.Builder()
.connectTimeout(100, TimeUnit.SECONDS) .connectTimeout(100, TimeUnit.SECONDS)
@@ -161,10 +166,10 @@ class Relay(
if (read) { if (read) {
if (isConnected()) { if (isConnected()) {
if (isReady) { if (isReady) {
val filters = Client.getSubscriptionFilters(requestId) val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
if (filters.isNotEmpty()) { if (filters.isNotEmpty()) {
val request = val request =
"""["REQ","$requestId",${filters.take(10).joinToString(",") { it.toJson() }}]""" """["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson() }}]"""
//println("FILTERSSENT ${url} " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""") //println("FILTERSSENT ${url} " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""")
socket?.send(request) socket?.send(request)
} }
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.service.relays package com.vitorpamplona.amethyst.service.relays
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -27,11 +26,11 @@ object RelayPool: Relay.Listener {
return relays.firstOrNull() { it.url == url } return relays.firstOrNull() { it.url == url }
} }
fun loadRelays(relayList: List<Relay>? = null){ fun loadRelays(relayList: List<Relay>){
if (!relayList.isNullOrEmpty()){ if (!relayList.isNullOrEmpty()){
relayList.forEach { addRelay(it) } relayList.forEach { addRelay(it) }
} else { } else {
Constants.defaultRelays.forEach { addRelay(it) } Constants.convertDefaultRelays().forEach { addRelay(it) }
} }
} }
@@ -0,0 +1,8 @@
package com.vitorpamplona.amethyst.service.relays
import nostr.postr.JsonFilter
class TypedFilter(
val types: Set<FeedType>,
val filter: JsonFilter
)
@@ -33,6 +33,9 @@ import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.DownloadDone import androidx.compose.material.icons.filled.DownloadDone
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.SyncProblem import androidx.compose.material.icons.filled.SyncProblem
import androidx.compose.material.icons.filled.Upload import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.outlined.BarChart import androidx.compose.material.icons.outlined.BarChart
@@ -48,6 +51,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -55,18 +60,21 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.FeedType
import java.lang.Math.round import java.lang.Math.round
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = "") { fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = "") {
val postViewModel: NewRelayListViewModel = viewModel() val postViewModel: NewRelayListViewModel = viewModel()
val ctx = LocalContext.current.applicationContext
val feedState by postViewModel.relays.collectAsState() val feedState by postViewModel.relays.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.load(account) postViewModel.load(account, ctx)
} }
Dialog( Dialog(
@@ -82,19 +90,18 @@ fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String =
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CloseButton(onCancel = { CloseButton(onCancel = {
postViewModel.clear() postViewModel.clear(ctx)
onClose() onClose()
}) })
PostButton( PostButton(
onPost = { onPost = {
postViewModel.create() postViewModel.create(ctx)
onClose() onClose()
}, },
true true
@@ -103,7 +110,7 @@ fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String =
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row(modifier = Modifier.fillMaxWidth(1f), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
LazyColumn( LazyColumn(
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 10.dp, top = 10.dp,
@@ -114,15 +121,15 @@ fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String =
if (index == 0) if (index == 0)
ServerConfigHeader() ServerConfigHeader()
ServerConfig(item, ServerConfig(item,
onToggleDownload = { onToggleDownload = { postViewModel.toggleDownload(it) },
postViewModel.toggleDownload(it) onToggleUpload = { postViewModel.toggleUpload(it) },
},
onToggleUpload = { onToggleFollows = { postViewModel.toggleFollows(it) },
postViewModel.toggleUpload(it) onTogglePrivateDMs = { postViewModel.toggleMessages(it) },
}, onTogglePublicChats = { postViewModel.togglePublicChats(it) },
onDelete = { onToggleGlobal = { postViewModel.toggleGlobal(it) },
postViewModel.deleteRelay(it)
} onDelete = { postViewModel.deleteRelay(it) }
) )
} }
} }
@@ -156,7 +163,7 @@ fun ServerConfigHeader() {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.size(25.dp))
Text( Text(
text = "Posts", text = "Posts",
@@ -186,7 +193,7 @@ fun ServerConfigHeader() {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
) )
Spacer(modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.size(5.dp))
} }
} }
} }
@@ -202,9 +209,32 @@ fun ServerConfig(
item: NewRelayListViewModel.Relay, item: NewRelayListViewModel.Relay,
onToggleDownload: (NewRelayListViewModel.Relay) -> Unit, onToggleDownload: (NewRelayListViewModel.Relay) -> Unit,
onToggleUpload: (NewRelayListViewModel.Relay) -> Unit, onToggleUpload: (NewRelayListViewModel.Relay) -> Unit,
onToggleFollows: (NewRelayListViewModel.Relay) -> Unit,
onTogglePrivateDMs: (NewRelayListViewModel.Relay) -> Unit,
onTogglePublicChats: (NewRelayListViewModel.Relay) -> Unit,
onToggleGlobal: (NewRelayListViewModel.Relay) -> Unit,
onDelete: (NewRelayListViewModel.Relay) -> Unit) { onDelete: (NewRelayListViewModel.Relay) -> Unit) {
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp)
) {
Column() {
IconButton(
modifier = Modifier.size(30.dp),
onClick = { onDelete(item) }
) {
Icon(
imageVector = Icons.Default.Cancel,
null,
modifier = Modifier.padding(end = 5.dp).size(15.dp),
tint = Color.Red
)
}
}
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
@@ -214,75 +244,126 @@ fun ServerConfig(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
}
Column(Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
IconButton( Column(Modifier.weight(1f)) {
modifier = Modifier.size(30.dp), Row(verticalAlignment = Alignment.CenterVertically) {
onClick = { onToggleDownload(item) } IconButton(
) { modifier = Modifier.size(30.dp),
Icon( onClick = { onToggleFollows(item) }
imageVector = Icons.Default.Download, ) {
null, Icon(
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp), painterResource(R.drawable.ic_home),
tint = if (item.read) Color.Green else Color.Red "Home Feed",
) modifier = Modifier.padding(end = 5.dp).size(15.dp),
tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
IconButton(
modifier = Modifier.size(30.dp),
onClick = { onTogglePrivateDMs(item) }
) {
Icon(
painterResource(R.drawable.ic_dm),
"Private Message Feed",
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
IconButton(
modifier = Modifier.size(30.dp),
onClick = { onTogglePublicChats(item) }
) {
Icon(
imageVector = Icons.Default.Groups,
"Public Chat Feed",
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
IconButton(
modifier = Modifier.size(30.dp),
onClick = { onToggleGlobal(item) }
) {
Icon(
imageVector = Icons.Default.Public,
"Global Feed",
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.feedTypes.contains(FeedType.GLOBAL)) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
}
} }
Text( Column(Modifier.weight(1f)) {
text = "${countToHumanReadable(item.downloadCount)}", Row(verticalAlignment = Alignment.CenterVertically) {
maxLines = 1, IconButton(
fontSize = 14.sp, modifier = Modifier.size(30.dp),
modifier = Modifier.weight(1f), onClick = { onToggleDownload(item) }
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), ) {
) Icon(
imageVector = Icons.Default.Download,
null,
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.read) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
IconButton( Text(
modifier = Modifier.size(30.dp), text = "${countToHumanReadable(item.downloadCount)}",
onClick = { onToggleUpload(item) } maxLines = 1,
) { fontSize = 14.sp,
Icon( modifier = Modifier.weight(1f),
imageVector = Icons.Default.Upload, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
null, )
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.write) Color.Green else Color.Red
)
}
Text( IconButton(
text = "${countToHumanReadable(item.uploadCount)}", modifier = Modifier.size(30.dp),
maxLines = 1, onClick = { onToggleUpload(item) }
fontSize = 14.sp, ) {
modifier = Modifier.weight(1f), Icon(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), imageVector = Icons.Default.Upload,
) null,
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
tint = if (item.write) Color.Green else MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
)
}
Icon( Text(
imageVector = Icons.Default.SyncProblem, text = "${countToHumanReadable(item.uploadCount)}",
null, maxLines = 1,
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp), fontSize = 14.sp,
tint = if (item.errorCount > 0) Color.Yellow else Color.Green modifier = Modifier.weight(1f),
) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
)
Text( Icon(
text = "${countToHumanReadable(item.errorCount)}", imageVector = Icons.Default.SyncProblem,
maxLines = 1, null,
fontSize = 14.sp, modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
modifier = Modifier.weight(1f), tint = if (item.errorCount > 0) Color.Yellow else Color.Green
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), )
)
IconButton( Text(
modifier = Modifier.size(30.dp), text = "${countToHumanReadable(item.errorCount)}",
onClick = { onDelete(item) } maxLines = 1,
) { fontSize = 14.sp,
Icon( modifier = Modifier.weight(1f),
imageVector = Icons.Default.Cancel, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
null, )
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp), }
tint = Color.Red
)
} }
} }
} }
@@ -323,7 +404,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (NewRelayListViewModel.
modifier = Modifier modifier = Modifier
.size(35.dp) .size(35.dp)
.padding(horizontal = 5.dp), .padding(horizontal = 5.dp),
tint = if (read) Color.Green else Color.Red tint = if (read) Color.Green else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
} }
@@ -334,7 +415,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (NewRelayListViewModel.
modifier = Modifier modifier = Modifier
.size(35.dp) .size(35.dp)
.padding(horizontal = 5.dp), .padding(horizontal = 5.dp),
tint = if (write) Color.Green else Color.Red tint = if (write) Color.Green else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
} }
@@ -342,7 +423,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (NewRelayListViewModel.
onClick = { onClick = {
if (url.isNotBlank()) { if (url.isNotBlank()) {
val addedWSS = if (!url.startsWith("wss://")) "wss://$url" else url val addedWSS = if (!url.startsWith("wss://")) "wss://$url" else url
onNewRelay(NewRelayListViewModel.Relay(addedWSS, read, write)) onNewRelay(NewRelayListViewModel.Relay(addedWSS, read, write, feedTypes = FeedType.values().toSet()))
url = "" url = ""
write = true write = true
read = true read = true
@@ -351,7 +432,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (NewRelayListViewModel.
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults colors = ButtonDefaults
.buttonColors( .buttonColors(
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else Color.Gray backgroundColor = if (url.isNotBlank()) Color.Green else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
) { ) {
Text(text = "Add", color = Color.White) Text(text = "Add", color = Color.White)
@@ -1,8 +1,12 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -18,48 +22,51 @@ class NewRelayListViewModel: ViewModel() {
val write: Boolean, val write: Boolean,
val errorCount: Int = 0, val errorCount: Int = 0,
val downloadCount: Int = 0, val downloadCount: Int = 0,
val uploadCount: Int = 0 val uploadCount: Int = 0,
val feedTypes: Set<FeedType>
) )
private val _relays = MutableStateFlow<List<Relay>>(emptyList()) private val _relays = MutableStateFlow<List<Relay>>(emptyList())
val relays = _relays.asStateFlow() val relays = _relays.asStateFlow()
fun load(account: Account) { fun load(account: Account, ctx: Context) {
this.account = account this.account = account
clear() clear(ctx)
} }
fun create() { fun create(ctx: Context) {
relays.let { relays.let {
account.sendNewRelayList(it.value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } ) account.saveRelayList(it.value)
LocalPreferences(ctx).saveToEncryptedStorage(account)
} }
clear() clear(ctx)
} }
fun clear() { fun clear(ctx: Context) {
_relays.update { _relays.update {
val relayFile = account.userProfile().relays val relayFile = account.userProfile().relays
if (relayFile != null) if (relayFile != null)
relayFile.map { relayFile.map {
val liveRelay = RelayPool.getRelay(it.key) val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet()
val errorCounter = liveRelay?.errorCounter ?: 0 val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0 val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0 val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
Relay(it.key, it.value.read, it.value.write, errorCounter, eventDownloadCounter, eventUploadCounter) Relay(it.key, it.value.read, it.value.write, errorCounter, eventDownloadCounter, eventUploadCounter, localInfoFeedTypes)
}.sortedBy { it.downloadCount }.reversed() }.sortedBy { it.downloadCount }.reversed()
else else
Constants.defaultRelays.map { account.localRelays.map {
val liveRelay = RelayPool.getRelay(it.url) val liveRelay = RelayPool.getRelay(it.url)
val errorCounter = liveRelay?.errorCounter ?: 0 val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0 val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0 val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
Relay(it.url, it.read, it.write, errorCounter, eventDownloadCounter, eventUploadCounter) Relay(it.url, it.read, it.write, errorCounter, eventDownloadCounter, eventUploadCounter, it.feedTypes)
}.sortedBy { it.downloadCount }.reversed() }.sortedBy { it.downloadCount }.reversed()
} }
} }
@@ -89,6 +96,38 @@ class NewRelayListViewModel: ViewModel() {
it.updated(relay, relay.copy(write = !relay.write)) it.updated(relay, relay.copy(write = !relay.write))
} }
} }
fun toggleFollows(relay: Relay) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.FOLLOWS)
_relays.update {
it.updated(relay, relay.copy(feedTypes = newTypes))
}
}
fun toggleMessages(relay: Relay) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PRIVATE_DMS)
_relays.update {
it.updated(relay, relay.copy(feedTypes = newTypes))
}
}
fun togglePublicChats(relay: Relay) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PUBLIC_CHATS)
_relays.update {
it.updated(relay, relay.copy(feedTypes = newTypes))
}
}
fun toggleGlobal(relay: Relay) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.GLOBAL)
_relays.update {
it.updated(relay, relay.copy( feedTypes = newTypes ))
}
}
} }
fun <T> Iterable<T>.updated(old: T, new: T): List<T> = map { if (it == old) new else it } fun <T> Iterable<T>.updated(old: T, new: T): List<T> = map { if (it == old) new else it }
fun <T> togglePresenceInSet(set: Set<T>, item: T): Set<T> {
return if (set.contains(item)) set.minus(item) else set.plus(item)
}
@@ -116,7 +116,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
Client.allSubscriptions().map { Client.allSubscriptions().map {
"${it} ${ "${it} ${
Client.getSubscriptionFilters(it) Client.getSubscriptionFilters(it)
.joinToString { it.toJson() } .joinToString { it.filter.toJson() }
}" }"
}.forEach { }.forEach {
Log.d("CURRENT FILTERS", it) Log.d("CURRENT FILTERS", it)