mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-07-01 07:40:42 +02:00
Relay Management (View/Edit)
This commit is contained in:
@ -0,0 +1,71 @@
|
|||||||
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.Constants
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
|
||||||
|
object ServiceManager {
|
||||||
|
private var account: Account? = null
|
||||||
|
|
||||||
|
fun start(account: Account) {
|
||||||
|
this.account = account
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val myAccount = account
|
||||||
|
|
||||||
|
if (myAccount != null) {
|
||||||
|
Client.connect(myAccount.activeRelays() ?: Constants.defaultRelays)
|
||||||
|
|
||||||
|
// start services
|
||||||
|
NostrAccountDataSource.account = myAccount
|
||||||
|
NostrHomeDataSource.account = myAccount
|
||||||
|
NostrNotificationDataSource.account = myAccount
|
||||||
|
NostrChatroomListDataSource.account = myAccount
|
||||||
|
|
||||||
|
NostrAccountDataSource.start()
|
||||||
|
NostrGlobalDataSource.start()
|
||||||
|
NostrHomeDataSource.start()
|
||||||
|
NostrNotificationDataSource.start()
|
||||||
|
NostrSingleEventDataSource.start()
|
||||||
|
NostrSingleUserDataSource.start()
|
||||||
|
NostrThreadDataSource.start()
|
||||||
|
NostrChatroomListDataSource.start()
|
||||||
|
} else {
|
||||||
|
// if not logged in yet, start a basic service wit default relays
|
||||||
|
Client.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
NostrAccountDataSource.stop()
|
||||||
|
NostrHomeDataSource.stop()
|
||||||
|
NostrChannelDataSource.stop()
|
||||||
|
NostrChatroomListDataSource.stop()
|
||||||
|
NostrUserProfileDataSource.stop()
|
||||||
|
NostrUserProfileFollowersDataSource.stop()
|
||||||
|
NostrUserProfileFollowsDataSource.stop()
|
||||||
|
|
||||||
|
NostrGlobalDataSource.stop()
|
||||||
|
NostrNotificationDataSource.stop()
|
||||||
|
NostrSingleEventDataSource.stop()
|
||||||
|
NostrSingleUserDataSource.stop()
|
||||||
|
NostrThreadDataSource.stop()
|
||||||
|
|
||||||
|
Client.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.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.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import nostr.postr.Contact
|
import nostr.postr.Contact
|
||||||
@ -40,6 +42,23 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
return loggedIn.privKey != null
|
return loggedIn.privKey != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendNewRelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val lastestContactList = userProfile().latestContactList
|
||||||
|
val event = if (lastestContactList != null) {
|
||||||
|
ContactListEvent.create(
|
||||||
|
lastestContactList.follows,
|
||||||
|
relays,
|
||||||
|
loggedIn.privKey!!)
|
||||||
|
} else {
|
||||||
|
ContactListEvent.create(listOf(), relays, loggedIn.privKey!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendNewUserMetadata(toString: String) {
|
fun sendNewUserMetadata(toString: String) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@ -234,6 +253,21 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun activeRelays(): Array<Relay>? {
|
||||||
|
return userProfile().relays?.map { Relay(it.key, it.value.read, it.value.write) }?.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
userProfile().subscribe(object: User.Listener() {
|
||||||
|
override fun onRelayChange() {
|
||||||
|
println("Updating Relays AAA Account")
|
||||||
|
Client.disconnect()
|
||||||
|
Client.connect(activeRelays() ?: Constants.defaultRelays)
|
||||||
|
RelayPool.requestAndWatch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: AccountLiveData = AccountLiveData(this)
|
val live: AccountLiveData = AccountLiveData(this)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
@ -19,6 +20,7 @@ import java.util.Collections
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
import nostr.postr.events.DeletionEvent
|
import nostr.postr.events.DeletionEvent
|
||||||
|
import nostr.postr.events.Event
|
||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import nostr.postr.events.PrivateDmEvent
|
||||||
import nostr.postr.events.RecommendRelayEvent
|
import nostr.postr.events.RecommendRelayEvent
|
||||||
@ -110,10 +112,10 @@ object LocalCache {
|
|||||||
|
|
||||||
// Adds notifications to users.
|
// Adds notifications to users.
|
||||||
mentions.forEach {
|
mentions.forEach {
|
||||||
it.taggedPosts.add(note)
|
it.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
replyTo.forEach {
|
replyTo.forEach {
|
||||||
it.author?.taggedPosts?.add(note)
|
it.author?.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts the replies
|
// Counts the replies
|
||||||
@ -148,6 +150,26 @@ object LocalCache {
|
|||||||
event.createdAt
|
event.createdAt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (user.pubkeyHex == "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
println("Updating Relays AAA ${user.toBestDisplayName()} ${event.content} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (event.content.isNotEmpty()) {
|
||||||
|
if (user.pubkeyHex == "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
println("Updating Relays AAA 1 ${user.toBestDisplayName()} ${event.content}")
|
||||||
|
val relays: Map<String, ContactListEvent.ReadWrite> =
|
||||||
|
Event.gson.fromJson(
|
||||||
|
event.content,
|
||||||
|
object : TypeToken<Map<String, ContactListEvent.ReadWrite>>() {}.type
|
||||||
|
)
|
||||||
|
user.updateRelays(relays)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (user.pubkeyHex == "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
println("Updating Relays AAA 2 for ${user.bestUsername()} ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
user.latestContactList = event
|
user.latestContactList = event
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,10 +223,10 @@ object LocalCache {
|
|||||||
|
|
||||||
// Adds notifications to users.
|
// Adds notifications to users.
|
||||||
mentions.forEach {
|
mentions.forEach {
|
||||||
it.taggedPosts.add(note)
|
it.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
repliesTo.forEach {
|
repliesTo.forEach {
|
||||||
it.author?.taggedPosts?.add(note)
|
it.author?.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts the replies
|
// Counts the replies
|
||||||
@ -231,10 +253,10 @@ object LocalCache {
|
|||||||
|
|
||||||
// Adds notifications to users.
|
// Adds notifications to users.
|
||||||
mentions.forEach {
|
mentions.forEach {
|
||||||
it.taggedPosts.add(note)
|
it.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
repliesTo.forEach {
|
repliesTo.forEach {
|
||||||
it.author?.taggedPosts?.add(note)
|
it.author?.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.content == "" || event.content == "+" || event.content == "\uD83E\uDD19") {
|
if (event.content == "" || event.content == "+" || event.content == "\uD83E\uDD19") {
|
||||||
@ -312,10 +334,10 @@ object LocalCache {
|
|||||||
|
|
||||||
// Adds notifications to users.
|
// Adds notifications to users.
|
||||||
mentions.forEach {
|
mentions.forEach {
|
||||||
it.taggedPosts.add(note)
|
it.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
replyTo.forEach {
|
replyTo.forEach {
|
||||||
it.author?.taggedPosts?.add(note)
|
it.author?.addTaggedPost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts the replies
|
// Counts the replies
|
||||||
|
@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@ -11,6 +13,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
|
import nostr.postr.events.Event
|
||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
|
|
||||||
class User(val pubkey: ByteArray) {
|
class User(val pubkey: ByteArray) {
|
||||||
@ -27,8 +30,11 @@ class User(val pubkey: ByteArray) {
|
|||||||
|
|
||||||
val notes = Collections.synchronizedSet(mutableSetOf<Note>())
|
val notes = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
val follows = Collections.synchronizedSet(mutableSetOf<User>())
|
val follows = Collections.synchronizedSet(mutableSetOf<User>())
|
||||||
|
|
||||||
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
|
var relays: Map<String, ContactListEvent.ReadWrite>? = null
|
||||||
|
|
||||||
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
||||||
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
||||||
|
|
||||||
@ -55,6 +61,10 @@ class User(val pubkey: ByteArray) {
|
|||||||
|
|
||||||
invalidateData()
|
invalidateData()
|
||||||
user.invalidateData()
|
user.invalidateData()
|
||||||
|
|
||||||
|
listeners.forEach {
|
||||||
|
it.onFollowsChange()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun unfollow(user: User) {
|
fun unfollow(user: User) {
|
||||||
follows.remove(user)
|
follows.remove(user)
|
||||||
@ -62,6 +72,15 @@ class User(val pubkey: ByteArray) {
|
|||||||
|
|
||||||
invalidateData()
|
invalidateData()
|
||||||
user.invalidateData()
|
user.invalidateData()
|
||||||
|
|
||||||
|
updateSubscribers {
|
||||||
|
it.onFollowsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTaggedPost(note: Note) {
|
||||||
|
taggedPosts.add(note)
|
||||||
|
updateSubscribers { it.onNewPosts() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -76,6 +95,7 @@ class User(val pubkey: ByteArray) {
|
|||||||
fun addMessage(user: User, msg: Note) {
|
fun addMessage(user: User, msg: Note) {
|
||||||
getOrCreateChannel(user).add(msg)
|
getOrCreateChannel(user).add(msg)
|
||||||
live.refresh()
|
live.refresh()
|
||||||
|
updateSubscribers { it.onNewMessage() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFollows(newFollows: Set<User>, updateAt: Long) {
|
fun updateFollows(newFollows: Set<User>, updateAt: Long) {
|
||||||
@ -95,6 +115,15 @@ class User(val pubkey: ByteArray) {
|
|||||||
updatedFollowsAt = updateAt
|
updatedFollowsAt = updateAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
|
||||||
|
if (relays != relayUse) {
|
||||||
|
relays = relayUse
|
||||||
|
listeners.forEach {
|
||||||
|
it.onRelayChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
|
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
|
||||||
info = newUserInfo
|
info = newUserInfo
|
||||||
updatedMetadataAt = updateAt
|
updatedMetadataAt = updateAt
|
||||||
@ -108,7 +137,42 @@ class User(val pubkey: ByteArray) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observers line up here.
|
// Model Observers
|
||||||
|
private var listeners = setOf<Listener>()
|
||||||
|
|
||||||
|
fun subscribe(listener: Listener) {
|
||||||
|
listeners = listeners.plus(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsubscribe(listener: Listener) {
|
||||||
|
listeners = listeners.minus(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Listener {
|
||||||
|
open fun onRelayChange() = Unit
|
||||||
|
open fun onFollowsChange() = Unit
|
||||||
|
open fun onNewPosts() = Unit
|
||||||
|
open fun onNewMessage() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refreshes observers in batches.
|
||||||
|
var modelHandlerWaiting = false
|
||||||
|
@Synchronized
|
||||||
|
fun updateSubscribers(on: (Listener) -> Unit) {
|
||||||
|
if (modelHandlerWaiting) return
|
||||||
|
|
||||||
|
modelHandlerWaiting = true
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
scope.launch {
|
||||||
|
delay(100)
|
||||||
|
listeners.forEach {
|
||||||
|
on(it)
|
||||||
|
}
|
||||||
|
modelHandlerWaiting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Observers line up here.
|
||||||
val live: UserLiveData = UserLiveData(this)
|
val live: UserLiveData = UserLiveData(this)
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
|
@ -13,27 +13,11 @@ import nostr.postr.events.TextNoteEvent
|
|||||||
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
|
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
private val cacheListener: (UserState) -> Unit = {
|
|
||||||
resetFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
if (this::account.isInitialized)
|
|
||||||
account.userProfile().live.observeForever(cacheListener)
|
|
||||||
super.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
super.stop()
|
|
||||||
if (this::account.isInitialized)
|
|
||||||
account.userProfile().live.removeObserver(cacheListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createAccountContactListFilter(): JsonFilter {
|
fun createAccountContactListFilter(): JsonFilter {
|
||||||
return JsonFilter(
|
return JsonFilter(
|
||||||
kinds = listOf(ContactListEvent.kind),
|
kinds = listOf(ContactListEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
limit = 1
|
limit = 5
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +25,7 @@ object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
|
|||||||
return JsonFilter(
|
return JsonFilter(
|
||||||
kinds = listOf(MetadataEvent.kind),
|
kinds = listOf(MetadataEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
limit = 3
|
limit = 5
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,12 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
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.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
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.Relay
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
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
|
||||||
@ -12,20 +16,22 @@ import nostr.postr.toHex
|
|||||||
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
|
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
private val cacheListener: (UserState) -> Unit = {
|
object cacheListener: User.Listener() {
|
||||||
resetFilters()
|
override fun onFollowsChange() {
|
||||||
|
resetFilters()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (this::account.isInitialized)
|
if (this::account.isInitialized)
|
||||||
account.userProfile().live.observeForever(cacheListener)
|
account.userProfile().subscribe(cacheListener)
|
||||||
super.start()
|
super.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
super.stop()
|
super.stop()
|
||||||
if (this::account.isInitialized)
|
if (this::account.isInitialized)
|
||||||
account.userProfile().live.removeObserver(cacheListener)
|
account.userProfile().unsubscribe(cacheListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowAccountsFilter(): JsonFilter {
|
fun createFollowAccountsFilter(): JsonFilter {
|
||||||
|
@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
|
|
||||||
object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") {
|
object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") {
|
||||||
|
@ -27,10 +27,9 @@ object Client: RelayPool.Listener {
|
|||||||
private var relays = Constants.defaultRelays
|
private var relays = Constants.defaultRelays
|
||||||
private val subscriptions = mutableMapOf<String, List<JsonFilter>>()
|
private val subscriptions = mutableMapOf<String, List<JsonFilter>>()
|
||||||
|
|
||||||
fun connect(
|
fun connect(relays: Array<Relay> = Constants.defaultRelays) {
|
||||||
relays: Array<Relay> = Constants.defaultRelays
|
|
||||||
) {
|
|
||||||
RelayPool.register(this)
|
RelayPool.register(this)
|
||||||
|
RelayPool.unloadRelays()
|
||||||
RelayPool.loadRelays(relays.toList())
|
RelayPool.loadRelays(relays.toList())
|
||||||
this.relays = relays
|
this.relays = relays
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,18 @@ import okhttp3.WebSocket
|
|||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
|
|
||||||
class Relay(
|
class Relay(
|
||||||
val url: String,
|
var url: String,
|
||||||
var read: Boolean = true,
|
var read: Boolean = true,
|
||||||
var write: Boolean = true
|
var write: Boolean = true
|
||||||
) {
|
) {
|
||||||
private val httpClient = OkHttpClient()
|
private val httpClient = OkHttpClient()
|
||||||
private var listeners = setOf<Listener>()
|
private var listeners = setOf<Listener>()
|
||||||
private var socket: WebSocket? = null
|
private var socket: WebSocket? = null
|
||||||
|
|
||||||
|
var eventDownloadCounter = 0
|
||||||
|
var eventUploadCounter = 0
|
||||||
|
var errorCounter = 0
|
||||||
|
|
||||||
fun register(listener: Listener) {
|
fun register(listener: Listener) {
|
||||||
listeners = listeners.plus(listener)
|
listeners = listeners.plus(listener)
|
||||||
}
|
}
|
||||||
@ -49,6 +53,7 @@ class Relay(
|
|||||||
val channel = msg[1].asString
|
val channel = msg[1].asString
|
||||||
when (type) {
|
when (type) {
|
||||||
"EVENT" -> {
|
"EVENT" -> {
|
||||||
|
eventDownloadCounter++
|
||||||
val event = Event.fromJson(msg[2], Client.lenient)
|
val event = Event.fromJson(msg[2], Client.lenient)
|
||||||
listeners.forEach { it.onEvent(this@Relay, channel, event) }
|
listeners.forEach { it.onEvent(this@Relay, channel, event) }
|
||||||
}
|
}
|
||||||
@ -88,6 +93,8 @@ class Relay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
errorCounter++
|
||||||
|
|
||||||
socket?.close(1000, "Normal close")
|
socket?.close(1000, "Normal close")
|
||||||
// Failures disconnect the relay.
|
// Failures disconnect the relay.
|
||||||
socket = null
|
socket = null
|
||||||
@ -98,6 +105,7 @@ class Relay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = httpClient.newWebSocket(request, listener)
|
socket = httpClient.newWebSocket(request, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,14 +116,17 @@ class Relay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendFilter(requestId: String) {
|
fun sendFilter(requestId: String) {
|
||||||
if (socket == null) {
|
if (read) {
|
||||||
requestAndWatch()
|
if (socket == null) {
|
||||||
} else {
|
requestAndWatch()
|
||||||
val filters = Client.getSubscriptionFilters(requestId)
|
} else {
|
||||||
if (filters.isNotEmpty()) {
|
val filters = Client.getSubscriptionFilters(requestId)
|
||||||
val request = """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]"""
|
if (filters.isNotEmpty()) {
|
||||||
//println("FILTERSSENT " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""")
|
val request =
|
||||||
socket?.send(request)
|
"""["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]"""
|
||||||
|
//println("FILTERSSENT " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""")
|
||||||
|
socket?.send(request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,8 +138,10 @@ class Relay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun send(signedEvent: Event) {
|
fun send(signedEvent: Event) {
|
||||||
if (write)
|
if (write) {
|
||||||
socket?.send("""["EVENT",${signedEvent.toJson()}]""")
|
socket?.send("""["EVENT",${signedEvent.toJson()}]""")
|
||||||
|
eventUploadCounter++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close(subscriptionId: String){
|
fun close(subscriptionId: String){
|
||||||
|
@ -24,6 +24,10 @@ object RelayPool: Relay.Listener {
|
|||||||
return relays.filter { it.isConnected() }.size
|
return relays.filter { it.isConnected() }.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRelay(url: String): Relay? {
|
||||||
|
return relays.firstOrNull() { it.url == url }
|
||||||
|
}
|
||||||
|
|
||||||
fun loadRelays(relayList: List<Relay>? = null){
|
fun loadRelays(relayList: List<Relay>? = null){
|
||||||
if (!relayList.isNullOrEmpty()){
|
if (!relayList.isNullOrEmpty()){
|
||||||
relayList.forEach { addRelay(it) }
|
relayList.forEach { addRelay(it) }
|
||||||
|
@ -15,6 +15,8 @@ import coil.decode.GifDecoder
|
|||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import com.vitorpamplona.amethyst.KeyStorage
|
import com.vitorpamplona.amethyst.KeyStorage
|
||||||
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
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
|
||||||
@ -68,24 +70,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Client.connect()
|
|
||||||
|
ServiceManager.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
NostrAccountDataSource.stop()
|
ServiceManager.pause()
|
||||||
NostrHomeDataSource.stop()
|
|
||||||
NostrChannelDataSource.stop()
|
|
||||||
NostrChatroomListDataSource.stop()
|
|
||||||
NostrUserProfileDataSource.stop()
|
|
||||||
NostrUserProfileFollowersDataSource.stop()
|
|
||||||
NostrUserProfileFollowsDataSource.stop()
|
|
||||||
|
|
||||||
NostrGlobalDataSource.stop()
|
|
||||||
NostrNotificationDataSource.stop()
|
|
||||||
NostrSingleEventDataSource.stop()
|
|
||||||
NostrSingleUserDataSource.stop()
|
|
||||||
NostrThreadDataSource.stop()
|
|
||||||
Client.disconnect()
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,369 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Checkbox
|
||||||
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.DownloadDone
|
||||||
|
import androidx.compose.material.icons.filled.SyncProblem
|
||||||
|
import androidx.compose.material.icons.filled.Upload
|
||||||
|
import androidx.compose.material.icons.outlined.BarChart
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import java.lang.Math.round
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NewRelayListView(onClose: () -> Unit, account: Account) {
|
||||||
|
val postViewModel: NewRelayListViewModel = viewModel()
|
||||||
|
|
||||||
|
val feedState by postViewModel.relays.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
postViewModel.load(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { onClose() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CloseButton(onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
PostButton(
|
||||||
|
onPost = {
|
||||||
|
postViewModel.create()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
itemsIndexed(feedState, key = { _, item -> item.url }) { index, item ->
|
||||||
|
if (index == 0)
|
||||||
|
ServerConfigHeader()
|
||||||
|
ServerConfig(item,
|
||||||
|
onToggleDownload = {
|
||||||
|
postViewModel.toggleDownload(it)
|
||||||
|
},
|
||||||
|
onToggleUpload = {
|
||||||
|
postViewModel.toggleUpload(it)
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
postViewModel.deleteRelay(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
EditableServerConfig() {
|
||||||
|
postViewModel.addRelay(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServerConfigHeader() {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Relay Address",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Posts",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Posts",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Errors",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServerConfig(
|
||||||
|
item: NewRelayListViewModel.Relay,
|
||||||
|
onToggleDownload: (NewRelayListViewModel.Relay) -> Unit,
|
||||||
|
onToggleUpload: (NewRelayListViewModel.Relay) -> Unit,
|
||||||
|
onDelete: (NewRelayListViewModel.Relay) -> Unit) {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = item.url.removePrefix("wss://"),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
onClick = { onToggleDownload(item) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Download,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||||
|
tint = if (item.read) Color.Green else Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${countToHumanReadable(item.downloadCount)}",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
onClick = { onToggleUpload(item) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Upload,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||||
|
tint = if (item.write) Color.Green else Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${countToHumanReadable(item.uploadCount)}",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SyncProblem,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||||
|
tint = if (item.errorCount > 0) Color.Yellow else Color.Green
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${countToHumanReadable(item.errorCount)}",
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
onClick = { onDelete(item) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||||
|
tint = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditableServerConfig(onNewRelay: (NewRelayListViewModel.Relay) -> Unit) {
|
||||||
|
var url by remember { mutableStateOf<String>("") }
|
||||||
|
var read by remember { mutableStateOf(true) }
|
||||||
|
var write by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = "Add a Relay") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "server.com",
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = { read = !read }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Download,
|
||||||
|
null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(35.dp)
|
||||||
|
.padding(horizontal = 5.dp),
|
||||||
|
tint = if (read) Color.Green else Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { write = !write }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Upload,
|
||||||
|
null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(35.dp)
|
||||||
|
.padding(horizontal = 5.dp),
|
||||||
|
tint = if (write) Color.Green else Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (url.isNotBlank()) {
|
||||||
|
val addedWSS = if (!url.startsWith("wss://")) "wss://$url" else url
|
||||||
|
onNewRelay(NewRelayListViewModel.Relay(addedWSS, read, write))
|
||||||
|
url = ""
|
||||||
|
write = true
|
||||||
|
read = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else Color.Gray
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = "Add", color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun countToHumanReadable(counter: Int) = when {
|
||||||
|
counter >= 1000000000 -> "${round(counter/1000000000f)}G"
|
||||||
|
counter >= 1000000 -> "${round(counter/1000000f)}M"
|
||||||
|
counter >= 1000 -> "${round(counter/1000f)}k"
|
||||||
|
else -> "$counter"
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.Constants
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import nostr.postr.events.ContactListEvent
|
||||||
|
|
||||||
|
class NewRelayListViewModel: ViewModel() {
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
data class Relay(
|
||||||
|
val url: String,
|
||||||
|
val read: Boolean,
|
||||||
|
val write: Boolean,
|
||||||
|
val errorCount: Int = 0,
|
||||||
|
val downloadCount: Int = 0,
|
||||||
|
val uploadCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _relays = MutableStateFlow<List<Relay>>(emptyList())
|
||||||
|
val relays = _relays.asStateFlow()
|
||||||
|
|
||||||
|
fun load(account: Account) {
|
||||||
|
this.account = account
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create() {
|
||||||
|
relays.let {
|
||||||
|
account.sendNewRelayList(it.value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } )
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_relays.update {
|
||||||
|
val relayFile = account.userProfile().relays
|
||||||
|
|
||||||
|
if (relayFile != null)
|
||||||
|
relayFile.map {
|
||||||
|
val liveRelay = RelayPool.getRelay(it.key)
|
||||||
|
|
||||||
|
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||||
|
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
|
||||||
|
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
|
||||||
|
|
||||||
|
Relay(it.key, it.value.read, it.value.write, errorCounter, eventDownloadCounter, eventUploadCounter)
|
||||||
|
}.sortedBy { it.downloadCount }.reversed()
|
||||||
|
else
|
||||||
|
Constants.defaultRelays.map {
|
||||||
|
val liveRelay = RelayPool.getRelay(it.url)
|
||||||
|
|
||||||
|
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||||
|
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
|
||||||
|
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
|
||||||
|
|
||||||
|
Relay(it.url, it.read, it.write, errorCounter, eventDownloadCounter, eventUploadCounter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRelay(relay: Relay) {
|
||||||
|
_relays.update {
|
||||||
|
it.plus(relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRelay(relay: Relay) {
|
||||||
|
_relays.update {
|
||||||
|
it.minus(relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleDownload(relay: Relay) {
|
||||||
|
_relays.update {
|
||||||
|
it.updated(relay, relay.copy(read = !relay.read))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleUpload(relay: Relay) {
|
||||||
|
_relays.update {
|
||||||
|
it.updated(relay, relay.copy(write = !relay.write))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Iterable<T>.updated(old: T, new: T): List<T> = map { if (it == old) new else it }
|
@ -1,7 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -20,12 +24,17 @@ import androidx.compose.material.icons.filled.ArrowBack
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@ -46,6 +55,8 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -72,49 +83,92 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var wantsToEditRelays by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantsToEditRelays)
|
||||||
|
NewRelayListView({ wantsToEditRelays = false }, account)
|
||||||
|
|
||||||
Column() {
|
Column() {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
elevation = 0.dp,
|
elevation = 0.dp,
|
||||||
backgroundColor = Color(0xFFFFFF),
|
backgroundColor = Color(0xFFFFFF),
|
||||||
title = {
|
title = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(start = 22.dp, end = 0.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
IconButton(
|
Box() {
|
||||||
onClick = {
|
Column(
|
||||||
Client.allSubscriptions().map { "${it} ${Client.getSubscriptionFilters(it).joinToString { it.toJson() }}" }.forEach {
|
modifier = Modifier
|
||||||
Log.d("CURRENT FILTERS", it)
|
.fillMaxWidth()
|
||||||
|
.padding(start = 0.dp, end = 20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
Client.allSubscriptions().map {
|
||||||
|
"${it} ${
|
||||||
|
Client.getSubscriptionFilters(it)
|
||||||
|
.joinToString { it.toJson() }
|
||||||
|
}"
|
||||||
|
}.forEach {
|
||||||
|
Log.d("CURRENT FILTERS", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
NostrAccountDataSource.printCounter()
|
||||||
|
NostrChannelDataSource.printCounter()
|
||||||
|
NostrChatRoomDataSource.printCounter()
|
||||||
|
NostrChatroomListDataSource.printCounter()
|
||||||
|
|
||||||
|
NostrGlobalDataSource.printCounter()
|
||||||
|
NostrHomeDataSource.printCounter()
|
||||||
|
NostrNotificationDataSource.printCounter()
|
||||||
|
|
||||||
|
NostrSingleEventDataSource.printCounter()
|
||||||
|
NostrSingleUserDataSource.printCounter()
|
||||||
|
NostrThreadDataSource.printCounter()
|
||||||
|
|
||||||
|
NostrUserProfileDataSource.printCounter()
|
||||||
|
NostrUserProfileFollowersDataSource.printCounter()
|
||||||
|
NostrUserProfileFollowsDataSource.printCounter()
|
||||||
|
|
||||||
|
println("AAA: " + RelayPool.connectedRelays())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.amethyst),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}",
|
||||||
|
color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = {
|
||||||
|
wantsToEditRelays = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
NostrAccountDataSource.printCounter()
|
|
||||||
NostrChannelDataSource.printCounter()
|
|
||||||
NostrChatRoomDataSource.printCounter()
|
|
||||||
NostrChatroomListDataSource.printCounter()
|
|
||||||
|
|
||||||
NostrGlobalDataSource.printCounter()
|
|
||||||
NostrHomeDataSource.printCounter()
|
|
||||||
NostrNotificationDataSource.printCounter()
|
|
||||||
|
|
||||||
NostrSingleEventDataSource.printCounter()
|
|
||||||
NostrSingleUserDataSource.printCounter()
|
|
||||||
NostrThreadDataSource.printCounter()
|
|
||||||
|
|
||||||
NostrUserProfileDataSource.printCounter()
|
|
||||||
NostrUserProfileFollowersDataSource.printCounter()
|
|
||||||
NostrUserProfileFollowsDataSource.printCounter()
|
|
||||||
|
|
||||||
println("AAA: " + RelayPool.connectedRelays())
|
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.amethyst),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
tint = Color.Unspecified
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -131,17 +185,13 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
|
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
|
||||||
contentDescription = "Profile Image",
|
contentDescription = "Profile Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(34.dp).height(34.dp)
|
.width(34.dp)
|
||||||
|
.height(34.dp)
|
||||||
.clip(shape = CircleShape),
|
.clip(shape = CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
Text(
|
|
||||||
"${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}",
|
|
||||||
color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
||||||
)
|
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {}, modifier = Modifier
|
onClick = {}, modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.DefaultChannels
|
import com.vitorpamplona.amethyst.model.DefaultChannels
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
@ -13,16 +15,24 @@ import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPreferences): ViewModel() {
|
class AccountStateViewModel(
|
||||||
|
private val encryptedPreferences: EncryptedSharedPreferences
|
||||||
|
): ViewModel() {
|
||||||
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
|
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
|
||||||
val accountContent = _accountContent.asStateFlow()
|
val accountContent = _accountContent.asStateFlow()
|
||||||
|
|
||||||
@ -65,19 +75,9 @@ class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPre
|
|||||||
else
|
else
|
||||||
_accountContent.update { AccountState.LoggedInViewOnly ( account ) }
|
_accountContent.update { AccountState.LoggedInViewOnly ( account ) }
|
||||||
|
|
||||||
NostrAccountDataSource.account = account
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
NostrHomeDataSource.account = account
|
ServiceManager.start(account)
|
||||||
NostrNotificationDataSource.account = account
|
}
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
|
|
||||||
NostrAccountDataSource.start()
|
|
||||||
NostrGlobalDataSource.start()
|
|
||||||
NostrHomeDataSource.start()
|
|
||||||
NostrNotificationDataSource.start()
|
|
||||||
NostrSingleEventDataSource.start()
|
|
||||||
NostrSingleUserDataSource.start()
|
|
||||||
NostrThreadDataSource.start()
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logOff() {
|
fun logOff() {
|
||||||
@ -90,6 +90,7 @@ class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPre
|
|||||||
encryptedPreferences.edit().apply {
|
encryptedPreferences.edit().apply {
|
||||||
remove("nostr_privkey")
|
remove("nostr_privkey")
|
||||||
remove("nostr_pubkey")
|
remove("nostr_pubkey")
|
||||||
|
remove("following_channels")
|
||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user