diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0301eda33..18ec62fcd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -16,7 +16,6 @@ import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool -import java.util.Date import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope @@ -150,7 +149,7 @@ class Account( if (!isWriteable()) return null 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 @@ -163,7 +162,7 @@ class Account( fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? { 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) { @@ -246,7 +245,7 @@ class Account( val event = if (contactList != null && follows.isNotEmpty()) { ContactListEvent.create( follows.plus(Contact(user.pubkeyHex, null)), - userProfile().relays, + contactList.relays(), loggedIn.privKey!!) } else { 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()) { val event = ContactListEvent.create( follows.filter { it.pubKeyHex != user.pubkeyHex }, - userProfile().relays, + contactList.relays(), loggedIn.privKey!!) Client.send(event) @@ -443,7 +442,7 @@ class Account( } 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. 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. fun activeRelays(): Array? { - var usersRelayList = userProfile().relays?.map { + var usersRelayList = userProfile().latestContactList?.relays()?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() Relay(it.key, it.value.read, it.value.write, localFeedTypes) } ?: return null @@ -490,15 +489,23 @@ class Account( fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers + fun followingKeySet(): Set { + return userProfile().latestContactList?.verifiedFollowKeySet ?: emptySet() + } + fun isAcceptable(user: User): Boolean { return !isHidden(user) // if user hasn't hided this author && 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 { 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 { @@ -510,7 +517,7 @@ class Account( } fun getRelevantReports(note: Note): Set { - val followsPlusMe = userProfile().follows + userProfile() + val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet() val innerReports = if (note.event is RepostEvent) { note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList() @@ -518,9 +525,10 @@ class Account( emptyList() } - return (note.reportsBy(followsPlusMe) + - (note.author?.reportsBy(followsPlusMe) ?: emptyList()) + - innerReports).toSet() + return ( + note.reportsBy(followsPlusMe) + + (note.author?.reportsBy(followsPlusMe) ?: emptyList() + ) + innerReports).toSet() } fun saveRelayList(value: List) { @@ -556,7 +564,7 @@ class Account( it.cache.spamMessages.snapshot().values.forEach { if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex) - if (userToBlock != userProfile() && userToBlock !in userProfile().follows) { + if (userToBlock != userProfile() && userToBlock.pubkeyHex !in followingKeySet()) { transientHiddenUsers = transientHiddenUsers + it.pubkeyHex } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index f68689df6..c01869a43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.lifecycle.LiveData import com.fasterxml.jackson.databind.DeserializationFeature 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.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent @@ -190,25 +189,16 @@ object LocalCache { return } - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + + val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(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)}") // Prepares user's profile view. author.addNote(note) - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - // Counts the replies replyTo.forEach { it.addReply(note) @@ -236,22 +226,13 @@ object LocalCache { return } - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, mentions, replyTo) + note.loadEvent(event, author, replyTo) author.addNote(note) - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - refreshObservers() } } @@ -264,7 +245,7 @@ object LocalCache { if (note.event?.id() == event.id()) return if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList(), emptyList()) + note.loadEvent(event, author, emptyList()) refreshObservers() } @@ -281,7 +262,7 @@ object LocalCache { event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) } if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList(), replyTo) + note.loadEvent(event, author, replyTo) author.updateAcceptedBadges(note) @@ -301,12 +282,7 @@ object LocalCache { val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) } val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) } - note.loadEvent(event, author, awardees, awardDefinition) - - // Adds notifications to users. - awardees.forEach { - it.addTaggedPost(note) - } + note.loadEvent(event, author, awardDefinition) // Counts the replies awardees.forEach { @@ -321,90 +297,17 @@ object LocalCache { refreshObservers() } - private fun findCitations(event: Event): Set { - var citations = mutableSetOf() - // 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 { - 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 { - 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) { //Log.d("RR", event.toJson()) } fun consume(event: ContactListEvent) { 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 - user.latestContactList = 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 = - Event.gson.fromJson( - event.content, - object : TypeToken>() {}.type - ) - - user.updateRelays(relays) - } - } catch (e: Exception) { - Log.w("Relay List Parser","Relay import issue ${e.message}", e) - e.printStackTrace() - } + user.updateContactList(event) Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") } @@ -427,9 +330,8 @@ object LocalCache { //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") 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) { author.addMessage(recipient, note) @@ -448,13 +350,10 @@ object LocalCache { deleteNote.author?.removeNote(deleteNote) // reverts the add - deleteNote.mentions?.forEach { user -> - user.removeTaggedPost(deleteNote) - user.removeReport(deleteNote) - } + val mentions = deleteNote.event?.tags()?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) } - deleteNote.replyTo?.forEach { replyingNote -> - replyingNote.author?.removeTaggedPost(deleteNote) + mentions?.forEach { user -> + user.removeReport(deleteNote) } // Counts the replies @@ -486,23 +385,14 @@ object LocalCache { //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") val author = getOrCreateUser(event.pubKey) - val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - note.loadEvent(event, author, mentions, repliesTo) + note.loadEvent(event, author, repliesTo) // Prepares user's profile view. author.addNote(note) - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - // Counts the replies repliesTo.forEach { it.addBoost(note) @@ -522,18 +412,10 @@ object LocalCache { val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(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)}") - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - if ( event.content == "" || event.content == "+" || @@ -573,7 +455,7 @@ object LocalCache { val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + 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)}") // Adds notifications to users. @@ -598,7 +480,7 @@ object LocalCache { val note = getOrCreateNote(event.id) oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) + note.loadEvent(event, author, emptyList()) refreshObservers() } @@ -621,7 +503,7 @@ object LocalCache { val note = getOrCreateNote(event.id) oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) + note.loadEvent(event, author, emptyList()) refreshObservers() } @@ -662,18 +544,10 @@ object LocalCache { .mapNotNull { checkGetOrCreateNote(it) } .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)}") - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - // Counts the replies replyTo.forEach { it.addReply(note) @@ -704,7 +578,7 @@ object LocalCache { event.taggedAddresses().map { getOrCreateAddressableNote(it) } + ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet()) - note.loadEvent(event, author, mentions, repliesTo) + note.loadEvent(event, author, repliesTo) if (zapRequest == null) { 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)}") - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - repliesTo.forEach { it.addZap(zapRequest, note) } @@ -740,18 +606,10 @@ object LocalCache { val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(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)}") - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - repliesTo.forEach { it.addZap(note, null) } @@ -809,12 +667,7 @@ object LocalCache { // Doesn't need to clean up the replies and mentions.. Too small to matter. // reverts the add - it.mentions?.forEach { user -> - user.removeTaggedPost(it) - } - it.replyTo?.forEach { replyingNote -> - replyingNote.author?.removeTaggedPost(it) - } + val mentions = it.event?.tags()?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) } // Counts the replies 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) { val toBeRemoved = account.hiddenUsers.map { (users[it]?.notes ?: emptySet()) @@ -872,14 +691,6 @@ object LocalCache { toBeRemoved.forEach { 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 it.replyTo?.forEach { masterNote -> masterNote.removeReply(it) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 9e9a58dfc..250c275e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -36,7 +36,6 @@ open class Note(val idHex: String) { // They are immutable after that. var event: EventInterface? = null var author: User? = null - var mentions: List? = null var replyTo: List? = null // 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() - fun loadEvent(event: Event, author: User, mentions: List, replyTo: List) { + fun loadEvent(event: Event, author: User, replyTo: List) { this.event = event this.author = author - this.mentions = mentions this.replyTo = replyTo liveSet?.metadata?.invalidateData() @@ -217,15 +215,15 @@ open class Note(val idHex: String) { return reports[user] ?: emptySet() } - fun reportAuthorsBy(users: Set): List { - return reports.keys.filter { it in users } + fun reportAuthorsBy(users: Set): List { + return reports.keys.filter { it.pubkeyHex in users } } - fun countReportAuthorsBy(users: Set): Int { - return reports.keys.count { it in users } + fun countReportAuthorsBy(users: Set): Int { + return reports.keys.count { it.pubkeyHex in users } } - fun reportsBy(users: Set): List { + fun reportsBy(users: Set): List { return reportAuthorsBy(users).mapNotNull { reports[it] }.flatten() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 690287c60..e5f91df3e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -24,25 +24,14 @@ import nostr.postr.toNpub val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") -class Badges(val definition: Note, val awardees: Set) - class User(val pubkeyHex: String) { var info: UserMetadata? = null - var updatedFollowsAt: Long = 0; var latestContactList: ContactListEvent? = null - var follows = setOf() - private set - var followers = setOf() - private set - var notes = setOf() private set - var taggedPosts = setOf() - private set - var reports = mapOf>() private set @@ -51,9 +40,6 @@ class User(val pubkeyHex: String) { var zaps = mapOf() private set - var relays: Map? = null - private set - var relaysBeingUsed = mapOf() private set @@ -92,54 +78,25 @@ class User(val pubkeyHex: String) { return info?.picture } - fun follow(user: User, followedAt: Long) { - follows = follows + user - user.followers = user.followers + this + fun updateContactList(event: ContactListEvent) { + if (event.id == latestContactList?.id) return + val oldContactListEvent = latestContactList + latestContactList = event + + // Update following of the current user liveSet?.follows?.invalidateData() - user.liveSet?.follows?.invalidateData() - } - fun unfollow(user: User) { - follows = follows - user - user.followers = user.followers - this - - liveSet?.follows?.invalidateData() - user.liveSet?.follows?.invalidateData() - } - - fun follow(users: Set, 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() - } + // Update Followers of the past user list + // Update Followers of the new contact list + (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { + LocalCache.users[it]?.liveSet?.follows?.invalidateData() + } + (latestContactList)?.unverifiedFollowKeySet()?.forEach { + LocalCache.users[it]?.liveSet?.follows?.invalidateData() } - liveSet?.follows?.invalidateData() - } - - fun unfollow(users: Set) { - 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 + liveSet?.relays?.invalidateData() } fun addNote(note: Note) { @@ -224,15 +181,15 @@ class User(val pubkeyHex: String) { return reports[user] ?: emptySet() } - fun reportAuthorsBy(users: Set): List { - return reports.keys.filter { it in users } + fun reportAuthorsBy(users: Set): List { + return reports.keys.filter { it.pubkeyHex in users } } - fun countReportAuthorsBy(users: Set): Int { - return reports.keys.count { it in users } + fun countReportAuthorsBy(users: Set): Int { + return reports.keys.count { it.pubkeyHex in users } } - fun reportsBy(users: Set): List { + fun reportsBy(users: Set): List { return reportAuthorsBy(users).mapNotNull { reports[it] }.flatten() @@ -268,22 +225,6 @@ class User(val pubkeyHex: String) { liveSet?.relayInfo?.invalidateData() } - - fun updateFollows(newFollows: Set, updateAt: Long) { - val toBeAdded = newFollows - follows - val toBeRemoved = follows - newFollows - - follow(toBeAdded, updateAt) - unfollow(toBeRemoved) - - updatedFollowsAt = updateAt - } - - fun updateRelays(relayUse: Map) { - // 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) { info = newUserInfo @@ -310,7 +251,29 @@ class User(val pubkeyHex: String) { } 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 { + 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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index c4ba61022..49c231442 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -37,10 +37,10 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { } fun createFollowAccountsFilter(): TypedFilter { - val follows = account.userProfile().follows + val follows = account.followingKeySet() val followKeys = follows.map { - it.pubkeyHex.substring(0, 6) + it.substring(0, 6) } val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt new file mode 100644 index 000000000..ea7fbb5b2 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -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>, + 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 { + var citations = mutableSetOf() + // 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 { + val repliesTo = replyTos() + if (repliesTo.isEmpty()) return repliesTo + + val citations = findCitations() + + return if (citations.isEmpty()) { + repliesTo + } else { + repliesTo.filter { it !in citations } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index 6a1d93f93..230bff7ae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -14,7 +14,7 @@ class ChannelCreateEvent ( content: String, sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun channelInfo() = try { + fun channelInfo(): ChannelData = try { MetadataEvent.gson.fromJson(content, ChannelData::class.java) } catch (e: Exception) { Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt index 502fb7551..6286a5d22 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils @@ -17,20 +18,38 @@ class ContactListEvent( content: String, sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun follows() = try { - tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) } - } catch (e: Exception) { - Log.e("ContactListEvent", "can't parse tags as follows: $tags", e) - null + // This function is only used by the user logged in + // But it is used all the time. + val verifiedFollowKeySet: Set by lazy { + tags.filter { it[0] == "p" }.mapNotNull { + it.getOrNull(1)?.let { unverifiedHex: String -> + decodePublicKey(unverifiedHex).toHexKey() + } + }.toSet() } - fun relayUse() = try { + val verifiedFollowKeySetAndMe: Set 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? = try { if (content.isNotEmpty()) - gson.fromJson(content, object: TypeToken>() {}.type) + gson.fromJson(content, object: TypeToken>() {}.type) as Map else null } 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 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 7ba3de1f5..056284167 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -37,6 +37,11 @@ open class Event( 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. */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 91dfbf96e..be17a058f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -22,4 +22,6 @@ interface EventInterface { fun checkSignature() fun hasValidSignature(): Boolean + + fun isTaggedUser(loggedInUser: String): Boolean } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 1824995e8..e9e966934 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -12,10 +12,7 @@ class LongTextNoteEvent( tags: List>, content: String, sig: HexKey -): Event(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) } - +): BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun address() = ATag(kind, pubKey, dTag(), null) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index ed5460f34..9f1b156b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -12,8 +12,8 @@ class TextNoteEvent( tags: List>, content: String, sig: HexKey -): Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } +): BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { val aTagValue = it.getOrNull(1) val relay = it.getOrNull(2) @@ -21,8 +21,6 @@ class TextNoteEvent( if (aTagValue != null) ATag.parse(aTagValue, relay) else null } - fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - companion object { const val kind = 1 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 7da8b44bb..4ef8be924 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.noProtocolUrlValidator import kotlinx.coroutines.flow.MutableSharedFlow @@ -35,7 +36,10 @@ class NewPostViewModel: ViewModel() { replyingTo?.let { replyNote -> this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) 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)) { this.mentions = currentMentions } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index 5d0c86866..d6f0471f3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -34,7 +34,7 @@ class NewRelayListViewModel: ViewModel() { fun clear(ctx: Context) { _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. // TODO: Remove when search becomes more available. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index bc3866a62..824a510bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -19,7 +19,7 @@ object GlobalFeedFilter: FeedFilter() { // does not show events already in the public chat list (it.channel() == null || it.channel() !in account.followingChannels()) // does not show people the user already follows - && (it.author !in account.userProfile().follows) + && (it.author?.pubkeyHex !in account.followingKeySet()) } .filter { account.isAcceptable(it) } .sortedBy { it.createdAt() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 5fd9e487c..c219ce612 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -15,7 +15,7 @@ object HomeConversationsFeedFilter: FeedFilter() { return LocalCache.notes.values .filter { (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 && it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true && !it.isNewThread() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 3abb99e4d..347d7ec06 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -16,7 +16,7 @@ object HomeNewThreadFeedFilter: FeedFilter() { val notes = LocalCache.notes.values .filter { it -> (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 && it.author?.let { !account.isHidden(it) } ?: true && it.isNewThread() @@ -25,7 +25,7 @@ object HomeNewThreadFeedFilter: FeedFilter() { val longFormNotes = LocalCache.addressables.values .filter { it -> (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 && it.author?.let { !account.isHidden(it) } ?: true && it.isNewThread() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 348981329..4cb5ef65e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* @@ -8,12 +9,11 @@ object NotificationFeedFilter: FeedFilter() { lateinit var account: Account override fun feed(): List { - return account.userProfile() - .taggedPosts - .asSequence() + val loggedInUser = account.userProfile().pubkeyHex + return LocalCache.notes.values + .filter { it.event?.isTaggedUser(loggedInUser) ?: false } .filter { - it.author == null - || (!account.isHidden(it.author!!) && it.author != account.userProfile()) + it.author == null || (!account.isHidden(it.author!!) && it.author != account.userProfile()) } .filter { it.event !is ChannelCreateEvent diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt index f76fc4f14..36f47d558 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt @@ -14,7 +14,8 @@ object UserProfileFollowersFeedFilter: FeedFilter() { } override fun feed(): List { - return user?.followers - ?.filter { account.isAcceptable(it) } ?: emptyList() + return user?.let { myUser -> + LocalCache.users.values.filter { it.isFollowing(myUser) && account.isAcceptable(it) } + }?: emptyList() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt index af3b9490e..f5411ba0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt @@ -14,7 +14,9 @@ object UserProfileFollowsFeedFilter: FeedFilter() { } override fun feed(): List { - return user?.follows + return user?.latestContactList?.unverifiedFollowKeySet()?.map { + LocalCache.getOrCreateUser(it) + } ?.filter { account.isAcceptable(it) } ?.reversed() ?: emptyList() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index a5a3bff10..23ffeee1f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -196,11 +196,11 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol } })) { Row() { - Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) + Text("${accountUserFollows.cachedFollowCount() ?: "--"}", fontWeight = FontWeight.Bold) Text(stringResource(R.string.following)) } 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)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 57d747086..d991909da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -41,9 +41,11 @@ import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.RoboHashCache +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent 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.ResizeImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -110,7 +112,10 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr } } else { - val replyAuthorBase = note.mentions?.first() + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } var userToComposeOn = note.author!! diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 6d41c140e..aaf70e181 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -280,7 +280,12 @@ fun NoteCompose( 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() if (replyingDirectlyTo != null && unPackReply) { NoteCompose( @@ -302,10 +307,13 @@ fun NoteCompose( navController = navController ) } 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)) { - val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) } + } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions() != null)) { + val sortedMentions = noteEvent.mentions() + .map { LocalCache.getOrCreateUser(it) } + .toSet() + .sortedBy { account.userProfile().isFollowing(it) } note.channel()?.let { ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) @@ -368,7 +376,9 @@ fun NoteCompose( Text(text = stringResource(R.string.award_granted_to)) FlowRow(modifier = Modifier.padding(top = 5.dp)) { - note.mentions?.forEach { + noteEvent.awardees() + .map { LocalCache.getOrCreateUser(it) } + .forEach { UserPicture( user = it, navController = navController, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index dd9f9f4a8..1e4591c74 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -23,6 +23,8 @@ import androidx.navigation.NavController import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 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.screen.loggedIn.AccountViewModel @@ -109,7 +111,11 @@ private fun FeedLoaded( if (channel != null) { route = "Channel/${channel.idHex}" } else { - val replyAuthorBase = note.mentions?.first() + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } + var userToComposeOn = note.author!! if (replyAuthorBase != null) { if (note.author == account.userProfile()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 99e2d40e4..50934fbab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -50,7 +50,7 @@ class RelayFeedViewModel: ViewModel() { val beingUsed = currentUser?.relaysBeingUsed?.values ?: emptyList() val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet() - val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull { + val newRelaysFromRecord = currentUser?.latestContactList?.relays()?.entries?.mapNotNull { if (it.key !in beingUsedSet) { RelayInfo(it.key, 0, 0) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 75b24de11..ef2d7938d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -196,13 +196,13 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro }, { 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)}") }, { 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)}") }, @@ -223,7 +223,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro val userRelaysBeingUsed = userState?.user?.relaysBeingUsed?.size ?: "--" 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)}") }