Abolishes the use of Mutable collections inside data models.
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
+1
-5
@@ -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() {}
|
||||||
|
|||||||
+1
-5
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user