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