Abolishes the use of Mutable collections inside data models.

This commit is contained in:
Vitor Pamplona 2023-02-03 17:23:47 -05:00
parent ee7120d803
commit 1a3b92a727
24 changed files with 219 additions and 251 deletions

View File

@ -34,8 +34,8 @@ val DefaultChannels = setOf(
class Account(
val loggedIn: Persona,
val followingChannels: MutableSet<String> = DefaultChannels.toMutableSet(),
val hiddenUsers: MutableSet<String> = mutableSetOf()
var followingChannels: Set<String> = DefaultChannels.toMutableSet(),
var hiddenUsers: Set<String> = 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? {

View File

@ -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)

View File

@ -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<User>? = null
var replyTo: MutableList<Note>? = null
var replyTo: List<Note>? = null
// These fields are updated every time an event related to this note is received.
val replies = Collections.synchronizedSet(mutableSetOf<Note>())
val reactions = Collections.synchronizedSet(mutableSetOf<Note>())
val boosts = Collections.synchronizedSet(mutableSetOf<Note>())
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
var replies = setOf<Note>()
private set
var reactions = setOf<Note>()
private set
var boosts = setOf<Note>()
private set
var reports = setOf<Note>()
private set
var channel: Channel? = null
var lastReactionsDownloadTime: Long? = null
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
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<Note> {
return synchronized(reports) {
reports.filter { it.author == user }
}
return reports.filter { it.author == user }
}
fun reportsBy(users: Set<User>): List<Note> {
return synchronized(reports) {
reports.filter { it.author in users }
}
return reports.filter { it.author in users }
}
fun directlyCiteUsers(): Set<User> {
@ -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)

View File

@ -14,8 +14,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object UrlCachedPreviewer {
val cache = ConcurrentHashMap<String, UrlInfoItem>()
val failures = ConcurrentHashMap<String, Throwable>()
var cache = mapOf<String, UrlInfoItem>()
private set
var failures = mapOf<String, Throwable>()
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()

View File

@ -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<Note>())
val follows = Collections.synchronizedSet(mutableSetOf<User>())
var follows = setOf<User>()
private set
var followers = setOf<User>()
private set
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())
var notes = setOf<Note>()
private set
var taggedPosts = setOf<Note>()
private set
var reports = setOf<Note>()
private set
var relays: Map<String, ContactListEvent.ReadWrite>? = null
private set
val followers = Collections.synchronizedSet(mutableSetOf<User>())
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
var relaysBeingUsed = mapOf<String, RelayInfo>()
private set
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
val relaysBeingUsed = Collections.synchronizedMap(mutableMapOf<String, RelayInfo>())
var messages = mapOf<User, Set<Note>>()
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<Note> {
return synchronized(reports) {
reports.filter { it.author == user }
}
return reports.filter { it.author == user }
}
fun reportsBy(users: Set<User>): List<Note> {
return synchronized(reports) {
reports.filter { it.author in users }
}
return reports.filter { it.author in users }
}
@Synchronized
fun getOrCreateChannel(user: User): MutableSet<Note> {
fun getOrCreateChannel(user: User): Set<Note> {
return messages[user] ?: run {
val channel = Collections.synchronizedSet(mutableSetOf<Note>())
messages[user] = channel
val channel = setOf<Note>()
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<User>, 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<String> {
return synchronized(relaysBeingUsed) {
relaysBeingUsed.keys.toSet()
}
}
fun getRelayValuesBeingUsed(): List<RelayInfo> {
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

View File

@ -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)
)
}

View File

@ -43,7 +43,7 @@ class Nip19 {
return null
}
fun parseTLV(data: ByteArray): Map<Byte, MutableList<ByteArray>> {
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data
while (rest.isNotEmpty()) {

View File

@ -39,14 +39,8 @@ object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
override fun feed(): List<Note> {
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()
}

View File

@ -50,11 +50,7 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
override fun feed(): List<Note> {
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() {

View File

@ -57,17 +57,10 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
// returns the last Note of each user.
override fun feed(): List<Note> {
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 {

View File

@ -35,10 +35,8 @@ import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent
abstract class NostrDataSource<T>(val debugName: String) {
private val channels = Collections.synchronizedSet(mutableSetOf<Channel>())
private val channelIds = Collections.synchronizedSet(mutableSetOf<String>())
private val eventCounter = mutableMapOf<String, Int>()
private var channels = mapOf<String, Channel>()
private var eventCounter = mapOf<String, Int>()
fun printCounter() {
eventCounter.forEach {
@ -48,12 +46,13 @@ abstract class NostrDataSource<T>(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<T>(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<T>(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<T>(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<T>(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) {

View File

@ -31,12 +31,10 @@ object NostrHomeDataSource: NostrDataSource<Note>("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<Note>("HomeFeed") {
override fun feed(): List<Note> {
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()

View File

@ -13,15 +13,15 @@ object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") {
lateinit var account: Account
override fun feed(): List<Note> {
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() {}

View File

@ -18,7 +18,7 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
private var eventsToWatch = setOf<String>()
private fun createRepliesAndReactionsFilter(): List<JsonFilter>? {
val reactionsToWatch = eventsToWatch.map { it }
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
if (reactionsToWatch.isEmpty()) {
return null
@ -26,16 +26,16 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("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
)
}
}

View File

@ -9,28 +9,23 @@ import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
private var eventsToWatch = setOf<String>()
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<Note>("SingleThreadFeed") {
val loadEventsChannel = requestNewChannel()
override fun feed(): List<Note> {
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<Note> { it.replyLevelSignature() }
return eventsToWatch.map {
LocalCache.getOrCreateNote(it)
}.sortedWith(order)
}
override fun updateChannelFilters() {
@ -84,9 +80,9 @@ object NostrThreadDataSource: NostrDataSource<Note>("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<Note>()
val threadSet = mutableSetOf<Note>()
@ -94,17 +90,12 @@ object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
loadDown(threadRoot, thread, threadSet)
// Currently orders by date of each event, descending, at each level of the reply stack
val order = compareByDescending<Note> { 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<Note>, threadSet: MutableSet<Note>) {
@ -112,9 +103,7 @@ object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
thread.add(note)
threadSet.add(note)
synchronized(note.replies) {
note.replies.toList()
}.forEach {
note.replies.forEach {
loadDown(it, thread, threadSet)
}
}

View File

@ -51,11 +51,11 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
val userInfoChannel = requestNewChannel()
override fun feed(): List<Note> {
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() {

View File

@ -16,11 +16,7 @@ object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFo
}
override fun feed(): List<User> {
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() {}

View File

@ -16,11 +16,7 @@ object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFoll
}
override fun feed(): List<User> {
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() {}

View File

@ -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<Listener>()
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()
}
}

View File

@ -26,7 +26,7 @@ class NewPostViewModel: ViewModel() {
private var originalNote: Note? = null
var mentions by mutableStateOf<List<User>?>(null)
var replyTos by mutableStateOf<MutableList<Note>?>(null)
var replyTos by mutableStateOf<List<Note>?>(null)
var message by mutableStateOf(TextFieldValue(""))
var urlPreview by mutableStateOf<String?>(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)) {

View File

@ -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()

View File

@ -24,14 +24,14 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@Composable
fun ReplyInformation(replyTo: MutableList<Note>?, mentions: List<User>?, navController: NavController) {
fun ReplyInformation(replyTo: List<Note>?, mentions: List<User>?, navController: NavController) {
ReplyInformation(replyTo, mentions) {
navController.navigate("User/${it.pubkeyHex}")
}
}
@Composable
fun ReplyInformation(replyTo: MutableList<Note>?, mentions: List<User>?, prefix: String = "", onUserTagClick: (User) -> Unit) {
fun ReplyInformation(replyTo: List<Note>?, mentions: List<User>?, prefix: String = "", onUserTagClick: (User) -> Unit) {
FlowRow() {
if (mentions != null && mentions.isNotEmpty()) {
if (replyTo != null && replyTo.isNotEmpty()) {
@ -76,7 +76,7 @@ fun ReplyInformation(replyTo: MutableList<Note>?, mentions: List<User>?, prefix:
@Composable
fun ReplyInformationChannel(replyTo: MutableList<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) {
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) {
ReplyInformationChannel(replyTo, mentions, channel,
onUserTagClick = {
navController.navigate("User/${it.pubkeyHex}")
@ -89,7 +89,7 @@ fun ReplyInformationChannel(replyTo: MutableList<Note>?, mentions: List<User>?,
@Composable
fun ReplyInformationChannel(replyTo: MutableList<Note>?,
fun ReplyInformationChannel(replyTo: List<Note>?,
mentions: List<User>?,
baseChannel: Channel,
prefix: String = "",

View File

@ -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<Note> {
// 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<Note> {
// Filter: only replies
return dataSource.feed().filter {!isNewThread(it) }.take(100)
return dataSource.feed().filter {! it.isNewThread() }.take(100)
}
}

View File

@ -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) {