diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..4d86ddb47 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file 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 b426e658f..1fe879bea 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -154,7 +154,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 @@ -167,7 +167,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) { @@ -250,7 +250,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 { @@ -275,7 +275,7 @@ class Account( if (contactList != null && follows.isNotEmpty()) { val event = ContactListEvent.create( follows.filter { it.pubKeyHex != user.pubkeyHex }, - userProfile().relays, + contactList.relays(), loggedIn.privKey!! ) @@ -454,7 +454,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) { @@ -465,7 +465,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 @@ -501,15 +501,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 { @@ -522,7 +530,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() @@ -532,8 +540,9 @@ class Account( return ( note.reportsBy(followsPlusMe) + - (note.author?.reportsBy(followsPlusMe) ?: emptyList()) + - innerReports + ( + note.author?.reportsBy(followsPlusMe) ?: emptyList() + ) + innerReports ).toSet() } @@ -575,7 +584,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 431a70470..91e8f9ffd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -4,8 +4,27 @@ 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.* +import com.vitorpamplona.amethyst.service.model.ATag +import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent +import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent +import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent +import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent +import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent +import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent +import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex import kotlinx.coroutines.* @@ -162,24 +181,15 @@ object LocalCache { return } - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = tagsWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(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) @@ -207,22 +217,13 @@ object LocalCache { return } - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = tagsWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + val replyTo = event.tagsWithoutCitations().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() } } @@ -235,7 +236,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() } @@ -252,7 +253,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) @@ -272,12 +273,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) // Replies of an Badge Definition are Award Events awardDefinition.forEach { @@ -287,53 +283,6 @@ 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]) - } - if (tag != null && tag[0] == "a") { - citations.add(tag[1]) - } - } catch (e: Exception) { - } - } - return citations - } - - private fun tagsWithoutCitations(event: TextNoteEvent): List { - val repliesTo = event.replyTos() - val tagAddresses = event.taggedAddresses().map { it.toTag() } - if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() - - val citations = findCitations(event) - - return if (citations.isEmpty()) { - repliesTo + tagAddresses - } else { - repliesTo.filter { it !in citations } - } - } - - private fun tagsWithoutCitations(event: LongTextNoteEvent): List { - val repliesTo = event.replyTos() - val tagAddresses = event.taggedAddresses().map { it.toTag() } - if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() - - val citations = findCitations(event) - - return if (citations.isEmpty()) { - repliesTo + tagAddresses - } else { - repliesTo.filter { it !in citations } - } - } - @Suppress("UNUSED_PARAMETER") fun consume(event: RecommendRelayEvent) { // // Log.d("RR", event.toJson()) @@ -341,41 +290,11 @@ object LocalCache { 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}") } @@ -397,10 +316,10 @@ 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) } + val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + .mapNotNull { checkGetOrCreateNote(it) } - note.loadEvent(event, author, mentions, repliesTo) + note.loadEvent(event, author, repliesTo) if (recipient != null) { author.addMessage(recipient, note) @@ -419,13 +338,11 @@ 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 @@ -457,23 +374,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) @@ -493,18 +401,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 == "+" || @@ -544,7 +444,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. @@ -570,7 +470,7 @@ object LocalCache { val note = getOrCreateNote(event.id) oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) + note.loadEvent(event, author, emptyList()) refreshObservers() } @@ -590,7 +490,7 @@ object LocalCache { val note = getOrCreateNote(event.id) oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) + note.loadEvent(event, author, emptyList()) refreshObservers() } @@ -631,18 +531,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) @@ -671,9 +563,12 @@ object LocalCache { val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + - ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet()) + ( + (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()}}") @@ -682,14 +577,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) } @@ -709,18 +596,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) } @@ -778,12 +657,9 @@ 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 { _ -> @@ -795,40 +671,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()) @@ -841,14 +683,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) @@ -872,7 +706,8 @@ object LocalCache { } } -class LocalCacheLiveData(val cache: LocalCache) : LiveData(LocalCacheState(cache)) { +class LocalCacheLiveData(val cache: LocalCache) : + LiveData(LocalCacheState(cache)) { // Refreshes observers in batches. var handlerWaiting = AtomicBoolean() 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 6e5a87e19..3cf1eba19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -35,7 +35,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. @@ -72,10 +71,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() @@ -215,15 +213,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 4f8efb2fc..eefdb80a7 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 java.util.regex.Pattern 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 @@ -89,54 +75,25 @@ class User(val pubkeyHex: String) { return info?.picture } - fun follow(user: User) { - 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) { - 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) { @@ -209,15 +166,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() @@ -254,22 +211,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) - 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 info?.latestMetadata = latestMetadata @@ -295,7 +236,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 eeab54f83..00b84d565 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -40,10 +40,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..ce6e4e255 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -0,0 +1,57 @@ +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 taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + 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]) + } + if (tag != null && tag[0] == "a") { + citations.add(tag[1]) + } + } catch (e: Exception) { + } + } + return citations + } + + fun tagsWithoutCitations(): List { + val repliesTo = replyTos() + val tagAddresses = taggedAddresses().map { it.toTag() } + if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() + + val citations = findCitations() + + return if (citations.isEmpty()) { + repliesTo + tagAddresses + } else { + repliesTo.filter { it !in citations } + } + } +} 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 bd907792b..f9205b61a 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 67aa25bdc..5c301b18e 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 nostr.postr.Utils import java.util.Date @@ -17,21 +18,39 @@ 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 70b237e2d..c6190f389 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,10 @@ 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 acc44c1fd..c30153767 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/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 75ce41ca2..2464eda1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -25,7 +25,12 @@ class LnZapRequestEvent( companion object { const val kind = 9734 - fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + fun create( + originalNote: EventInterface, + relays: Set, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( @@ -42,7 +47,12 @@ class LnZapRequestEvent( return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } - fun create(userHex: String, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + fun create( + userHex: String, + relays: Set, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( 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 f0ff6c3e7..46f136248 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,16 +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) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - +) : 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 faa883fb3..2a9b60857 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,16 +12,7 @@ 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) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - - fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } +) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { 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 1df19f6c3..2d43021fa 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.service.nip19.Nip19 import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator @@ -36,7 +37,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 edb72c6d4..cafe4d200 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 @@ -32,7 +32,7 @@ class NewRelayListViewModel : ViewModel() { fun clear() { _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 5eda2e560..4899dbea0 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 8b2101ce1..449cbd733 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 45b220dd9..22c3d69f7 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 707d93a50..507feaeb2 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 f25ecc17b..2fe984757 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 4249adff9..be4360c57 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 5a525e8e3..97a2d883d 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 @@ -197,11 +197,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 04f14fb09..246b1fef2 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.compose.ui.unit.sp import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +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.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -127,7 +129,10 @@ fun ChatroomCompose( ) } } 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 2ad5d5453..ac10c138e 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 @@ -294,7 +294,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( @@ -316,10 +321,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) @@ -381,14 +389,16 @@ fun NoteCompose( Text(text = stringResource(R.string.award_granted_to)) FlowRow(modifier = Modifier.padding(top = 5.dp)) { - note.mentions?.forEach { - UserPicture( - user = it, - navController = navController, - userAccount = account.userProfile(), - size = 35.dp - ) - } + noteEvent.awardees() + .map { LocalCache.getOrCreateUser(it) } + .forEach { + UserPicture( + user = it, + navController = navController, + userAccount = account.userProfile(), + size = 35.dp + ) + } } note.replyTo?.firstOrNull()?.let { 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 be5b3f150..6d4b94e82 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 @@ -26,6 +26,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController 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 @@ -100,7 +102,11 @@ private fun FeedLoaded( val route = if (channel != null) { "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 65fb60c38..8dd8df5f6 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 @@ -53,7 +53,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 08c9a1e38..041780219 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 @@ -198,13 +198,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)}") }, @@ -236,7 +236,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)}") }