diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0437f7bb3..82c31551b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -34,8 +34,8 @@ val DefaultChannels = setOf( class Account( val loggedIn: Persona, - val followingChannels: MutableSet = DefaultChannels.toMutableSet(), - val hiddenUsers: MutableSet = mutableSetOf() + var followingChannels: Set = DefaultChannels.toMutableSet(), + var hiddenUsers: Set = mutableSetOf() ) { fun userProfile(): User { @@ -91,7 +91,7 @@ class Account( fun reactTo(note: Note) { if (!isWriteable()) return - if (note.reactions.firstOrNull { it.author == userProfile() && it.event?.content == "+" } != null) { + if (note.hasReacted(userProfile(), "+")) { // has already liked this note return } @@ -106,9 +106,7 @@ class Account( fun report(note: Note, type: ReportEvent.ReportType) { if (!isWriteable()) return - if ( - note.reactions.firstOrNull { it.author == userProfile() && it.event?.content == "⚠️"} != null - ) { + if (note.hasReacted(userProfile(), "⚠️")) { // has already liked this note return } @@ -129,9 +127,7 @@ class Account( fun report(user: User, type: ReportEvent.ReportType) { if (!isWriteable()) return - if ( - user.reports.firstOrNull { it.author == userProfile() && it.event is ReportEvent && (it.event as ReportEvent).reportType.contains(type) } != null - ) { + if (user.hasReport(userProfile(), type)) { // has already reported this note return } @@ -144,11 +140,7 @@ class Account( fun boost(note: Note) { if (!isWriteable()) return - val currentTime = Date().time / 1000 - - if ( - note.boosts.firstOrNull { it.author == userProfile() && (it?.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection - ) { + if (note.hasBoosted(userProfile())) { // has already bosted in the past 5mins return } @@ -269,22 +261,22 @@ class Account( } fun joinChannel(idHex: String) { - followingChannels.add(idHex) + followingChannels = followingChannels + idHex invalidateData() } fun leaveChannel(idHex: String) { - followingChannels.remove(idHex) + followingChannels = followingChannels - idHex invalidateData() } fun hideUser(pubkeyHex: String) { - hiddenUsers.add(pubkeyHex) + hiddenUsers = hiddenUsers + pubkeyHex invalidateData() } fun showUser(pubkeyHex: String) { - hiddenUsers.remove(pubkeyHex) + hiddenUsers = hiddenUsers - pubkeyHex invalidateData() } @@ -304,7 +296,7 @@ class Account( Client.send(event) LocalCache.consume(event) - followingChannels.add(event.id.toHex()) + joinChannel(event.id.toHex()) } fun decryptContent(note: Note): String? { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 9d349ba5c..cd45087dc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -105,15 +105,15 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val mentions = Collections.synchronizedList(event.mentions.map { getOrCreateUser(it) }) - val replyTo = Collections.synchronizedList(event.replyTos.map { getOrCreateNote(it) }.toMutableList()) + val mentions = event.mentions.map { getOrCreateUser(it) } + val replyTo = event.replyTos.map { getOrCreateNote(it) } note.loadEvent(event, author, mentions, replyTo) //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}") // Prepares user's profile view. - author.notes.add(note) + author.addNote(note) // Adds notifications to users. mentions.forEach { @@ -183,7 +183,7 @@ object LocalCache { //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) }.toMutableList() + val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) } val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateUser(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -209,13 +209,13 @@ object LocalCache { //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = event.originalAuthor.map { getOrCreateUser(it) }.toList() - val repliesTo = event.boostedPost.map { getOrCreateNote(it) }.toMutableList() + val mentions = event.originalAuthor.map { getOrCreateUser(it) } + val repliesTo = event.boostedPost.map { getOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) // Prepares user's profile view. - author.notes.add(note) + author.addNote(note) // Adds notifications to users. mentions.forEach { @@ -241,7 +241,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.originalAuthor.map { getOrCreateUser(it) } - val repliesTo = event.originalPost.map { getOrCreateNote(it) }.toMutableList() + val repliesTo = event.originalPost.map { getOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -286,7 +286,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) val mentions = event.reportedAuthor.map { getOrCreateUser(it) } - val repliesTo = event.reportedPost.map { getOrCreateNote(it) }.toMutableList() + val repliesTo = event.reportedPost.map { getOrCreateNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -312,7 +312,7 @@ object LocalCache { val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) note.channel = oldChannel - note.loadEvent(event, author, emptyList(), mutableListOf()) + note.loadEvent(event, author, emptyList(), emptyList()) refreshObservers() } @@ -334,7 +334,7 @@ object LocalCache { val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) note.channel = oldChannel - note.loadEvent(event, author, emptyList(), mutableListOf()) + note.loadEvent(event, author, emptyList(), emptyList()) refreshObservers() } @@ -355,20 +355,10 @@ object LocalCache { if (note.event != null) return val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = Collections.synchronizedList(event.mentions.map { getOrCreateUser(it) }) - val replyTo = Collections.synchronizedList( - event.replyTos - .mapNotNull { - try { - getOrCreateNote(it) - } catch (e: Exception) { - println("Failed to parse Key: $it") - null - } - } - .filter { it.event !is ChannelCreateEvent } - .toMutableList() - ) + val mentions = event.mentions.map { getOrCreateUser(it) } + val replyTo = event.replyTos + .map { getOrCreateNote(it) } + .filter { it.event !is ChannelCreateEvent } note.channel = channel note.loadEvent(event, author, mentions, replyTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 3b7f051a7..3b5ab68c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import com.vitorpamplona.amethyst.service.relays.Relay +import java.util.Date import java.util.regex.Matcher import java.util.regex.Pattern import nostr.postr.events.Event @@ -33,19 +34,23 @@ class Note(val idHex: String) { var event: Event? = null var author: User? = null var mentions: List? = null - var replyTo: MutableList? = null + var replyTo: List? = null // These fields are updated every time an event related to this note is received. - val replies = Collections.synchronizedSet(mutableSetOf()) - val reactions = Collections.synchronizedSet(mutableSetOf()) - val boosts = Collections.synchronizedSet(mutableSetOf()) - val reports = Collections.synchronizedSet(mutableSetOf()) + var replies = setOf() + private set + var reactions = setOf() + private set + var boosts = setOf() + private set + var reports = setOf() + private set var channel: Channel? = null var lastReactionsDownloadTime: Long? = null - fun loadEvent(event: Event, author: User, mentions: List, replyTo: MutableList) { + fun loadEvent(event: Event, author: User, mentions: List, replyTo: List) { this.event = event this.author = author this.mentions = mentions @@ -82,47 +87,47 @@ class Note(val idHex: String) { } fun addReply(note: Note) { - if (replies.add(note)) + if (note !in replies) { + replies = replies + note liveReplies.invalidateData() + } } fun addBoost(note: Note) { - if (boosts.add(note)) + if (note !in boosts) { + boosts = boosts + note liveBoosts.invalidateData() + } } fun addReaction(note: Note) { - if (reactions.add(note)) + if (note !in reactions) { + reactions = reactions + note liveReactions.invalidateData() + } } fun addReport(note: Note) { - if (reports.add(note)) + if (note !in reports) { + reports = reports + note liveReports.invalidateData() + } } fun isReactedBy(user: User): Boolean { - return synchronized(reactions) { - reactions.any { it.author == user } - } + return reactions.any { it.author == user } } fun isBoostedBy(user: User): Boolean { - return synchronized(boosts) { - boosts.any { it.author == user } - } + return boosts.any { it.author == user } } fun reportsBy(user: User): List { - return synchronized(reports) { - reports.filter { it.author == user } - } + return reports.filter { it.author == user } } fun reportsBy(users: Set): List { - return synchronized(reports) { - reports.filter { it.author in users } - } + return reports.filter { it.author in users } } fun directlyCiteUsers(): Set { @@ -148,6 +153,19 @@ class Note(val idHex: String) { || (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) } + fun isNewThread(): Boolean { + return event is RepostEvent || replyTo == null || replyTo?.size == 0 + } + + fun hasReacted(loggedIn: User, content: String): Boolean { + return reactions.firstOrNull { it.author == loggedIn && it.event?.content == content } != null + } + + fun hasBoosted(loggedIn: User): Boolean { + val currentTime = Date().time / 1000 + return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection + } + // Observers line up here. val live: NoteLiveData = NoteLiveData(this) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index e4deb076c..74927fe09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -14,8 +14,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch object UrlCachedPreviewer { - val cache = ConcurrentHashMap() - val failures = ConcurrentHashMap() + var cache = mapOf() + private set + var failures = mapOf() + private set fun previewInfo(url: String, callback: IUrlPreviewCallback? = null) { cache[url]?.let { @@ -32,12 +34,12 @@ object UrlCachedPreviewer { scope.launch { BahaUrlPreview(url, object : IUrlPreviewCallback { override fun onComplete(urlInfo: UrlInfoItem) { - cache.put(url, urlInfo) + cache = cache + Pair(url, urlInfo) callback?.onComplete(urlInfo) } override fun onFailed(throwable: Throwable) { - failures.put(url, throwable) + failures = failures + Pair(url, throwable) callback?.onFailed(throwable) } }).fetchUrlPreview() 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 73fa2e57c..93d70cabd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.note.toShortenHex @@ -30,18 +31,27 @@ class User(val pubkeyHex: String) { var latestContactList: ContactListEvent? = null var latestMetadata: MetadataEvent? = null - val notes = Collections.synchronizedSet(mutableSetOf()) - val follows = Collections.synchronizedSet(mutableSetOf()) + var follows = setOf() + private set + var followers = setOf() + private set - val taggedPosts = Collections.synchronizedSet(mutableSetOf()) + var notes = setOf() + private set + var taggedPosts = setOf() + private set + + var reports = setOf() + private set var relays: Map? = null + private set - val followers = Collections.synchronizedSet(mutableSetOf()) - val messages = ConcurrentHashMap>() + var relaysBeingUsed = mapOf() + private set - val reports = Collections.synchronizedSet(mutableSetOf()) - val relaysBeingUsed = Collections.synchronizedMap(mutableMapOf()) + var messages = mapOf>() + private set var latestMetadataRequestEOSE: Long? = null var latestReportRequestEOSE: Long? = null @@ -64,8 +74,8 @@ class User(val pubkeyHex: String) { } fun follow(user: User, followedAt: Long) { - follows.add(user) - user.followers.add(this) + follows = follows + user + user.followers = user.followers + this liveFollows.invalidateData() user.liveFollows.invalidateData() @@ -75,8 +85,8 @@ class User(val pubkeyHex: String) { } } fun unfollow(user: User) { - follows.remove(user) - user.followers.remove(this) + follows = follows - user + user.followers = user.followers - this liveFollows.invalidateData() user.liveFollows.invalidateData() @@ -87,42 +97,50 @@ class User(val pubkeyHex: String) { } fun addTaggedPost(note: Note) { - taggedPosts.add(note) - updateSubscribers { it.onNewPosts() } + if (note !in taggedPosts) { + taggedPosts = taggedPosts + note + updateSubscribers { it.onNewTaggedPosts() } + } + } + + fun addNote(note: Note) { + if (note !in notes) { + notes = notes + note + updateSubscribers { it.onNewNotes() } + } } fun addReport(note: Note) { - if (reports.add(note)) { - updateSubscribers { it.onNewReports() } + if (note !in reports) { + reports = reports + note liveReports.invalidateData() } } fun reportsBy(user: User): List { - return synchronized(reports) { - reports.filter { it.author == user } - } + return reports.filter { it.author == user } } fun reportsBy(users: Set): List { - return synchronized(reports) { - reports.filter { it.author in users } - } + return reports.filter { it.author in users } } @Synchronized - fun getOrCreateChannel(user: User): MutableSet { + fun getOrCreateChannel(user: User): Set { return messages[user] ?: run { - val channel = Collections.synchronizedSet(mutableSetOf()) - messages[user] = channel + val channel = setOf() + messages = messages + Pair(user, channel) channel } } fun addMessage(user: User, msg: Note) { - getOrCreateChannel(user).add(msg) - liveMessages.invalidateData() - updateSubscribers { it.onNewMessage() } + val channel = getOrCreateChannel(user) + if (msg !in channel) { + messages = messages + Pair(user, channel + msg) + liveMessages.invalidateData() + updateSubscribers { it.onNewMessage() } + } } data class RelayInfo ( @@ -132,9 +150,9 @@ class User(val pubkeyHex: String) { ) fun addRelay(relay: Relay, eventTime: Long) { - val here = relaysBeingUsed.get(relay.url) + val here = relaysBeingUsed[relay.url] if (here == null) { - relaysBeingUsed.put(relay.url, RelayInfo(relay.url, eventTime, 1) ) + relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1)) } else { if (eventTime > here.lastEvent) { here.lastEvent = eventTime @@ -147,12 +165,9 @@ class User(val pubkeyHex: String) { } fun updateFollows(newFollows: Set, updateAt: Long) { - val toBeAdded = synchronized(follows) { - newFollows - follows - } - val toBeRemoved = synchronized(follows) { - follows - newFollows - } + val toBeAdded = newFollows - follows + val toBeRemoved = follows - newFollows + toBeAdded.forEach { follow(it, updateAt) } @@ -182,29 +197,21 @@ class User(val pubkeyHex: String) { } fun isFollowing(user: User): Boolean { - return synchronized(follows) { - follows.contains(user) - } - } - - fun getRelayKeysBeingUsed(): Set { - return synchronized(relaysBeingUsed) { - relaysBeingUsed.keys.toSet() - } - } - - fun getRelayValuesBeingUsed(): List { - return synchronized(relaysBeingUsed) { - relaysBeingUsed.values.toList() - } + return follows.contains(user) } fun hasSentMessagesTo(user: User?): Boolean { val messagesToUser = messages[user] ?: return false - return synchronized(messagesToUser) { - messagesToUser.firstOrNull { this == it.author } != null - } + return messagesToUser.firstOrNull { this == it.author } != null + } + + fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean { + return reports.firstOrNull { + it.author == loggedIn + && it.event is ReportEvent + && (it.event as ReportEvent).reportType.contains(type) + } != null } // Model Observers @@ -221,7 +228,8 @@ class User(val pubkeyHex: String) { abstract class Listener { open fun onRelayChange() = Unit open fun onFollowsChange() = Unit - open fun onNewPosts() = Unit + open fun onNewTaggedPosts() = Unit + open fun onNewNotes() = Unit open fun onNewMessage() = Unit open fun onNewRelayInfo() = Unit open fun onNewReports() = Unit diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Constants.kt index 97d5d884b..14430b13a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Constants.kt @@ -6,20 +6,22 @@ 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://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-2.zebedee.cloud", 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://relay.nostr.ch", read = true, write = true), + Relay("wss://no.str.cr", read = true, write = true), + Relay("wss://nos.lol", read = true, write = true) ) } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index f525882e5..46d464af1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -43,7 +43,7 @@ class Nip19 { return null } - fun parseTLV(data: ByteArray): Map> { + fun parseTLV(data: ByteArray): Map> { var result = mutableMapOf>() var rest = data while (rest.isNotEmpty()) { 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 13c234f41..41bb78d7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -39,14 +39,8 @@ object NostrAccountDataSource: NostrDataSource("AccountData") { override fun feed(): List { val user = account.userProfile() - 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 allowSet } + .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author in user.follows } .sortedBy { it.event?.createdAt } .reversed() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt index 34cfe36b9..bb1ebb883 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt @@ -50,11 +50,7 @@ object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") { override fun feed(): List { val messages = account.userProfile().messages[withUser] ?: return emptyList() - val filteredMessages = synchronized(messages) { - messages.filter { account.isAcceptable(it) } - } - - return filteredMessages.sortedBy { it.event?.createdAt }.reversed() + return messages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed() } override fun updateChannelFilters() { 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 a3b27ceaa..ed69fe3b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -57,17 +57,10 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { // returns the last Note of each user. override fun feed(): List { val messages = account.userProfile().messages - val messagingWith = messages.keys().toList().filter { account.isAcceptable(it) } + val messagingWith = messages.keys.filter { account.isAcceptable(it) } val privateMessages = messagingWith.mapNotNull { - val conversation = messages[it] - if (conversation != null) { - synchronized(conversation) { - conversation.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } - } - } else { - null - } + messages[it]?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null } } val publicChannels = account.followingChannels().map { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index d55a7a65d..ee9b62eed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -35,10 +35,8 @@ import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.TextNoteEvent abstract class NostrDataSource(val debugName: String) { - private val channels = Collections.synchronizedSet(mutableSetOf()) - private val channelIds = Collections.synchronizedSet(mutableSetOf()) - - private val eventCounter = mutableMapOf() + private var channels = mapOf() + private var eventCounter = mapOf() fun printCounter() { eventCounter.forEach { @@ -48,12 +46,13 @@ abstract class NostrDataSource(val debugName: String) { private val clientListener = object : Client.Listener() { override fun onEvent(event: Event, subscriptionId: String, relay: Relay) { - if (subscriptionId in channelIds) { + if (subscriptionId in channels.keys) { val key = "${debugName} ${subscriptionId} ${event.kind}" - if (eventCounter.contains(key)) { - eventCounter.put(key, eventCounter.get(key)!! + 1) + val keyValue = eventCounter.get(key) + if (keyValue != null) { + eventCounter = eventCounter + Pair(key, keyValue + 1) } else { - eventCounter.put(key, 1) + eventCounter = eventCounter + Pair(key, 1) } try { @@ -102,7 +101,7 @@ abstract class NostrDataSource(val debugName: String) { if (type == Relay.Type.EOSE && channel != null) { // updates a per subscripton since date - channels.filter { it.id == channel }.firstOrNull()?.updateEOSE(Date().time / 1000) + channels[channel]?.updateEOSE(Date().time / 1000) } } @@ -120,7 +119,7 @@ abstract class NostrDataSource(val debugName: String) { } open fun stop() { - channels.forEach { channel -> + channels.values.forEach { channel -> if (channel.filter != null) // if it is active, close Client.close(channel.id) } @@ -148,15 +147,13 @@ abstract class NostrDataSource(val debugName: String) { fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Channel { val newChannel = Channel(debugName+UUID.randomUUID().toString().substring(0,4), onEOSE) - channels.add(newChannel) - channelIds.add(newChannel.id) + channels = channels + Pair(newChannel.id, newChannel) return newChannel } fun dismissChannel(channel: Channel) { Client.close(channel.id) - channels.remove(channel) - channelIds.remove(channel.id) + channels = channels.minus(channel.id) } var handlerWaiting = false @@ -182,14 +179,14 @@ abstract class NostrDataSource(val debugName: String) { fun resetFiltersSuspend() { // saves the channels that are currently active - val activeChannels = channels.filter { it.filter != null } + val activeChannels = channels.values.filter { it.filter != null } // saves the current content to only update if it changes val currentFilter = activeChannels.associate { it.id to it.filter!!.joinToString("|") { it.toJson() } } updateChannelFilters() // Makes sure to only send an updated filter when it actually changes. - channels.forEach { channel -> + channels.values.forEach { channel -> val channelsNewFilter = channel.filter if (channel in activeChannels) { 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 979c5271c..aaec3293c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -31,12 +31,10 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { } fun createFollowAccountsFilter(): JsonFilter { - val follows = account.userProfile().follows ?: emptySet() + val follows = account.userProfile().follows - val followKeys = synchronized(follows) { - follows.map { - it.pubkey.toHex().substring(0, 6) - } + val followKeys = follows.map { + it.pubkey.toHex().substring(0, 6) } val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) @@ -69,15 +67,8 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { override fun feed(): List { val user = account.userProfile() - 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 allowSet } + .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author in user.follows } .filter { account.isAcceptable(it) } .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 3641c798d..9a10941ba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt @@ -13,15 +13,15 @@ object NostrNotificationDataSource: NostrDataSource("NotificationFeed") { lateinit var account: Account override fun feed(): List { - val set = account.userProfile().taggedPosts - val filtered = synchronized(set) { - set.filter { it.event != null }.filter { account.isAcceptable(it) } - } - - return filtered.filter { + return account.userProfile().taggedPosts + .filter { it.event != null } + .filter { account.isAcceptable(it) } + .filter { it.event !is ChannelCreateEvent && it.event !is ChannelMetadataEvent - }.sortedBy { it.event?.createdAt }.reversed() + } + .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 01279a1a5..29f412f5e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -18,7 +18,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() private fun createRepliesAndReactionsFilter(): List? { - val reactionsToWatch = eventsToWatch.map { it } + val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) } if (reactionsToWatch.isEmpty()) { return null @@ -26,16 +26,16 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { val now = Date().time / 1000 - return eventsToWatch.filter { - val lastTime = LocalCache.getOrCreateNote(it).lastReactionsDownloadTime; + return reactionsToWatch.filter { + val lastTime = it.lastReactionsDownloadTime; lastTime == null || lastTime < (now - 10) }.map { JsonFilter( kinds = listOf( TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind ), - tags = mapOf("e" to listOf(it)), - since = LocalCache.getOrCreateNote(it).lastReactionsDownloadTime + tags = mapOf("e" to listOf(it.idHex)), + since = it.lastReactionsDownloadTime ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index a158afc67..634522c11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -9,28 +9,23 @@ import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { - val eventsToWatch = Collections.synchronizedList(mutableListOf()) + private var eventsToWatch = setOf() fun createRepliesAndReactionsFilter(): JsonFilter? { - val reactionsToWatch = eventsToWatch.map { it } - - if (reactionsToWatch.isEmpty()) { + if (eventsToWatch.isEmpty()) { return null } return JsonFilter( kinds = listOf(TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind), - tags = mapOf("e" to reactionsToWatch) + tags = mapOf("e" to eventsToWatch.toList()) ) } fun createLoadEventsIfNotLoadedFilter(): JsonFilter? { - val nodes = synchronized(eventsToWatch) { - eventsToWatch.map { LocalCache.notes[it] } - } + val nodes = eventsToWatch.map { LocalCache.getOrCreateNote(it) } val eventsToLoad = nodes - .filterNotNull() .filter { it.event == null } .map { it.idHex.substring(0, 8) } @@ -46,11 +41,12 @@ object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { val loadEventsChannel = requestNewChannel() override fun feed(): List { - return synchronized(eventsToWatch) { - eventsToWatch.map { - LocalCache.notes[it] - }.filterNotNull() - } + // Currently orders by date of each event, descending, at each level of the reply stack + val order = compareByDescending { it.replyLevelSignature() } + + return eventsToWatch.map { + LocalCache.getOrCreateNote(it) + }.sortedWith(order) } override fun updateChannelFilters() { @@ -84,9 +80,9 @@ object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { } fun loadThread(noteId: String) { - val note = LocalCache.notes[noteId] + val note = LocalCache.getOrCreateNote(noteId) - if (note != null) { + if (note.event != null) { val thread = mutableListOf() val threadSet = mutableSetOf() @@ -94,17 +90,12 @@ object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { loadDown(threadRoot, thread, threadSet) - // Currently orders by date of each event, descending, at each level of the reply stack - val order = compareByDescending { it.replyLevelSignature() } - - eventsToWatch.clear() - eventsToWatch.addAll(thread.sortedWith(order).map { it.idHex }) + eventsToWatch = thread.map { it.idHex }.toSet() } else { - eventsToWatch.clear() - eventsToWatch.add(noteId) + eventsToWatch = setOf(noteId) } - resetFilters() + invalidateFilters() } fun loadDown(note: Note, thread: MutableList, threadSet: MutableSet) { @@ -112,9 +103,7 @@ object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { thread.add(note) threadSet.add(note) - synchronized(note.replies) { - note.replies.toList() - }.forEach { + note.replies.forEach { loadDown(it, thread, threadSet) } } 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 cc00c64f2..a77bd2c1f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -51,11 +51,11 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { val userInfoChannel = requestNewChannel() override fun feed(): List { - val notes = user?.notes ?: return emptyList() - val sortedNotes = synchronized(notes) { - notes.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt } - } - return sortedNotes.reversed() + return user?.notes + ?.filter { account.isAcceptable(it) } + ?.sortedBy { it.event?.createdAt } + ?.reversed() + ?: emptyList() } 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 a257c1120..7d80368e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt @@ -16,11 +16,7 @@ object NostrUserProfileFollowersDataSource: NostrDataSource("UserProfileFo } override fun feed(): List { - val followers = user?.followers ?: emptyList() - - return synchronized(followers) { - followers.filter { account.isAcceptable(it) }.toList() - } + return user?.followers?.filter { account.isAcceptable(it) } ?: emptyList() } override fun updateChannelFilters() {} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt index ff2de319c..ba6244ea3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt @@ -16,11 +16,7 @@ object NostrUserProfileFollowsDataSource: NostrDataSource("UserProfileFoll } override fun feed(): List { - val follows = user?.follows ?: emptyList() - - return synchronized(follows) { - follows.filter { account.isAcceptable(it) }.toList() - } + return user?.follows?.filter { account.isAcceptable(it) } ?: emptyList() } override fun updateChannelFilters() {} 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 355edbd4c..2719e2c75 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 @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays import android.util.Log import com.google.gson.JsonElement import java.util.Date +import java.util.concurrent.TimeUnit import nostr.postr.events.Event import okhttp3.OkHttpClient import okhttp3.Request @@ -15,13 +16,18 @@ class Relay( var read: Boolean = true, var write: Boolean = true ) { - private val httpClient = OkHttpClient() + private val httpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + private var listeners = setOf() private var socket: WebSocket? = null var eventDownloadCounter = 0 var eventUploadCounter = 0 var errorCounter = 0 + var ping: Long? = null var closingTime = 0L @@ -43,6 +49,8 @@ class Relay( val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { + ping = response.receivedResponseAtMillis - response.sentRequestAtMillis + // Sends everything. Client.allSubscriptions().forEach { sendFilter(requestId = it) @@ -109,8 +117,8 @@ class Relay( socket = null closingTime = Date().time / 1000 - Log.w("Relay", "Relay onFailure ${url}, ${response?.message}") - //t.printStackTrace() + Log.w("Relay", "Relay onFailure $url, ${response?.message}") + t.printStackTrace() listeners.forEach { it.onError(this@Relay, "", Error("WebSocket Failure. Response: ${response}. Exception: ${t.message}", t)) } @@ -120,7 +128,7 @@ class Relay( socket = httpClient.newWebSocket(request, listener) } catch (e: Exception) { closingTime = Date().time / 1000 - Log.e("Relay", "Relay Invalid ${url}") + Log.e("Relay", "Relay Invalid $url") e.printStackTrace() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index bf344f984..9c151483f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -26,7 +26,7 @@ class NewPostViewModel: ViewModel() { private var originalNote: Note? = null var mentions by mutableStateOf?>(null) - var replyTos by mutableStateOf?>(null) + var replyTos by mutableStateOf?>(null) var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) @@ -37,7 +37,7 @@ class NewPostViewModel: ViewModel() { fun load(account: Account, replyingTo: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> - this.replyTos = (replyNote.replyTo ?: mutableListOf()).plus(replyNote).toMutableList() + this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) replyNote.author?.let { replyUser -> val currentMentions = replyNote.mentions ?: emptyList() if (currentMentions.contains(replyUser)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 4fe5a983b..183c341dd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -115,7 +115,7 @@ private fun homeHasNewItems(cache: NotificationCache): Boolean { val homeFeed = NostrHomeDataSource.feed().take(100) val hasNewInFollows = homeFeed.filter { - it.event is RepostEvent || it.replyTo == null || it.replyTo?.size == 0 + it.isNewThread() }.filter { (it.event?.createdAt ?: 0) > lastTimeFollows }.isNotEmpty() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 4f8467343..500fc9cb2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -24,14 +24,14 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @Composable -fun ReplyInformation(replyTo: MutableList?, mentions: List?, navController: NavController) { +fun ReplyInformation(replyTo: List?, mentions: List?, navController: NavController) { ReplyInformation(replyTo, mentions) { navController.navigate("User/${it.pubkeyHex}") } } @Composable -fun ReplyInformation(replyTo: MutableList?, mentions: List?, prefix: String = "", onUserTagClick: (User) -> Unit) { +fun ReplyInformation(replyTo: List?, mentions: List?, prefix: String = "", onUserTagClick: (User) -> Unit) { FlowRow() { if (mentions != null && mentions.isNotEmpty()) { if (replyTo != null && replyTo.isNotEmpty()) { @@ -76,7 +76,7 @@ fun ReplyInformation(replyTo: MutableList?, mentions: List?, prefix: @Composable -fun ReplyInformationChannel(replyTo: MutableList?, mentions: List?, channel: Channel, navController: NavController) { +fun ReplyInformationChannel(replyTo: List?, mentions: List?, channel: Channel, navController: NavController) { ReplyInformationChannel(replyTo, mentions, channel, onUserTagClick = { navController.navigate("User/${it.pubkeyHex}") @@ -89,7 +89,7 @@ fun ReplyInformationChannel(replyTo: MutableList?, mentions: List?, @Composable -fun ReplyInformationChannel(replyTo: MutableList?, +fun ReplyInformationChannel(replyTo: List?, mentions: List?, baseChannel: Channel, prefix: String = "", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 54d642ab0..2cd17cd1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -54,21 +54,17 @@ class NostrChatroomListNewFeedViewModel: FeedViewModel(NostrChatroomListDataSour } } -fun isNewThread(note: Note): Boolean { - return note.event is RepostEvent || note.replyTo == null || note.replyTo?.size == 0 -} - class NostrHomeFeedViewModel: FeedViewModel(NostrHomeDataSource) { override fun newListFromDataSource(): List { // Filter: no replies - return dataSource.feed().filter { isNewThread(it) }.take(100) + return dataSource.feed().filter { it.isNewThread() }.take(100) } } class NostrHomeRepliesFeedViewModel: FeedViewModel(NostrHomeDataSource) { override fun newListFromDataSource(): List { // Filter: only replies - return dataSource.feed().filter {!isNewThread(it) }.take(100) + return dataSource.feed().filter {! it.isNewThread() }.take(100) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 39223bf6d..c60eb0d89 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -42,8 +42,8 @@ class RelayFeedViewModel: ViewModel() { fun refresh() { viewModelScope.launch(Dispatchers.Default) { - val beingUsed = currentUser?.getRelayValuesBeingUsed() ?: emptyList() - val beingUsedSet = currentUser?.getRelayKeysBeingUsed() ?: emptySet() + val beingUsed = currentUser?.relaysBeingUsed?.values ?: emptyList() + val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet() val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull { if (it.key !in beingUsedSet) {