diff --git a/README.md b/README.md index c9928b683..d033fa7b4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Amethyst brings the best social network to your Android phone. Just insert your - [ ] Profile Edit - [ ] Relay Edit - [ ] Account Creation / Backup Guidance +- [ ] Message Sent feedback # Development Overview diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index b1f707fb5..e1770426f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -67,9 +67,12 @@ class User(val pubkey: ByteArray) { } fun updateFollows(newFollows: List, updateAt: Long) { - val toBeAdded = newFollows - follows - val toBeRemoved = follows - newFollows - + val toBeAdded = synchronized(follows) { + newFollows - follows + } + val toBeRemoved = synchronized(follows) { + follows - newFollows + } toBeAdded.forEach { follow(it) } @@ -89,6 +92,12 @@ class User(val pubkey: ByteArray) { live.refresh() } + fun isFollowing(user: User): Boolean { + return synchronized(follows) { + follows.contains(user) + } + } + // Observers line up here. val live: UserLiveData = UserLiveData(this) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 2be70a467..76d58d4c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -57,10 +57,15 @@ object NostrAccountDataSource: NostrDataSource("AccountData") { override fun feed(): List { val user = account.userProfile() - val follows = user.follows.map { it.pubkeyHex }.plus(user.pubkeyHex).toSet() + + val follows = user.follows + val followKeys = synchronized(follows) { + follows.map { it.pubkeyHex } + } + val allowSet = followKeys.plus(user.pubkeyHex).toSet() return LocalCache.notes.values - .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in follows } + .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in allowSet } .sortedBy { it.event!!.createdAt } .reversed() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 42be07919..6727663eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -50,14 +50,14 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { val messagingWith = messages.keys().toList() val privateMessages = messagingWith.mapNotNull { - messages[it]?.sortedBy { it.event?.createdAt }?.last { it.event != null } + messages[it]?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null } } val publicChannels = account.followingChannels().map { - it.notes.values.sortedBy { it.event?.createdAt }.last { it.event != null } + it.notes.values.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } } - return (privateMessages + publicChannels).sortedBy { it.event?.createdAt }.reversed() + return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed() } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 20ebe5c8b..41da73d0c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -29,17 +29,21 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { } fun createFollowAccountsFilter(): JsonFilter? { - val follows = listOf(account.userProfile().pubkeyHex.substring(0, 6)).plus( - account.userProfile().follows?.map { - it.pubkey.toHex().substring(0, 6) - } ?: emptyList() - ) + val follows = account.userProfile().follows ?: emptySet() - if (follows.isEmpty()) return null + val followKeys = synchronized(follows) { + follows.map { + it.pubkey.toHex().substring(0, 6) + } + } + + val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) + + if (followSet.isEmpty()) return null return JsonFilter( kinds = listOf(TextNoteEvent.kind, RepostEvent.kind), - authors = follows, + authors = followSet, since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 1), // 24 hours ) } @@ -64,10 +68,16 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { override fun feed(): List { val user = account.userProfile() - val follows = user.follows.map { it.pubkeyHex }.plus(user.pubkeyHex).toSet() + + val follows = user.follows + val followKeys = synchronized(follows) { + follows.map { it.pubkeyHex } + } + + val allowSet = followKeys.plus(user.pubkeyHex).toSet() return LocalCache.notes.values - .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in follows } + .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in allowSet } .sortedBy { it.event!!.createdAt } .reversed() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt index 85df90629..a02362fe5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt @@ -32,10 +32,12 @@ object NostrNotificationDataSource: NostrDataSource("GlobalFeed") { } override fun feed(): List { - return account.userProfile().taggedPosts - .filter { it.event != null } - .sortedBy { it.event!!.createdAt } - .reversed() + val set = account.userProfile().taggedPosts + val filtered = synchronized(set) { + set.filter { it.event != null } + } + + return filtered.sortedBy { it.event!!.createdAt }.reversed() } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 3da3fbeb3..cc81eb3ef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -9,9 +9,9 @@ import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { - val eventsToWatch = Collections.synchronizedList(mutableListOf()) + private var eventsToWatch = listOf() - fun createRepliesAndReactionsFilter(): JsonFilter? { + private fun createRepliesAndReactionsFilter(): JsonFilter? { val reactionsToWatch = eventsToWatch.map { it.substring(0, 8) } if (reactionsToWatch.isEmpty()) { @@ -65,12 +65,12 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { } fun add(eventId: String) { - eventsToWatch.add(eventId) + eventsToWatch = eventsToWatch.plus(eventId) resetFilters() } fun remove(eventId: String) { - eventsToWatch.remove(eventId) + eventsToWatch = eventsToWatch.minus(eventId) resetFilters() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index b533e0925..bdda0cf05 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -7,7 +7,7 @@ import nostr.postr.JsonFilter import nostr.postr.events.MetadataEvent object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { - val usersToWatch = Collections.synchronizedList(mutableListOf()) + var usersToWatch = listOf() fun createUserFilter(): JsonFilter? { if (usersToWatch.isEmpty()) return null @@ -31,12 +31,12 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { } fun add(userId: String) { - usersToWatch.add(userId) + usersToWatch = usersToWatch.plus(userId) resetFilters() } fun remove(userId: String) { - usersToWatch.remove(userId) + usersToWatch = usersToWatch.minus(userId) resetFilters() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index a2d14fdcd..082dd661b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -35,7 +35,11 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { val notesChannel = requestNewChannel() override fun feed(): List { - return user?.notes?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList() + val notes = user?.notes ?: return emptyList() + val sortedNotes = synchronized(notes) { + notes.sortedBy { it.event?.createdAt } + } + return sortedNotes.reversed() } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt index 9c7943393..9f6085832 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt @@ -22,7 +22,11 @@ object NostrUserProfileFollowersDataSource: NostrDataSource("UserProfileFo val followerChannel = requestNewChannel() override fun feed(): List { - return user?.followers?.toList() ?: emptyList() + val followers = user?.followers ?: emptyList() + + return synchronized(followers) { + followers.toList() + } } override fun updateChannelFilters() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index 507105a3e..952ed2963 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -21,7 +21,7 @@ class ChannelMuteUserEvent ( } companion object { - const val kind = 43 + const val kind = 44 fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { val content = reason diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index acfda2ef1..cb9fe1065 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -23,9 +23,9 @@ object Client: RelayPool.Listener { * something. **/ var lenient: Boolean = false - private val listeners = Collections.synchronizedSet(HashSet()) - internal var relays = Constants.defaultRelays - internal val subscriptions = ConcurrentHashMap>() + private var listeners = setOf() + private var relays = Constants.defaultRelays + private val subscriptions = mutableMapOf>() fun connect( relays: Array = Constants.defaultRelays @@ -35,17 +35,9 @@ object Client: RelayPool.Listener { this.relays = relays } - fun requestAndWatch( - subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: MutableList = mutableListOf(JsonFilter()) - ) { - subscriptions[subscriptionId] = filters - RelayPool.requestAndWatch() - } - fun sendFilter( subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: MutableList = mutableListOf(JsonFilter()) + filters: List = listOf(JsonFilter()) ) { subscriptions[subscriptionId] = filters RelayPool.sendFilter(subscriptionId) @@ -53,10 +45,10 @@ object Client: RelayPool.Listener { fun sendFilterOnlyIfDisconnected( subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: MutableList = mutableListOf(JsonFilter()) + filters: List = listOf(JsonFilter()) ) { subscriptions[subscriptionId] = filters - RelayPool.sendFilterOnlyIfDisconnected(subscriptionId) + RelayPool.sendFilterOnlyIfDisconnected() } fun send(signedEvent: Event) { @@ -90,13 +82,22 @@ object Client: RelayPool.Listener { } fun subscribe(listener: Listener) { - listeners.add(listener) + listeners = listeners.plus(listener) } - fun unsubscribe(listener: Listener): Boolean { - return listeners.remove(listener) + fun unsubscribe(listener: Listener) { + listeners = listeners.minus(listener) } + fun allSubscriptions(): List { + return synchronized(subscriptions) { + subscriptions.keys.toList() + } + } + + fun getSubscriptionFilters(subId: String): List { + return subscriptions[subId] ?: emptyList() + } abstract class Listener { /** diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index def33d7fc..960d3f3e5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.relays import com.google.gson.JsonElement import java.util.Collections -import nostr.postr.JsonFilter import nostr.postr.events.Event import okhttp3.OkHttpClient import okhttp3.Request @@ -16,27 +15,29 @@ class Relay( var write: Boolean = true ) { private val httpClient = OkHttpClient() - private val listeners = Collections.synchronizedSet(HashSet()) + private var listeners = setOf() private var socket: WebSocket? = null fun register(listener: Listener) { - listeners.add(listener) + listeners = listeners.plus(listener) + } + + fun unregister(listener: Listener) { + listeners = listeners.minus(listener) } fun isConnected(): Boolean { return socket != null } - fun unregister(listener: Listener) = listeners.remove(listener) - - fun requestAndWatch(reconnectTs: Long? = null) { + fun requestAndWatch() { val request = Request.Builder().url(url).build() val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { // Sends everything. - Client.subscriptions.forEach { - sendFilter(requestId = it.key, reconnectTs = reconnectTs) + Client.allSubscriptions().forEach { + sendFilter(requestId = it) } listeners.forEach { it.onRelayStateChange(this@Relay, Type.CONNECT) } } @@ -101,28 +102,22 @@ class Relay( socket?.close(1000, "Normal close") } - fun sendFilter(requestId: String, reconnectTs: Long? = null) { + fun sendFilter(requestId: String) { if (socket == null) { - requestAndWatch(reconnectTs) + requestAndWatch() } else { - val filters = if (reconnectTs != null) { - Client.subscriptions[requestId]?.let { - it.map { filter -> - JsonFilter(filter.ids, filter.authors, filter.kinds, filter.tags, since = reconnectTs) - } - } ?: error("No filter(s) found.") - } else { - Client.subscriptions[requestId] ?: error("No filter(s) found.") + val filters = Client.getSubscriptionFilters(requestId) + if (filters.isNotEmpty()) { + val request = """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""" + //println("FILTERSSENT " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""") + socket!!.send(request) } - val request = """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""" - //println("FILTERSSENT " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""") - socket!!.send(request) } } - fun sendFilterOnlyIfDisconnected(requestId: String, reconnectTs: Long? = null) { + fun sendFilterOnlyIfDisconnected() { if (socket == null) { - requestAndWatch(reconnectTs) + requestAndWatch() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 6b1e89a57..405c3403f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -9,8 +9,8 @@ import nostr.postr.events.Event * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. */ object RelayPool: Relay.Listener { - private val relays = Collections.synchronizedList(ArrayList()) - private val listeners = Collections.synchronizedSet(HashSet()) + private var relays = listOf() + private var listeners = setOf() fun availableRelays(): Int { return relays.size @@ -29,7 +29,8 @@ object RelayPool: Relay.Listener { } fun unloadRelays() { - relays.toList().forEach { removeRelay(it) } + relays.forEach { it.unregister(this) } + relays = listOf() } fun requestAndWatch() { @@ -40,8 +41,8 @@ object RelayPool: Relay.Listener { relays.forEach { it.sendFilter(subscriptionId) } } - fun sendFilterOnlyIfDisconnected(subscriptionId: String) { - relays.forEach { it.sendFilterOnlyIfDisconnected(subscriptionId) } + fun sendFilterOnlyIfDisconnected() { + relays.forEach { it.sendFilterOnlyIfDisconnected() } } fun send(signedEvent: Event) { @@ -61,19 +62,17 @@ object RelayPool: Relay.Listener { relays += relay } - fun removeRelay(relay: Relay): Boolean { + fun removeRelay(relay: Relay) { relay.unregister(this) - return relays.remove(relay) + relays = relays.minus(relay) } - fun getRelays(): List = relays - fun register(listener: Listener) { - listeners.add(listener) + listeners = listeners.plus(listener) } - fun unregister(listener: Listener): Boolean { - return listeners.remove(listener) + fun unregister(listener: Listener) { + listeners = listeners.minus(listener) } interface Listener { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 278e116b6..bd302b093 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -71,7 +71,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) ) { IconButton( onClick = { - Client.subscriptions.map { "${it.key} ${it.value.joinToString { it.toJson() }}" }.forEach { + Client.allSubscriptions().map { "${it} ${Client.getSubscriptionFilters(it).joinToString { it.toJson() }}" }.forEach { Log.d("CURRENT FILTERS", it) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 3112320d6..609ef53d4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -64,7 +64,7 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle } Column(modifier = Modifier.padding(start = 10.dp)) { - if (accountState?.account?.userProfile()?.follows?.contains(user) == true) { + if (accountState?.account?.userProfile()?.isFollowing(user) == true) { UnfollowButton { accountState?.account?.unfollow(user) } } else { FollowButton { accountState?.account?.follow(user) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 95d4891d4..49c86a67c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -143,7 +143,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro if (accountUser == user) { EditButton() } else { - if (accountUser.follows?.contains(user) == true) { + if (accountUser.isFollowing(user) == true) { UnfollowButton { account.unfollow(user) } } else { FollowButton { account.follow(user) }