Removes several Indexes from User and Notes to see if we can reduce App's memory usage.

This commit is contained in:
Vitor Pamplona
2023-03-08 08:47:33 -05:00
parent 6543df5d28
commit 3fd656da9f
26 changed files with 236 additions and 359 deletions

View File

@@ -16,7 +16,6 @@ import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -150,7 +149,7 @@ class Account(
if (!isWriteable()) return null if (!isWriteable()) return null
note.event?.let { note.event?.let {
return LnZapRequestEvent.create(it, userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
} }
return null return null
@@ -163,7 +162,7 @@ class Account(
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? { fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
if (!isWriteable()) return null if (!isWriteable()) return null
return LnZapRequestEvent.create(userPubKeyHex, userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
} }
fun report(note: Note, type: ReportEvent.ReportType) { fun report(note: Note, type: ReportEvent.ReportType) {
@@ -246,7 +245,7 @@ class Account(
val event = if (contactList != null && follows.isNotEmpty()) { val event = if (contactList != null && follows.isNotEmpty()) {
ContactListEvent.create( ContactListEvent.create(
follows.plus(Contact(user.pubkeyHex, null)), follows.plus(Contact(user.pubkeyHex, null)),
userProfile().relays, contactList.relays(),
loggedIn.privKey!!) loggedIn.privKey!!)
} else { } else {
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
@@ -270,7 +269,7 @@ class Account(
if (contactList != null && follows.isNotEmpty()) { if (contactList != null && follows.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.create(
follows.filter { it.pubKeyHex != user.pubkeyHex }, follows.filter { it.pubKeyHex != user.pubkeyHex },
userProfile().relays, contactList.relays(),
loggedIn.privKey!!) loggedIn.privKey!!)
Client.send(event) Client.send(event)
@@ -443,7 +442,7 @@ class Account(
} }
private fun updateContactListTo(newContactList: ContactListEvent?) { private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList?.follows().isNullOrEmpty()) return if (newContactList?.unverifiedFollowKeySet().isNullOrEmpty()) return
// Events might be different objects, we have to compare their ids. // Events might be different objects, we have to compare their ids.
if (backupContactList?.id != newContactList?.id) { if (backupContactList?.id != newContactList?.id) {
@@ -454,7 +453,7 @@ class Account(
// Takes a User's relay list and adds the types of feeds they are active for. // Takes a User's relay list and adds the types of feeds they are active for.
fun activeRelays(): Array<Relay>? { fun activeRelays(): Array<Relay>? {
var usersRelayList = userProfile().relays?.map { var usersRelayList = userProfile().latestContactList?.relays()?.map {
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
Relay(it.key, it.value.read, it.value.write, localFeedTypes) Relay(it.key, it.value.read, it.value.write, localFeedTypes)
} ?: return null } ?: return null
@@ -490,15 +489,23 @@ class Account(
fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers
fun followingKeySet(): Set<HexKey> {
return userProfile().latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun isAcceptable(user: User): Boolean { fun isAcceptable(user: User): Boolean {
return !isHidden(user) // if user hasn't hided this author return !isHidden(user) // if user hasn't hided this author
&& user.reportsBy( userProfile() ).isEmpty() // if user has not reported this post && user.reportsBy( userProfile() ).isEmpty() // if user has not reported this post
&& user.countReportAuthorsBy( userProfile().follows ) < 5 && user.countReportAuthorsBy( followingKeySet() ) < 5
} }
fun isAcceptableDirect(note: Note): Boolean { fun isAcceptableDirect(note: Note): Boolean {
return note.reportsBy( userProfile() ).isEmpty() // if user has not reported this post return note.reportsBy( userProfile() ).isEmpty() // if user has not reported this post
&& note.countReportAuthorsBy( userProfile().follows ) < 5 // if it has 5 reports by reliable users && note.countReportAuthorsBy( followingKeySet() ) < 5 // if it has 5 reports by reliable users
}
fun isFollowing(user: User): Boolean {
return user.pubkeyHex in followingKeySet()
} }
fun isAcceptable(note: Note): Boolean { fun isAcceptable(note: Note): Boolean {
@@ -510,7 +517,7 @@ class Account(
} }
fun getRelevantReports(note: Note): Set<Note> { fun getRelevantReports(note: Note): Set<Note> {
val followsPlusMe = userProfile().follows + userProfile() val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet()
val innerReports = if (note.event is RepostEvent) { val innerReports = if (note.event is RepostEvent) {
note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList() note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList()
@@ -518,9 +525,10 @@ class Account(
emptyList() emptyList()
} }
return (note.reportsBy(followsPlusMe) + return (
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) + note.reportsBy(followsPlusMe) +
innerReports).toSet() (note.author?.reportsBy(followsPlusMe) ?: emptyList()
) + innerReports).toSet()
} }
fun saveRelayList(value: List<RelaySetupInfo>) { fun saveRelayList(value: List<RelaySetupInfo>) {
@@ -556,7 +564,7 @@ class Account(
it.cache.spamMessages.snapshot().values.forEach { it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex) val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex)
if (userToBlock != userProfile() && userToBlock !in userProfile().follows) { if (userToBlock != userProfile() && userToBlock.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = transientHiddenUsers + it.pubkeyHex transientHiddenUsers = transientHiddenUsers + it.pubkeyHex
} }
} }

View File

@@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.service.model.ATag import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
@@ -190,25 +189,16 @@ object LocalCache {
return return
} }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } +
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, 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.addNote(note) author.addNote(note)
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
replyTo.forEach {
it.author?.addTaggedPost(note)
}
// Counts the replies // Counts the replies
replyTo.forEach { replyTo.forEach {
it.addReply(note) it.addReply(note)
@@ -236,22 +226,13 @@ object LocalCache {
return return
} }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, replyTo)
author.addNote(note) author.addNote(note)
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
replyTo.forEach {
it.author?.addTaggedPost(note)
}
refreshObservers() refreshObservers()
} }
} }
@@ -264,7 +245,7 @@ object LocalCache {
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList<User>(), emptyList<Note>()) note.loadEvent(event, author, emptyList<Note>())
refreshObservers() refreshObservers()
} }
@@ -281,7 +262,7 @@ object LocalCache {
event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) } event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList(), replyTo) note.loadEvent(event, author, replyTo)
author.updateAcceptedBadges(note) author.updateAcceptedBadges(note)
@@ -301,12 +282,7 @@ object LocalCache {
val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) } val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) }
val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) } val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, awardees, awardDefinition) note.loadEvent(event, author, awardDefinition)
// Adds notifications to users.
awardees.forEach {
it.addTaggedPost(note)
}
// Counts the replies // Counts the replies
awardees.forEach { awardees.forEach {
@@ -321,90 +297,17 @@ object LocalCache {
refreshObservers() refreshObservers()
} }
private fun findCitations(event: Event): Set<String> {
var citations = mutableSetOf<String>()
// Removes citations from replies:
val matcher = tagSearch.matcher(event.content)
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { event.tags[it.toInt()] }
if (tag != null && tag[0] == "e") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
return citations
}
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
val repliesTo = event.replyTos()
if (repliesTo.isEmpty()) return repliesTo
val citations = findCitations(event)
return if (citations.isEmpty()) {
repliesTo
} else {
repliesTo.filter { it !in citations }
}
}
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> {
val repliesTo = event.replyTos()
if (repliesTo.isEmpty()) return repliesTo
val citations = findCitations(event)
return if (citations.isEmpty()) {
repliesTo
} else {
repliesTo.filter { it !in citations }
}
}
fun consume(event: RecommendRelayEvent) { fun consume(event: RecommendRelayEvent) {
//Log.d("RR", event.toJson()) //Log.d("RR", event.toJson())
} }
fun consume(event: ContactListEvent) { fun consume(event: ContactListEvent) {
val user = getOrCreateUser(event.pubKey) val user = getOrCreateUser(event.pubKey)
val follows = event.follows() val follows = event.unverifiedFollowKeySet()
if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) { if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !follows.isNullOrEmpty()) {
// Saves relay list only if it's a user that is currently been seen // Saves relay list only if it's a user that is currently been seen
user.latestContactList = event user.updateContactList(event)
user.updateFollows(
follows.map {
try {
val pubKey = decodePublicKey(it.pubKeyHex)
getOrCreateUser(pubKey.toHexKey())
} catch (e: Exception) {
Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}")
//e.printStackTrace()
null
}
}.filterNotNull().toSet(),
event.createdAt
)
// Saves relay list only if it's a user that is currently been seen
try {
if (event.content.isNotEmpty()) {
val relays: Map<String, ContactListEvent.ReadWrite> =
Event.gson.fromJson(
event.content,
object : TypeToken<Map<String, ContactListEvent.ReadWrite>>() {}.type
)
user.updateRelays(relays)
}
} catch (e: Exception) {
Log.w("Relay List Parser","Relay import issue ${e.message}", e)
e.printStackTrace()
}
Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
} }
@@ -427,9 +330,8 @@ 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) }.mapNotNull { checkGetOrCreateNote(it) } val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
if (recipient != null) { if (recipient != null) {
author.addMessage(recipient, note) author.addMessage(recipient, note)
@@ -448,13 +350,10 @@ object LocalCache {
deleteNote.author?.removeNote(deleteNote) deleteNote.author?.removeNote(deleteNote)
// reverts the add // reverts the add
deleteNote.mentions?.forEach { user -> val mentions = deleteNote.event?.tags()?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) }
user.removeTaggedPost(deleteNote)
user.removeReport(deleteNote)
}
deleteNote.replyTo?.forEach { replyingNote -> mentions?.forEach { user ->
replyingNote.author?.removeTaggedPost(deleteNote) user.removeReport(deleteNote)
} }
// Counts the replies // Counts the replies
@@ -486,23 +385,14 @@ 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) val author = getOrCreateUser(event.pubKey)
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
// Prepares user's profile view. // Prepares user's profile view.
author.addNote(note) author.addNote(note)
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
repliesTo.forEach {
it.author?.addTaggedPost(note)
}
// Counts the replies // Counts the replies
repliesTo.forEach { repliesTo.forEach {
it.addBoost(note) it.addBoost(note)
@@ -522,18 +412,10 @@ object LocalCache {
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
//Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
repliesTo.forEach {
it.author?.addTaggedPost(note)
}
if ( if (
event.content == "" || event.content == "" ||
event.content == "+" || event.content == "+" ||
@@ -573,7 +455,7 @@ object LocalCache {
val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
//Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
@@ -598,7 +480,7 @@ object LocalCache {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
oldChannel.addNote(note) oldChannel.addNote(note)
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList())
refreshObservers() refreshObservers()
} }
@@ -621,7 +503,7 @@ object LocalCache {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
oldChannel.addNote(note) oldChannel.addNote(note)
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList())
refreshObservers() refreshObservers()
} }
@@ -662,18 +544,10 @@ object LocalCache {
.mapNotNull { checkGetOrCreateNote(it) } .mapNotNull { checkGetOrCreateNote(it) }
.filter { it.event !is ChannelCreateEvent } .filter { it.event !is ChannelCreateEvent }
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, replyTo)
//Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}") //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
replyTo.forEach {
it.author?.addTaggedPost(note)
}
// Counts the replies // Counts the replies
replyTo.forEach { replyTo.forEach {
it.addReply(note) it.addReply(note)
@@ -704,7 +578,7 @@ object LocalCache {
event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()) ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>())
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
if (zapRequest == null) { if (zapRequest == null) {
Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}") Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
@@ -713,14 +587,6 @@ object LocalCache {
//Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
repliesTo.forEach {
it.author?.addTaggedPost(note)
}
repliesTo.forEach { repliesTo.forEach {
it.addZap(zapRequest, note) it.addZap(zapRequest, note)
} }
@@ -740,18 +606,10 @@ object LocalCache {
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, repliesTo)
//Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
repliesTo.forEach {
it.author?.addTaggedPost(note)
}
repliesTo.forEach { repliesTo.forEach {
it.addZap(note, null) it.addZap(note, null)
} }
@@ -809,12 +667,7 @@ object LocalCache {
// Doesn't need to clean up the replies and mentions.. Too small to matter. // Doesn't need to clean up the replies and mentions.. Too small to matter.
// reverts the add // reverts the add
it.mentions?.forEach { user -> val mentions = it.event?.tags()?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) }
user.removeTaggedPost(it)
}
it.replyTo?.forEach { replyingNote ->
replyingNote.author?.removeTaggedPost(it)
}
// Counts the replies // Counts the replies
it.replyTo?.forEach { replyingNote -> it.replyTo?.forEach { replyingNote ->
@@ -826,40 +679,6 @@ object LocalCache {
} }
} }
fun pruneNonFollows(account: Account) {
val follows = account.userProfile().follows
val knownPMs = account.userProfile().privateChatrooms.filter {
account.userProfile().hasSentMessagesTo(it.key) && account.isAcceptable(it.key)
}
val followsFollow = follows.map {
it.follows
}.flatten()
val followSet = follows.plus(knownPMs).plus(account.userProfile()).plus(followsFollow)
val toBeRemoved = notes
.filter {
(it.value.author == null || it.value.author!! !in followSet) && it.value.event?.kind() == TextNoteEvent.kind && it.value.liveSet?.isInUse() != true
}
toBeRemoved.forEach {
notes.remove(it.key)
}
val toBeRemovedUsers = users
.filter {
(it.value !in followSet) && it.value.liveSet?.isInUse() != true
}
toBeRemovedUsers.forEach {
users.remove(it.key)
}
println("PRUNE: ${toBeRemoved.size} messages removed because they came from NonFollows")
println("PRUNE: ${toBeRemovedUsers.size} users removed because are NonFollows")
}
fun pruneHiddenMessages(account: Account) { fun pruneHiddenMessages(account: Account) {
val toBeRemoved = account.hiddenUsers.map { val toBeRemoved = account.hiddenUsers.map {
(users[it]?.notes ?: emptySet()) (users[it]?.notes ?: emptySet())
@@ -872,14 +691,6 @@ object LocalCache {
toBeRemoved.forEach { toBeRemoved.forEach {
it.author?.removeNote(it) it.author?.removeNote(it)
// reverts the add
it.mentions?.forEach { user ->
user.removeTaggedPost(it)
}
it.replyTo?.forEach { replyingNote ->
replyingNote.author?.removeTaggedPost(it)
}
// Counts the replies // Counts the replies
it.replyTo?.forEach { masterNote -> it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it) masterNote.removeReply(it)

View File

@@ -36,7 +36,6 @@ open class Note(val idHex: String) {
// They are immutable after that. // They are immutable after that.
var event: EventInterface? = null var event: EventInterface? = null
var author: User? = null var author: User? = null
var mentions: List<User>? = null
var replyTo: List<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.
@@ -73,10 +72,9 @@ open class Note(val idHex: String) {
open fun createdAt() = event?.createdAt() open fun createdAt() = event?.createdAt()
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) { fun loadEvent(event: Event, author: User, replyTo: List<Note>) {
this.event = event this.event = event
this.author = author this.author = author
this.mentions = mentions
this.replyTo = replyTo this.replyTo = replyTo
liveSet?.metadata?.invalidateData() liveSet?.metadata?.invalidateData()
@@ -217,15 +215,15 @@ open class Note(val idHex: String) {
return reports[user] ?: emptySet() return reports[user] ?: emptySet()
} }
fun reportAuthorsBy(users: Set<User>): List<User> { fun reportAuthorsBy(users: Set<HexKey>): List<User> {
return reports.keys.filter { it in users } return reports.keys.filter { it.pubkeyHex in users }
} }
fun countReportAuthorsBy(users: Set<User>): Int { fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.keys.count { it in users } return reports.keys.count { it.pubkeyHex in users }
} }
fun reportsBy(users: Set<User>): List<Note> { fun reportsBy(users: Set<HexKey>): List<Note> {
return reportAuthorsBy(users).mapNotNull { return reportAuthorsBy(users).mapNotNull {
reports[it] reports[it]
}.flatten() }.flatten()

View File

@@ -24,25 +24,14 @@ import nostr.postr.toNpub
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
class Badges(val definition: Note, val awardees: Set<Note>)
class User(val pubkeyHex: String) { class User(val pubkeyHex: String) {
var info: UserMetadata? = null var info: UserMetadata? = null
var updatedFollowsAt: Long = 0;
var latestContactList: ContactListEvent? = null var latestContactList: ContactListEvent? = null
var follows = setOf<User>()
private set
var followers = setOf<User>()
private set
var notes = setOf<Note>() var notes = setOf<Note>()
private set private set
var taggedPosts = setOf<Note>()
private set
var reports = mapOf<User, Set<Note>>() var reports = mapOf<User, Set<Note>>()
private set private set
@@ -51,9 +40,6 @@ class User(val pubkeyHex: String) {
var zaps = mapOf<Note, Note?>() var zaps = mapOf<Note, Note?>()
private set private set
var relays: Map<String, ContactListEvent.ReadWrite>? = null
private set
var relaysBeingUsed = mapOf<String, RelayInfo>() var relaysBeingUsed = mapOf<String, RelayInfo>()
private set private set
@@ -92,54 +78,25 @@ class User(val pubkeyHex: String) {
return info?.picture return info?.picture
} }
fun follow(user: User, followedAt: Long) { fun updateContactList(event: ContactListEvent) {
follows = follows + user if (event.id == latestContactList?.id) return
user.followers = user.followers + this
val oldContactListEvent = latestContactList
latestContactList = event
// Update following of the current user
liveSet?.follows?.invalidateData() liveSet?.follows?.invalidateData()
user.liveSet?.follows?.invalidateData()
}
fun unfollow(user: User) { // Update Followers of the past user list
follows = follows - user // Update Followers of the new contact list
user.followers = user.followers - this (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.follows?.invalidateData()
liveSet?.follows?.invalidateData() }
user.liveSet?.follows?.invalidateData() (latestContactList)?.unverifiedFollowKeySet()?.forEach {
} LocalCache.users[it]?.liveSet?.follows?.invalidateData()
fun follow(users: Set<User>, followedAt: Long) {
follows = follows + users
users.forEach {
if (this !in it.followers && it.liveSet?.isInUse() == true) {
it.followers = it.followers + this
it.liveSet?.follows?.invalidateData()
}
} }
liveSet?.follows?.invalidateData() liveSet?.relays?.invalidateData()
}
fun unfollow(users: Set<User>) {
follows = follows - users
users.forEach {
if (this in it.followers && it.liveSet?.isInUse() == true) {
it.followers = it.followers - this
it.liveSet?.follows?.invalidateData()
}
}
liveSet?.follows?.invalidateData()
}
fun addTaggedPost(note: Note) {
if (note !in taggedPosts) {
taggedPosts = taggedPosts + note
// No need for Listener yet
}
}
fun removeTaggedPost(note: Note) {
taggedPosts = taggedPosts - note
} }
fun addNote(note: Note) { fun addNote(note: Note) {
@@ -224,15 +181,15 @@ class User(val pubkeyHex: String) {
return reports[user] ?: emptySet() return reports[user] ?: emptySet()
} }
fun reportAuthorsBy(users: Set<User>): List<User> { fun reportAuthorsBy(users: Set<HexKey>): List<User> {
return reports.keys.filter { it in users } return reports.keys.filter { it.pubkeyHex in users }
} }
fun countReportAuthorsBy(users: Set<User>): Int { fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.keys.count { it in users } return reports.keys.count { it.pubkeyHex in users }
} }
fun reportsBy(users: Set<User>): List<Note> { fun reportsBy(users: Set<HexKey>): List<Note> {
return reportAuthorsBy(users).mapNotNull { return reportAuthorsBy(users).mapNotNull {
reports[it] reports[it]
}.flatten() }.flatten()
@@ -268,22 +225,6 @@ class User(val pubkeyHex: String) {
liveSet?.relayInfo?.invalidateData() liveSet?.relayInfo?.invalidateData()
} }
fun updateFollows(newFollows: Set<User>, updateAt: Long) {
val toBeAdded = newFollows - follows
val toBeRemoved = follows - newFollows
follow(toBeAdded, updateAt)
unfollow(toBeRemoved)
updatedFollowsAt = updateAt
}
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
// no need to test if relays are different. The Account will check for us.
relays = relayUse
liveSet?.relays?.invalidateData()
}
fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) { fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) {
info = newUserInfo info = newUserInfo
@@ -310,7 +251,29 @@ class User(val pubkeyHex: String) {
} }
fun isFollowing(user: User): Boolean { fun isFollowing(user: User): Boolean {
return follows.contains(user) return (latestContactList)?.unverifiedFollowKeySet()?.toSet()?.let {
return user.pubkeyHex in it
} ?: false
}
fun transientFollowCount(): Int? {
return latestContactList?.unverifiedFollowKeySet()?.size
}
fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
}
fun cachedFollowingKeySet(): Set<HexKey> {
return latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun cachedFollowCount(): Int? {
return latestContactList?.verifiedFollowKeySet?.size
}
fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
} }
fun hasSentMessagesTo(user: User?): Boolean { fun hasSentMessagesTo(user: User?): Boolean {

View File

@@ -37,10 +37,10 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
} }
fun createFollowAccountsFilter(): TypedFilter { fun createFollowAccountsFilter(): TypedFilter {
val follows = account.userProfile().follows val follows = account.followingKeySet()
val followKeys = follows.map { val followKeys = follows.map {
it.pubkeyHex.substring(0, 6) it.substring(0, 6)
} }
val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6))

View File

@@ -0,0 +1,48 @@
package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.tagSearch
open class BaseTextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = taggedUsers()
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun findCitations(): Set<String> {
var citations = mutableSetOf<String>()
// Removes citations from replies:
val matcher = tagSearch.matcher(content)
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag[0] == "e") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
return citations
}
fun replyToWithoutCitations(): List<String> {
val repliesTo = replyTos()
if (repliesTo.isEmpty()) return repliesTo
val citations = findCitations()
return if (citations.isEmpty()) {
repliesTo
} else {
repliesTo.filter { it !in citations }
}
}
}

View File

@@ -14,7 +14,7 @@ class ChannelCreateEvent (
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channelInfo() = try { fun channelInfo(): ChannelData = try {
MetadataEvent.gson.fromJson(content, ChannelData::class.java) MetadataEvent.gson.fromJson(content, ChannelData::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
@@ -17,20 +18,38 @@ class ContactListEvent(
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun follows() = try { // This function is only used by the user logged in
tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) } // But it is used all the time.
} catch (e: Exception) { val verifiedFollowKeySet: Set<HexKey> by lazy {
Log.e("ContactListEvent", "can't parse tags as follows: $tags", e) tags.filter { it[0] == "p" }.mapNotNull {
null it.getOrNull(1)?.let { unverifiedHex: String ->
decodePublicKey(unverifiedHex).toHexKey()
}
}.toSet()
} }
fun relayUse() = try { val verifiedFollowKeySetAndMe: Set<HexKey> by lazy {
verifiedFollowKeySet + pubKey
}
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun follows() = tags.filter { it[0] == "p" }.mapNotNull {
try {
Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2))
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
}
fun relays(): Map<String, ReadWrite>? = try {
if (content.isNotEmpty()) if (content.isNotEmpty())
gson.fromJson(content, object: TypeToken<Map<String, ReadWrite>>() {}.type) gson.fromJson(content, object: TypeToken<Map<String, ReadWrite>>() {}.type) as Map<String, ReadWrite>
else else
null null
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ContactListEvent", "can't parse content as relay lists: $tags", e) Log.w("ContactListEvent", "Can't parse content as relay lists: $content", e)
null null
} }

View File

@@ -37,6 +37,11 @@ open class Event(
override fun toJson(): String = gson.toJson(this) override fun toJson(): String = gson.toJson(this)
fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex }
/** /**
* Checks if the ID is correct and then if the pubKey's secret key signed the event. * Checks if the ID is correct and then if the pubKey's secret key signed the event.
*/ */

View File

@@ -22,4 +22,6 @@ interface EventInterface {
fun checkSignature() fun checkSignature()
fun hasValidSignature(): Boolean fun hasValidSignature(): Boolean
fun isTaggedUser(loggedInUser: String): Boolean
} }

View File

@@ -12,10 +12,7 @@ class LongTextNoteEvent(
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag(), null) fun address() = ATag(kind, pubKey, dTag(), null)

View File

@@ -12,8 +12,8 @@ class TextNoteEvent(
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: HexKey sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2) val relay = it.getOrNull(2)
@@ -21,8 +21,6 @@ class TextNoteEvent(
if (aTagValue != null) ATag.parse(aTagValue, relay) else null if (aTagValue != null) ATag.parse(aTagValue, relay) else null
} }
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object { companion object {
const val kind = 1 const val kind = 1

View File

@@ -10,6 +10,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -35,7 +36,10 @@ class NewPostViewModel: ViewModel() {
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote)
replyNote.author?.let { replyUser -> replyNote.author?.let { replyUser ->
val currentMentions = replyNote.mentions ?: emptyList() val currentMentions = (replyNote.event as? TextNoteEvent)
?.mentions()
?.map { LocalCache.getOrCreateUser(it) } ?: emptyList()
if (currentMentions.contains(replyUser)) { if (currentMentions.contains(replyUser)) {
this.mentions = currentMentions this.mentions = currentMentions
} else { } else {

View File

@@ -34,7 +34,7 @@ class NewRelayListViewModel: ViewModel() {
fun clear(ctx: Context) { fun clear(ctx: Context) {
_relays.update { _relays.update {
var relayFile = account.userProfile().relays var relayFile = account.userProfile().latestContactList?.relays()
// Ugly, but forces nostr.band as the only search-supporting relay today. // Ugly, but forces nostr.band as the only search-supporting relay today.
// TODO: Remove when search becomes more available. // TODO: Remove when search becomes more available.

View File

@@ -19,7 +19,7 @@ object GlobalFeedFilter: FeedFilter<Note>() {
// does not show events already in the public chat list // does not show events already in the public chat list
(it.channel() == null || it.channel() !in account.followingChannels()) (it.channel() == null || it.channel() !in account.followingChannels())
// does not show people the user already follows // does not show people the user already follows
&& (it.author !in account.userProfile().follows) && (it.author?.pubkeyHex !in account.followingKeySet())
} }
.filter { account.isAcceptable(it) } .filter { account.isAcceptable(it) }
.sortedBy { it.createdAt() } .sortedBy { it.createdAt() }

View File

@@ -15,7 +15,7 @@ object HomeConversationsFeedFilter: FeedFilter<Note>() {
return LocalCache.notes.values return LocalCache.notes.values
.filter { .filter {
(it.event is TextNoteEvent || it.event is RepostEvent) (it.event is TextNoteEvent || it.event is RepostEvent)
&& it.author in user.follows && it.author?.pubkeyHex in user.cachedFollowingKeySet()
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true && it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
&& !it.isNewThread() && !it.isNewThread()

View File

@@ -16,7 +16,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
val notes = LocalCache.notes.values val notes = LocalCache.notes.values
.filter { it -> .filter { it ->
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
&& it.author in user.follows && it.author?.pubkeyHex in user.cachedFollowingKeySet()
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
&& it.author?.let { !account.isHidden(it) } ?: true && it.author?.let { !account.isHidden(it) } ?: true
&& it.isNewThread() && it.isNewThread()
@@ -25,7 +25,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
val longFormNotes = LocalCache.addressables.values val longFormNotes = LocalCache.addressables.values
.filter { it -> .filter { it ->
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
&& it.author in user.follows && it.author?.pubkeyHex in user.cachedFollowingKeySet()
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
&& it.author?.let { !account.isHidden(it) } ?: true && it.author?.let { !account.isHidden(it) } ?: true
&& it.isNewThread() && it.isNewThread()

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
@@ -8,12 +9,11 @@ object NotificationFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
override fun feed(): List<Note> { override fun feed(): List<Note> {
return account.userProfile() val loggedInUser = account.userProfile().pubkeyHex
.taggedPosts return LocalCache.notes.values
.asSequence() .filter { it.event?.isTaggedUser(loggedInUser) ?: false }
.filter { .filter {
it.author == null it.author == null || (!account.isHidden(it.author!!) && it.author != account.userProfile())
|| (!account.isHidden(it.author!!) && it.author != account.userProfile())
} }
.filter { .filter {
it.event !is ChannelCreateEvent it.event !is ChannelCreateEvent

View File

@@ -14,7 +14,8 @@ object UserProfileFollowersFeedFilter: FeedFilter<User>() {
} }
override fun feed(): List<User> { override fun feed(): List<User> {
return user?.followers return user?.let { myUser ->
?.filter { account.isAcceptable(it) } ?: emptyList() LocalCache.users.values.filter { it.isFollowing(myUser) && account.isAcceptable(it) }
}?: emptyList()
} }
} }

View File

@@ -14,7 +14,9 @@ object UserProfileFollowsFeedFilter: FeedFilter<User>() {
} }
override fun feed(): List<User> { override fun feed(): List<User> {
return user?.follows return user?.latestContactList?.unverifiedFollowKeySet()?.map {
LocalCache.getOrCreateUser(it)
}
?.filter { account.isAcceptable(it) } ?.filter { account.isAcceptable(it) }
?.reversed() ?: emptyList() ?.reversed() ?: emptyList()
} }

View File

@@ -196,11 +196,11 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
} }
})) { })) {
Row() { Row() {
Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) Text("${accountUserFollows.cachedFollowCount() ?: "--"}", fontWeight = FontWeight.Bold)
Text(stringResource(R.string.following)) Text(stringResource(R.string.following))
} }
Row(modifier = Modifier.padding(start = 10.dp)) { Row(modifier = Modifier.padding(start = 10.dp)) {
Text("${accountUserFollows.followers.size}", fontWeight = FontWeight.Bold) Text("${accountUserFollows.cachedFollowerCount() ?: "--"}", fontWeight = FontWeight.Bold)
Text(stringResource(R.string.followers)) Text(stringResource(R.string.followers))
} }
} }

View File

@@ -41,9 +41,11 @@ import androidx.navigation.NavController
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -110,7 +112,10 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
} }
} else { } else {
val replyAuthorBase = note.mentions?.first() val replyAuthorBase =
(note.event as? PrivateDmEvent)
?.recipientPubKey()
?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!! var userToComposeOn = note.author!!

View File

@@ -280,7 +280,12 @@ fun NoteCompose(
Spacer(modifier = Modifier.height(3.dp)) Spacer(modifier = Modifier.height(3.dp))
if (noteEvent is TextNoteEvent && (note.replyTo != null || note.mentions != null)) { if (noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) {
val sortedMentions = noteEvent.mentions()
.map { LocalCache.getOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowing(it) }
val replyingDirectlyTo = note.replyTo?.lastOrNull() val replyingDirectlyTo = note.replyTo?.lastOrNull()
if (replyingDirectlyTo != null && unPackReply) { if (replyingDirectlyTo != null && unPackReply) {
NoteCompose( NoteCompose(
@@ -302,10 +307,13 @@ fun NoteCompose(
navController = navController navController = navController
) )
} else { } else {
ReplyInformation(note.replyTo, note.mentions, account, navController) ReplyInformation(note.replyTo, sortedMentions, account, navController)
} }
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || note.mentions != null)) { } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions() != null)) {
val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) } val sortedMentions = noteEvent.mentions()
.map { LocalCache.getOrCreateUser(it) }
.toSet()
.sortedBy { account.userProfile().isFollowing(it) }
note.channel()?.let { note.channel()?.let {
ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) ReplyInformationChannel(note.replyTo, sortedMentions, it, navController)
@@ -368,7 +376,9 @@ fun NoteCompose(
Text(text = stringResource(R.string.award_granted_to)) Text(text = stringResource(R.string.award_granted_to))
FlowRow(modifier = Modifier.padding(top = 5.dp)) { FlowRow(modifier = Modifier.padding(top = 5.dp)) {
note.mentions?.forEach { noteEvent.awardees()
.map { LocalCache.getOrCreateUser(it) }
.forEach {
UserPicture( UserPicture(
user = it, user = it,
navController = navController, navController = navController,

View File

@@ -23,6 +23,8 @@ import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -109,7 +111,11 @@ private fun FeedLoaded(
if (channel != null) { if (channel != null) {
route = "Channel/${channel.idHex}" route = "Channel/${channel.idHex}"
} else { } else {
val replyAuthorBase = note.mentions?.first() val replyAuthorBase =
(note.event as? PrivateDmEvent)
?.recipientPubKey()
?.let { LocalCache.getOrCreateUser(it) }
var userToComposeOn = note.author!! var userToComposeOn = note.author!!
if (replyAuthorBase != null) { if (replyAuthorBase != null) {
if (note.author == account.userProfile()) { if (note.author == account.userProfile()) {

View File

@@ -50,7 +50,7 @@ class RelayFeedViewModel: ViewModel() {
val beingUsed = currentUser?.relaysBeingUsed?.values ?: emptyList() val beingUsed = currentUser?.relaysBeingUsed?.values ?: emptyList()
val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet() val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet()
val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull { val newRelaysFromRecord = currentUser?.latestContactList?.relays()?.entries?.mapNotNull {
if (it.key !in beingUsedSet) { if (it.key !in beingUsedSet) {
RelayInfo(it.key, 0, 0) RelayInfo(it.key, 0, 0)
} else { } else {

View File

@@ -196,13 +196,13 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
}, },
{ {
val userState by baseUser.live().follows.observeAsState() val userState by baseUser.live().follows.observeAsState()
val userFollows = userState?.user?.follows?.size ?: "--" val userFollows = userState?.user?.transientFollowCount() ?: "--"
Text(text = "$userFollows ${stringResource(R.string.follows)}") Text(text = "$userFollows ${stringResource(R.string.follows)}")
}, },
{ {
val userState by baseUser.live().follows.observeAsState() val userState by baseUser.live().follows.observeAsState()
val userFollowers = userState?.user?.followers?.size ?: "--" val userFollowers = userState?.user?.transientFollowerCount() ?: "--"
Text(text = "$userFollowers ${stringResource(id = R.string.followers)}") Text(text = "$userFollowers ${stringResource(id = R.string.followers)}")
}, },
@@ -223,7 +223,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val userRelaysBeingUsed = userState?.user?.relaysBeingUsed?.size ?: "--" val userRelaysBeingUsed = userState?.user?.relaysBeingUsed?.size ?: "--"
val userStateRelayInfo by baseUser.live().relayInfo.observeAsState() val userStateRelayInfo by baseUser.live().relayInfo.observeAsState()
val userRelays = userStateRelayInfo?.user?.relays?.size ?: "--" val userRelays = userStateRelayInfo?.user?.latestContactList?.relays()?.size ?: "--"
Text(text = "$userRelaysBeingUsed / $userRelays ${stringResource(R.string.relays)}") Text(text = "$userRelaysBeingUsed / $userRelays ${stringResource(R.string.relays)}")
} }