diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LargeCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LargeCache.kt new file mode 100644 index 000000000..fe33eff40 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LargeCache.kt @@ -0,0 +1,323 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model + +import java.util.concurrent.ConcurrentSkipListMap +import java.util.function.BiConsumer + +class LargeCache { + val cache = ConcurrentSkipListMap() + + fun get(key: K) = cache.get(key) + + fun remove(key: K) = cache.remove(key) + + fun size() = cache.size + + fun getOrCreate( + key: K, + builder: (key: K) -> V, + ): V { + val value = cache.get(key) + + return if (value != null) { + value + } else { + val newObject = builder(key) + cache.putIfAbsent(key, newObject) ?: newObject + } + } + + fun forEach(consumer: BiConsumer) { + cache.forEach(consumer) + } + + fun filter(consumer: BiFilter): List { + val runner = BiFilterCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun filterIntoSet(consumer: BiFilter): Set { + val runner = BiFilterUniqueCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun mapNotNull(consumer: BiMapper): List { + val runner = BiMapCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun mapNotNullIntoSet(consumer: BiMapper): Set { + val runner = BiMapUniqueCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun mapFlatten(consumer: BiMapper?>): List { + val runner = BiMapFlattenCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun mapFlattenIntoSet(consumer: BiMapper?>): Set { + val runner = BiMapFlattenUniqueCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun map(consumer: BiNotNullMapper): List { + val runner = BiNotNullMapCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun sumOf(consumer: BiSumOf): Int { + val runner = BiSumOfCollector(consumer) + cache.forEach(runner) + return runner.sum + } + + fun sumOfLong(consumer: BiSumOfLong): Long { + val runner = BiSumOfLongCollector(consumer) + cache.forEach(runner) + return runner.sum + } + + fun groupBy(consumer: BiNotNullMapper): Map> { + val runner = BiGroupByCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun countByGroup(consumer: BiNotNullMapper): Map { + val runner = BiCountByGroupCollector(consumer) + cache.forEach(runner) + return runner.results + } + + fun count(consumer: BiFilter): Int { + val runner = BiCountIfCollector(consumer) + cache.forEach(runner) + return runner.count + } +} + +fun interface BiFilter { + fun filter( + k: K, + v: V, + ): Boolean +} + +class BiFilterCollector(val filter: BiFilter) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } +} + +class BiFilterUniqueCollector(val filter: BiFilter) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } +} + +fun interface BiMapper { + fun map( + k: K, + v: V, + ): R? +} + +class BiMapCollector(val mapper: BiMapper) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } +} + +class BiMapUniqueCollector(val mapper: BiMapper) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } +} + +class BiMapFlattenCollector(val mapper: BiMapper?>) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } +} + +class BiMapFlattenUniqueCollector(val mapper: BiMapper?>) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } +} + +fun interface BiNotNullMapper { + fun map( + k: K, + v: V, + ): R +} + +class BiNotNullMapCollector(val mapper: BiNotNullMapper) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + results.add(mapper.map(k, v)) + } +} + +fun interface BiSumOf { + fun map( + k: K, + v: V, + ): Int +} + +class BiSumOfCollector(val mapper: BiSumOf) : BiConsumer { + var sum = 0 + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } +} + +fun interface BiSumOfLong { + fun map( + k: K, + v: V, + ): Long +} + +class BiSumOfLongCollector(val mapper: BiSumOfLong) : BiConsumer { + var sum = 0L + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } +} + +class BiGroupByCollector(val mapper: BiNotNullMapper) : BiConsumer { + var results = HashMap>() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(v) + results[group] = answer + } else { + list.add(v) + } + } +} + +class BiCountByGroupCollector(val mapper: BiNotNullMapper) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val count = results[group] + if (count == null) { + results[group] = 1 + } else { + results[group] = count + 1 + } + } +} + +class BiCountIfCollector(val filter: BiFilter) : BiConsumer { + var count = 0 + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) count++ + } +} 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 f211e30f4..5a59b6477 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -120,30 +120,16 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap -import kotlin.time.measureTimedValue object LocalCache { val antiSpam = AntiSpamFilter() - private val users = ConcurrentHashMap(5000) - private val notes = ConcurrentHashMap(5000) + val users = LargeCache() + val notes = LargeCache() + val addressables = LargeCache() + val channels = ConcurrentHashMap() - val addressables = ConcurrentHashMap(100) - - val awaitingPaymentRequests = - ConcurrentHashMap Unit>>(10) - - var noteListCache: List = emptyList() - var userListCache: List = emptyList() - - fun updateListCache() { - val (value, elapsed) = - measureTimedValue { - noteListCache = ArrayList(notes.values) - userListCache = ArrayList(users.values) - } - Log.d("LocalCache", "UpdateListCache $elapsed") - } + val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) fun checkGetOrCreateUser(key: String): User? { // checkNotInMainThread() @@ -156,27 +142,24 @@ object LocalCache { fun getOrCreateUser(key: HexKey): User { // checkNotInMainThread() + require(isValidHex(key = key)) { "$key is not a valid hex" } - return users[key] - ?: run { - require(isValidHex(key = key)) { "$key is not a valid hex" } - - val newObject = User(key) - users.putIfAbsent(key, newObject) ?: newObject - } + return users.getOrCreate(key) { + User(it) + } } fun getUserIfExists(key: String): User? { if (key.isEmpty()) return null - return users[key] + return users.get(key) } fun getAddressableNoteIfExists(key: String): AddressableNote? { - return addressables[key] + return addressables.get(key) } fun getNoteIfExists(key: String): Note? { - return addressables[key] ?: notes[key] + return addressables.get(key) ?: notes.get(key) } fun getChannelIfExists(key: String): Channel? { @@ -216,24 +199,21 @@ object LocalCache { ): Note { checkNotInMainThread() - return notes.get(idHex) - ?: run { - require(isValidHex(idHex)) { "$idHex is not a valid hex" } + require(isValidHex(idHex)) { "$idHex is not a valid hex" } - notes.putIfAbsent(idHex, note) ?: note - } + return notes.getOrCreate(idHex) { + note + } } fun getOrCreateNote(idHex: String): Note { checkNotInMainThread() - return notes.get(idHex) - ?: run { - require(isValidHex(idHex)) { "$idHex is not a valid hex" } + require(isValidHex(idHex)) { "$idHex is not a valid hex" } - val newObject = Note(idHex) - notes.putIfAbsent(idHex, newObject) ?: newObject - } + return notes.getOrCreate(idHex) { + Note(idHex) + } } fun checkGetOrCreateChannel(key: String): Channel? { @@ -288,11 +268,9 @@ object LocalCache { // we can't use naddr here because naddr might include relay info and // the preferred relay should not be part of the index. - return addressables[key.toTag()] - ?: run { - val newObject = AddressableNote(key) - addressables.putIfAbsent(key.toTag(), newObject) ?: newObject - } + return addressables.getOrCreate(key.toTag()) { + AddressableNote(key) + } } fun getOrCreateAddressableNote(key: ATag): AddressableNote { @@ -761,9 +739,6 @@ object LocalCache { if (version.event == null) { version.loadEvent(event, author, emptyList()) - if (version.liveSet != null) { - updateListCache() - } version.liveSet?.innerOts?.invalidateData() } @@ -1423,9 +1398,6 @@ object LocalCache { checkGetOrCreateNote(it)?.let { editedNote -> modificationCache.remove(editedNote.idHex) // must update list of Notes to quickly update the user. - if (editedNote.liveSet != null) { - updateListCache() - } editedNote.liveSet?.innerModifications?.invalidateData() } } @@ -1636,10 +1608,10 @@ object LocalCache { } } - return userListCache.filter { - (it.anyNameStartsWith(username)) || - it.pubkeyHex.startsWith(username, true) || - it.pubkeyNpub().startsWith(username, true) + return users.filter { _, user: User -> + (user.anyNameStartsWith(username)) || + user.pubkeyHex.startsWith(username, true) || + user.pubkeyNpub().startsWith(username, true) } } @@ -1655,39 +1627,39 @@ object LocalCache { } } - return noteListCache.filter { + return notes.filter { _, note -> ( - it.event !is GenericRepostEvent && - it.event !is RepostEvent && - it.event !is CommunityPostApprovalEvent && - it.event !is ReactionEvent && - it.event !is GiftWrapEvent && - it.event !is SealedGossipEvent && - it.event !is OtsEvent && - it.event !is LnZapEvent && - it.event !is LnZapRequestEvent + note.event !is GenericRepostEvent && + note.event !is RepostEvent && + note.event !is CommunityPostApprovalEvent && + note.event !is ReactionEvent && + note.event !is GiftWrapEvent && + note.event !is SealedGossipEvent && + note.event !is OtsEvent && + note.event !is LnZapEvent && + note.event !is LnZapRequestEvent ) && ( - it.event?.content()?.contains(text, true) + note.event?.content()?.contains(text, true) ?: false || - it.event?.matchTag1With(text) ?: false || - it.idHex.startsWith(text, true) || - it.idNote().startsWith(text, true) + note.event?.matchTag1With(text) ?: false || + note.idHex.startsWith(text, true) || + note.idNote().startsWith(text, true) ) } + - addressables.values.filter { + addressables.filter { _, addressable -> ( - it.event !is GenericRepostEvent && - it.event !is RepostEvent && - it.event !is CommunityPostApprovalEvent && - it.event !is ReactionEvent && - it.event !is GiftWrapEvent && - it.event !is LnZapEvent && - it.event !is LnZapRequestEvent + addressable.event !is GenericRepostEvent && + addressable.event !is RepostEvent && + addressable.event !is CommunityPostApprovalEvent && + addressable.event !is ReactionEvent && + addressable.event !is GiftWrapEvent && + addressable.event !is LnZapEvent && + addressable.event !is LnZapRequestEvent ) && ( - it.event?.content()?.contains(text, true) - ?: false || it.event?.matchTag1With(text) ?: false || it.idHex.startsWith(text, true) + addressable.event?.content()?.contains(text, true) + ?: false || addressable.event?.matchTag1With(text) ?: false || addressable.idHex.startsWith(text, true) ) } } @@ -1710,17 +1682,15 @@ object LocalCache { suspend fun findStatusesForUser(user: User): ImmutableList { checkNotInMainThread() - return addressables - .filter { - val noteEvent = it.value.event - ( - noteEvent is StatusEvent && - noteEvent.pubKey == user.pubkeyHex && - !noteEvent.isExpired() && - noteEvent.content.isNotBlank() - ) - } - .values + return addressables.filter { _, it -> + val noteEvent = it.event + ( + noteEvent is StatusEvent && + noteEvent.pubKey == user.pubkeyHex && + !noteEvent.isExpired() && + noteEvent.content.isNotBlank() + ) + } .sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex })) .reversed() .toImmutableList() @@ -1732,7 +1702,7 @@ object LocalCache { var minTime: Long? = null val time = TimeUtils.now() - noteListCache.forEach { item -> + notes.forEach { _, item -> val noteEvent = item.event if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) { noteEvent.verifiedTime?.let { stampedTime -> @@ -1764,7 +1734,7 @@ object LocalCache { val time = TimeUtils.now() val newNotes = - noteListCache.filter { item -> + notes.filter { _, item -> val noteEvent = item.event noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) @@ -1776,11 +1746,9 @@ object LocalCache { } fun cleanObservers() { - noteListCache.forEach { it.clearLive() } - - addressables.forEach { it.value.clearLive() } - - userListCache.forEach { it.clearLive() } + notes.forEach { _, it -> it.clearLive() } + addressables.forEach { _, it -> it.clearLive() } + users.forEach { _, it -> it.clearLive() } } fun pruneOldAndHiddenMessages(account: Account) { @@ -1806,8 +1774,8 @@ object LocalCache { } } - userListCache.forEach { userPair -> - userPair.privateChatrooms.values.map { + users.forEach { _, user -> + user.privateChatrooms.values.map { val toBeRemoved = it.pruneMessagesToTheLatestOnly() val childrenToBeRemoved = mutableListOf() @@ -1822,7 +1790,7 @@ object LocalCache { if (toBeRemoved.size > 1) { println( - "PRUNE: ${toBeRemoved.size} private messages with ${userPair.toBestDisplayName()} removed. ${it.roomMessages.size} kept", + "PRUNE: ${toBeRemoved.size} private messages with ${user.toBestDisplayName()} removed. ${it.roomMessages.size} kept", ) } } @@ -1831,21 +1799,20 @@ object LocalCache { fun prunePastVersionsOfReplaceables() { val toBeRemoved = - noteListCache - .filter { - val noteEvent = it.event - if (noteEvent is AddressableEvent) { - noteEvent.createdAt() < - (addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0) - } else { - false - } + notes.filter { _, note -> + val noteEvent = note.event + if (noteEvent is AddressableEvent) { + noteEvent.createdAt() < + (addressables.get(noteEvent.address().toTag())?.event?.createdAt() ?: 0) + } else { + false } + } val childrenToBeRemoved = mutableListOf() toBeRemoved.forEach { - val newerVersion = addressables[(it.event as? AddressableEvent)?.address()?.toTag()] + val newerVersion = (it.event as? AddressableEvent)?.address()?.toTag()?.let { tag -> addressables.get(tag) } if (newerVersion != null) { it.moveAllReferencesTo(newerVersion) } @@ -1865,23 +1832,22 @@ object LocalCache { checkNotInMainThread() val toBeRemoved = - noteListCache - .filter { - ( - (it.event is TextNoteEvent && !it.isNewThread()) || - it.event is ReactionEvent || - it.event is LnZapEvent || - it.event is LnZapRequestEvent || - it.event is ReportEvent || - it.event is GenericRepostEvent - ) && - it.replyTo?.any { it.liveSet?.isInUse() == true } != true && - it.liveSet?.isInUse() != true && // don't delete if observing. - it.author?.pubkeyHex !in - accounts && // don't delete if it is the logged in account - it.event?.isTaggedUsers(accounts) != - true // don't delete if it's a notification to the logged in user - } + notes.filter { _, note -> + ( + (note.event is TextNoteEvent && !note.isNewThread()) || + note.event is ReactionEvent || + note.event is LnZapEvent || + note.event is LnZapRequestEvent || + note.event is ReportEvent || + note.event is GenericRepostEvent + ) && + note.replyTo?.any { it.liveSet?.isInUse() == true } != true && + note.liveSet?.isInUse() != true && // don't delete if observing. + note.author?.pubkeyHex !in + accounts && // don't delete if it is the logged in account + note.event?.isTaggedUsers(accounts) != + true // don't delete if it's a notification to the logged in user + } val childrenToBeRemoved = mutableListOf() @@ -1948,7 +1914,7 @@ object LocalCache { checkNotInMainThread() val now = TimeUtils.now() - val toBeRemoved = noteListCache.filter { it.event?.isExpirationBefore(now) == true } + val toBeRemoved = notes.filter { _, it -> it.event?.isExpirationBefore(now) == true } val childrenToBeRemoved = mutableListOf() @@ -1973,11 +1939,7 @@ object LocalCache { account.liveHiddenUsers.value ?.hiddenUsers ?.map { userHex -> - ( - noteListCache.filter { it.event?.pubKey() == userHex } + - addressables.values.filter { it.event?.pubKey() == userHex } - ) - .toSet() + (notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet() } ?.flatten() ?: emptyList() @@ -1996,13 +1958,13 @@ object LocalCache { checkNotInMainThread() var removingContactList = 0 - userListCache.forEach { + users.forEach { _, user -> if ( - it.pubkeyHex !in loggedIn && - (it.liveSet == null || it.liveSet?.isInUse() == false) && - it.latestContactList != null + user.pubkeyHex !in loggedIn && + (user.liveSet == null || user.liveSet?.isInUse() == false) && + user.latestContactList != null ) { - it.latestContactList = null + user.latestContactList = null removingContactList++ } } @@ -2154,7 +2116,6 @@ class LocalCacheLiveData { fun invalidateData(newNote: Note) { bundler.invalidateList(newNote) { bundledNewNotes -> - LocalCache.updateListCache() _newEventBundles.emit(bundledNewNotes) } } 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 4b16bcdc5..bd7c4915a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -354,7 +354,7 @@ class User(val pubkeyHex: String) { } suspend fun transientFollowerCount(): Int { - return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } + return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } } fun cachedFollowingKeySet(): Set { @@ -378,7 +378,7 @@ class User(val pubkeyHex: String) { } suspend fun cachedFollowerCount(): Int { - return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } + return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } } fun hasSentMessagesTo(key: ChatroomKey?): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt index f9f4de701..107c6cb95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -45,7 +45,6 @@ class BookmarkPrivateFeedFilter(val account: Account) : FeedFilter() { return notes .plus(addresses) .toSet() - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() + .sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt index 351a7347d..37832131a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt @@ -40,7 +40,6 @@ class BookmarkPublicFeedFilter(val account: Account) : FeedFilter() { return notes .plus(addresses) .toSet() - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() + .sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index 0e69e94b5..a8a6ec236 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -44,6 +44,6 @@ class ChannelFeedFilter(val channel: Channel, val account: Account) : AdditiveFe } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 112c59f0f..8cbd4b1fd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -47,6 +47,6 @@ class ChatroomFeedFilter(val withUser: ChatroomKey, val account: Account) : } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 0ce003ce4..b78716deb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -197,6 +197,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index 2e20f0483..567132745 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -138,6 +138,6 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt index 767d31f51..4a30b7d77 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt @@ -24,16 +24,22 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent -class CommunityFeedFilter(val note: AddressableNote, val account: Account) : - AdditiveFeedFilter() { +class CommunityFeedFilter(val note: AddressableNote, val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { return account.userProfile().pubkeyHex + "-" + note.idHex } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val myPubKey = account.userProfile().pubkeyHex + val result = + LocalCache.notes.mapFlattenIntoSet { _, it -> + filterMap(it, myPubKey) + } + + return sort(result) } override fun applyFilter(collection: Set): Set { @@ -41,31 +47,36 @@ class CommunityFeedFilter(val note: AddressableNote, val account: Account) : } private fun innerApplyFilter(collection: Collection): Set { - val myUnapprovedPosts = - collection - .asSequence() - .filter { it.event is CommunityPostApprovalEvent } // Only Approvals - .filter { - it.author?.pubkeyHex == account.userProfile().pubkeyHex - } // made by the logged in user - .filter { it.event?.isTaggedAddressableNote(note.idHex) == true } // for this community - .filter { it.isNewThread() } // check if it is a new thread - .toSet() + val myPubKey = account.userProfile().pubkeyHex - val approvedPosts = - collection - .asSequence() - .filter { it.event is CommunityPostApprovalEvent } // Only Approvals - .filter { it.event?.isTaggedAddressableNote(note.idHex) == true } // Of the given community - .mapNotNull { it.replyTo } - .flatten() // get approved posts - .filter { it.isNewThread() } // check if it is a new thread - .toSet() + return collection.mapNotNull { + filterMap(it, myPubKey) + }.flatten().toSet() + } - return myUnapprovedPosts + approvedPosts + private fun filterMap( + note: Note, + myPubKey: HexKey, + ): List? { + return if ( + // Only Approvals + note.event is CommunityPostApprovalEvent && + // Of the given community + note.event?.isTaggedAddressableNote(this.note.idHex) == true + ) { + // if it is my post, bring on + if (note.author?.pubkeyHex == myPubKey && note.isNewThread()) { + listOf(note) + } else { + // brings the actual posts, not the approvals + note.replyTo?.filter { it.isNewThread() } + } + } else { + null + } } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt similarity index 50% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt index 7cb58d8b9..873f671e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt @@ -20,36 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS -import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.OnlineChecker -import com.vitorpamplona.quartz.events.LiveActivitiesEvent -import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE -class DiscoverLiveNowFeedFilter( - account: Account, -) : DiscoverLiveFeedFilter(account) { - override fun followList(): String { - // uses follows by default, but other lists if they were selected in the top bar - val currentList = super.followList() - return if (currentList == GLOBAL_FOLLOWS) { - KIND3_FOLLOWS - } else { - currentList - } - } - - override fun innerApplyFilter(collection: Collection): Set { - val allItems = super.innerApplyFilter(collection) - - val onlineOnly = - allItems.filter { - val noteEvent = it.event as? LiveActivitiesEvent - noteEvent?.status() == STATUS_LIVE && OnlineChecker.isOnline(noteEvent.streaming()) - } - - return onlineOnly.toSet() - } -} +val DefaultFeedOrder = compareBy({ it.createdAt() }, { it.idHex }).reversed() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 2d4d7302d..2a58fb1f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder @@ -29,7 +28,6 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.IsInPublicChatChannel import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { @@ -56,39 +54,34 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): Set { - val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + val params = buildFilterParams(account) - val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() - - val createEvents = collection.filter { it.event is ChannelCreateEvent } - val anyOtherChannelEvent = - collection - .asSequence() - .filter { it.event is IsInPublicChatChannel } - .mapNotNull { (it.event as? IsInPublicChatChannel)?.channel() } - .mapNotNull { LocalCache.checkGetOrCreateNote(it) } - .toSet() - - val activities = - (createEvents + anyOtherChannelEvent) - .asSequence() - // .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet. - .filter { - isGlobal || - it.author?.pubkeyHex in followingKeySet || - it.event?.isTaggedHashes(followingTagSet) == true || - it.event?.isTaggedGeoHashes(followingGeohashSet) == true + return collection.mapNotNullTo(HashSet()) { note -> + // note event here will never be null + val noteEvent = note.event + if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) { + note + } else if (noteEvent is IsInPublicChatChannel) { + val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) } + if (channel != null && (channel.event == null || params.match(channel.event))) { + channel + } else { + null } - .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } - .filter { (it.createdAt() ?: 0) <= now } - .toSet() - - return activities + } else { + null + } + } } override fun sort(collection: Set): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index e4e32df9b..163905fad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -21,15 +21,14 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { @@ -44,9 +43,27 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte } override fun feed(): List { - val allNotes = LocalCache.addressables.values + val filterParams = + FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultDiscoveryFollowList.value, + followLists = account.liveDiscoveryFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) - val notes = innerApplyFilter(allNotes) + // Here we only need to look for CommunityDefinition Events + val notes = + LocalCache.addressables.mapNotNullIntoSet { key, note -> + val noteEvent = note.event + if (noteEvent == null && shouldInclude(ATag.parseAtagUnckecked(key), filterParams)) { + // send unloaded communities to the screen + note + } else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) { + note + } else { + null + } + } return sort(notes) } @@ -56,41 +73,44 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte } protected open fun innerApplyFilter(collection: Collection): Set { - val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + // here, we need to look for CommunityDefinition in new collection AND new CommunityDefinition from Post Approvals + val filterParams = + FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultDiscoveryFollowList.value, + followLists = account.liveDiscoveryFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) - val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() + return collection.mapNotNull { note -> + // note event here will never be null + val noteEvent = note.event + if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) { + listOf(note) + } else if (noteEvent is CommunityPostApprovalEvent) { + noteEvent.communities().mapNotNull { + val definitionNote = LocalCache.getOrCreateAddressableNote(it) + val definitionEvent = definitionNote.event - val createEvents = collection.filter { it.event is CommunityDefinitionEvent } - val anyOtherCommunityEvent = - collection - .asSequence() - .filter { it.event is CommunityPostApprovalEvent } - .mapNotNull { (it.event as? CommunityPostApprovalEvent)?.communities() } - .flatten() - .map { LocalCache.getOrCreateAddressableNote(it) } - .toSet() - - val activities = - (createEvents + anyOtherCommunityEvent) - .asSequence() - .filter { it.event is CommunityDefinitionEvent } - .filter { - isGlobal || - it.author?.pubkeyHex in followingKeySet || - it.event?.isTaggedHashes(followingTagSet) == true || - it.event?.isTaggedGeoHashes(followingGeohashSet) == true + if (definitionEvent == null && shouldInclude(it, filterParams)) { + definitionNote + } else if (definitionEvent is CommunityDefinitionEvent && filterParams.match(definitionEvent)) { + definitionNote + } else { + null + } } - .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } - .filter { (it.createdAt() ?: 0) <= now } - .toSet() - - return activities + } else { + null + } + }.flatten().toSet() } + private fun shouldInclude( + aTag: ATag?, + params: FilterByListParams, + ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag) + override fun sort(collection: Set): List { val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 0b8fc858c..6fc737bb8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder @@ -31,7 +30,6 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverLiveFeedFilter( val account: Account, @@ -64,33 +62,15 @@ open class DiscoverLiveFeedFilter( } protected open fun innerApplyFilter(collection: Collection): Set { - val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + val filterParams = + FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultDiscoveryFollowList.value, + followLists = account.liveDiscoveryFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) - val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() - - val activities = - collection - .asSequence() - .filter { it.event is LiveActivitiesEvent } - .filter { - isGlobal || - (it.event as LiveActivitiesEvent).participantsIntersect(followingKeySet) || - it.event?.isTaggedHashes( - followingTagSet, - ) == true || - it.event?.isTaggedGeoHashes( - followingGeohashSet, - ) == true - } - .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } - .filter { (it.createdAt() ?: 0) <= now } - .toSet() - - return activities + return collection.filterTo(HashSet()) { it.event is LiveActivitiesEvent && filterParams.match(it.event) } } override fun sort(collection: Set): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt index 1f59fd1b9..0be87e5b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt @@ -21,13 +21,11 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverMarketplaceFeedFilter( val account: Account, @@ -46,10 +44,13 @@ open class DiscoverMarketplaceFeedFilter( } override fun feed(): List { - val classifieds = - LocalCache.addressables.filter { it.value.event is ClassifiedsEvent }.map { it.value } + val params = buildFilterParams(account) - val notes = innerApplyFilter(classifieds) + val notes = + LocalCache.addressables.filterIntoSet { _, it -> + val noteEvent = it.event + noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent) + } return sort(notes) } @@ -58,35 +59,22 @@ open class DiscoverMarketplaceFeedFilter( return innerApplyFilter(collection) } + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + account.userProfile().pubkeyHex, + account.defaultDiscoveryFollowList.value, + account.liveDiscoveryFollowLists.value, + account.flowHiddenUsers.value, + ) + } + protected open fun innerApplyFilter(collection: Collection): Set { - val now = TimeUtils.now() - val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + val params = buildFilterParams(account) - val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet() - - val activities = - collection - .asSequence() - .filter { - it.event is ClassifiedsEvent && - it.event?.hasTagWithContent("image") == true && - it.event?.hasTagWithContent("price") == true && - it.event?.hasTagWithContent("title") == true - } - .filter { - isGlobal || - it.author?.pubkeyHex in followingKeySet || - it.event?.isTaggedHashes(followingTagSet) == true || - it.event?.isTaggedGeoHashes(followingGeohashSet) == true - } - .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } - .filter { (it.createdAt() ?: 0) <= now } - .toSet() - - return activities + return collection.filterTo(HashSet()) { + val noteEvent = it.event + noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent) + } } override fun sort(collection: Set): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt new file mode 100644 index 000000000..d920ca7ce --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventInterface +import com.vitorpamplona.quartz.events.LiveActivitiesEvent +import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.PeopleListEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +class FilterByListParams( + val isGlobal: Boolean, + val isHiddenList: Boolean, + val followLists: Account.LiveFollowLists?, + val hiddenLists: Account.LiveHiddenUsers, + val now: Long = TimeUtils.now(), +) { + fun isNotHidden(userHex: String) = !(hiddenLists.hiddenUsers.contains(userHex) || hiddenLists.spammers.contains(userHex)) + + fun isNotInTheFuture(noteEvent: Event) = noteEvent.createdAt <= now + + fun isEventInList(noteEvent: Event): Boolean { + if (followLists == null) return false + + return if (noteEvent is LiveActivitiesEvent) { + noteEvent.participantsIntersect(followLists.users) || + noteEvent.isTaggedHashes(followLists.hashtags) || + noteEvent.isTaggedGeoHashes(followLists.users) || + noteEvent.isTaggedAddressableNotes(followLists.communities) + } else { + noteEvent.pubKey in followLists.users || + noteEvent.isTaggedHashes(followLists.hashtags) || + noteEvent.isTaggedGeoHashes(followLists.users) || + noteEvent.isTaggedAddressableNotes(followLists.communities) + } + } + + fun isATagInList(aTag: ATag): Boolean { + if (followLists == null) return false + + return aTag.pubKeyHex in followLists.users + } + + fun match( + noteEvent: EventInterface?, + isGlobalRelay: Boolean = true, + ) = if (noteEvent is Event) match(noteEvent, isGlobalRelay) else false + + fun match( + noteEvent: Event, + isGlobalRelay: Boolean = true, + ) = ((isGlobal && isGlobalRelay) || isEventInList(noteEvent)) && + (isHiddenList || isNotHidden(noteEvent.pubKey)) && + isNotInTheFuture(noteEvent) + + fun match(aTag: ATag?) = + aTag != null && + (isGlobal || isATagInList(aTag)) && + (isHiddenList || isNotHidden(aTag.pubKeyHex)) + + companion object { + fun showHiddenKey( + selectedListName: String, + userHex: String, + ) = selectedListName == PeopleListEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex) + + fun create( + userHex: String, + selectedListName: String, + followLists: Account.LiveFollowLists?, + hiddenUsers: Account.LiveHiddenUsers, + ): FilterByListParams { + return FilterByListParams( + isGlobal = selectedListName == GLOBAL_FOLLOWS, + isHiddenList = showHiddenKey(selectedListName, userHex), + followLists = followLists, + hiddenLists = hiddenUsers, + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index 65d249e5a..a80e2d0be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -36,7 +36,12 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val notes = + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it, tag) + } + + return sort(notes) } override fun applyFilter(collection: Set): Set { @@ -44,25 +49,24 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } private fun innerApplyFilter(collection: Collection): Set { - val myTag = tag ?: return emptySet() + return collection.filterTo(HashSet()) { acceptableEvent(it, tag) } + } - return collection - .asSequence() - .filter { - ( - it.event is TextNoteEvent || - it.event is LongTextNoteEvent || - it.event is ChannelMessageEvent || - it.event is PrivateDmEvent || - it.event is PollNoteEvent || - it.event is AudioHeaderEvent - ) && it.event?.isTaggedGeoHash(myTag) == true - } - .filter { account.isAcceptable(it) } - .toSet() + fun acceptableEvent( + it: Note, + geoTag: String, + ): Boolean { + return ( + it.event is TextNoteEvent || + it.event is LongTextNoteEvent || + it.event is ChannelMessageEvent || + it.event is PrivateDmEvent || + it.event is PollNoteEvent || + it.event is AudioHeaderEvent + ) && it.event?.isTaggedGeoHash(geoTag) == true && account.isAcceptable(it) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 4eaa3778f..125a4c1c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -36,7 +36,12 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val notes = + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it, tag) + } + + return sort(notes) } override fun applyFilter(collection: Set): Set { @@ -44,25 +49,24 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } private fun innerApplyFilter(collection: Collection): Set { - val myTag = tag ?: return emptySet() + return collection.filterTo(HashSet()) { acceptableEvent(it, tag) } + } - return collection - .asSequence() - .filter { - ( - it.event is TextNoteEvent || - it.event is LongTextNoteEvent || - it.event is ChannelMessageEvent || - it.event is PrivateDmEvent || - it.event is PollNoteEvent || - it.event is AudioHeaderEvent - ) && it.event?.isTaggedHash(myTag) == true - } - .filter { account.isAcceptable(it) } - .toSet() + fun acceptableEvent( + it: Note, + hashTag: String, + ): Boolean { + return ( + it.event is TextNoteEvent || + it.event is LongTextNoteEvent || + it.event is ChannelMessageEvent || + it.event is PrivateDmEvent || + it.event is PollNoteEvent || + it.event is AudioHeaderEvent + ) && it.event?.isTaggedHash(hashTag) == true && account.isAcceptable(it) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } 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 15d977db7..c5c2981fb 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 @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.ChannelMessageEvent @@ -30,7 +29,6 @@ import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { @@ -38,55 +36,54 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val filterParams = buildFilterParams(account) + + return sort( + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it, filterParams) + }, + ) } override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultHomeFollowList.value, + followLists = account.liveHomeFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) + } + private fun innerApplyFilter(collection: Collection): Set { - val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + val filterParams = buildFilterParams(account) - val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet() + return collection.filterTo(HashSet()) { + acceptableEvent(it, filterParams) + } + } - val now = TimeUtils.now() - - return collection - .asSequence() - .filter { - ( - it.event is TextNoteEvent || - it.event is PollNoteEvent || - it.event is ChannelMessageEvent || - it.event is LiveActivitiesChatMessageEvent - ) && - ( - isGlobal || - it.author?.pubkeyHex in followingKeySet || - it.event?.isTaggedHashes(followingTagSet) ?: false || - it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false - ) && - // && account.isAcceptable(it) // This filter follows only. No need to check if - // acceptable - (isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) && - ((it.event?.createdAt() ?: 0) < now) && - !it.isNewThread() - } - .toSet() + fun acceptableEvent( + it: Note, + filterParams: FilterByListParams, + ): Boolean { + return ( + it.event is TextNoteEvent || + it.event is PollNoteEvent || + it.event is ChannelMessageEvent || + it.event is LiveActivitiesChatMessageEvent + ) && filterParams.match(it.event) && !it.isNewThread() } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } 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 7218a8e72..98a556bc9 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 @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.AudioHeaderEvent @@ -35,7 +34,6 @@ import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { @@ -43,69 +41,70 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun showHiddenKey(): Boolean { - return account.defaultHomeFollowList.value == - PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || - account.defaultHomeFollowList.value == - MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + } + + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultHomeFollowList.value, + followLists = account.liveHomeFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.noteListCache, true) - val longFormNotes = innerApplyFilter(LocalCache.addressables.values, false) + val gRelays = account.activeGlobalRelays().toSet() + val filterParams = buildFilterParams(account) + + val notes = + LocalCache.notes.filterIntoSet { _, note -> + // Avoids processing addressables twice. + (note.event?.kind() ?: 99999) < 10000 && acceptableEvent(note, gRelays, filterParams) + } + + val longFormNotes = + LocalCache.addressables.filterIntoSet { _, note -> + acceptableEvent(note, gRelays, filterParams) + } return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): Set { - return innerApplyFilter(collection, false) + return innerApplyFilter(collection) } - private fun innerApplyFilter( - collection: Collection, - ignoreAddressables: Boolean, - ): Set { - val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS - val gRelays = account.activeGlobalRelays() - val isHiddenList = showHiddenKey() + private fun innerApplyFilter(collection: Collection): Set { + val gRelays = account.activeGlobalRelays().toSet() + val filterParams = buildFilterParams(account) - val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet() - val followingCommunities = account.liveHomeFollowLists.value?.communities ?: emptySet() + return collection.filterTo(HashSet()) { + acceptableEvent(it, gRelays, filterParams) + } + } - val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future. - - return collection - .asSequence() - .filter { it -> - val noteEvent = it.event - val isGlobalRelay = it.relays.any { gRelays.contains(it.url) } - ( - noteEvent is TextNoteEvent || - noteEvent is ClassifiedsEvent || - noteEvent is RepostEvent || - noteEvent is GenericRepostEvent || - noteEvent is LongTextNoteEvent || - noteEvent is PollNoteEvent || - noteEvent is HighlightEvent || - noteEvent is AudioTrackEvent || - noteEvent is AudioHeaderEvent - ) && - (!ignoreAddressables || noteEvent.kind() < 10000) && - ( - (isGlobal && isGlobalRelay) || - it.author?.pubkeyHex in followingKeySet || - noteEvent.isTaggedHashes(followingTagSet) || - noteEvent.isTaggedGeoHashes(followingGeohashSet) || - noteEvent.isTaggedAddressableNotes(followingCommunities) - ) && - // && account.isAcceptable(it) // This filter follows only. No need to check if - // acceptable - (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && - ((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) && - it.isNewThread() - } - .toSet() + fun acceptableEvent( + it: Note, + globalRelays: Set, + filterParams: FilterByListParams, + ): Boolean { + val noteEvent = it.event + val isGlobalRelay = it.relays.any { globalRelays.contains(it.url) } + return ( + noteEvent is TextNoteEvent || + noteEvent is ClassifiedsEvent || + noteEvent is RepostEvent || + noteEvent is GenericRepostEvent || + noteEvent is LongTextNoteEvent || + noteEvent is PollNoteEvent || + noteEvent is HighlightEvent || + noteEvent is AudioTrackEvent || + noteEvent is AudioHeaderEvent + ) && + filterParams.match(noteEvent, isGlobalRelay) && + it.isNewThread() } override fun sort(collection: Set): List { @@ -115,6 +114,6 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } else { it.idHex } - }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + }.sortedWith(DefaultFeedOrder) } } 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 5e49b616e..6770a48ed 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 @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.encoders.HexKey @@ -35,7 +34,6 @@ import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GitIssueEvent import com.vitorpamplona.quartz.events.GitPatchEvent import com.vitorpamplona.quartz.events.HighlightEvent -import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -54,8 +52,24 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultNotificationFollowList.value, + followLists = account.liveNotificationFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) + } + override fun feed(): List { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val filterParams = buildFilterParams(account) + + val notifications = + LocalCache.notes.filterIntoSet { _, note -> + acceptableEvent(note, filterParams) + } + + return sort(notifications) } override fun applyFilter(collection: Set): Set { @@ -63,32 +77,31 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() } private fun innerApplyFilter(collection: Collection): Set { - val isGlobal = account.defaultNotificationFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = showHiddenKey() + val filterParams = buildFilterParams(account) - val followingKeySet = account.liveNotificationFollowLists.value?.users ?: emptySet() + return collection.filterTo(HashSet()) { acceptableEvent(it, filterParams) } + } - val loggedInUser = account.userProfile() - val loggedInUserHex = loggedInUser.pubkeyHex + fun acceptableEvent( + it: Note, + filterParams: FilterByListParams, + ): Boolean { + val loggedInUserHex = account.userProfile().pubkeyHex - return collection - .filterTo(HashSet()) { - it.event !is ChannelCreateEvent && - it.event !is ChannelMetadataEvent && - it.event !is LnZapRequestEvent && - it.event !is BadgeDefinitionEvent && - it.event !is BadgeProfilesEvent && - it.event !is GiftWrapEvent && - (it.event is LnZapEvent || it.author !== loggedInUser) && - (isGlobal || it.author?.pubkeyHex in followingKeySet) && - it.event?.isTaggedUser(loggedInUserHex) ?: false && - (isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && - tagsAnEventByUser(it, loggedInUserHex) - } + return it.event !is ChannelCreateEvent && + it.event !is ChannelMetadataEvent && + it.event !is LnZapRequestEvent && + it.event !is BadgeDefinitionEvent && + it.event !is BadgeProfilesEvent && + it.event !is GiftWrapEvent && + (filterParams.isGlobal || filterParams.followLists?.users?.contains(it.author?.pubkeyHex) == true) && + it.event?.isTaggedUser(loggedInUserHex) ?: false && + (filterParams.isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && + tagsAnEventByUser(it, loggedInUserHex) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } fun tagsAnEventByUser( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt index 44c93d7a5..332281ce8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt @@ -31,7 +31,12 @@ class UserProfileAppRecommendationsFeedFilter(val user: User) : AdditiveFeedFilt } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.addressables.values)) + val recommendations = + LocalCache.addressables.mapFlattenIntoSet { _, note -> + filterMap(note) + } + + return sort(recommendations) } override fun applyFilter(collection: Set): Set { @@ -39,26 +44,21 @@ class UserProfileAppRecommendationsFeedFilter(val user: User) : AdditiveFeedFilt } private fun innerApplyFilter(collection: Collection): Set { - val recommendations = - collection - .asSequence() - .filter { it.event is AppRecommendationEvent } - .mapNotNull { - val noteEvent = it.event as? AppRecommendationEvent - if (noteEvent != null && noteEvent.pubKey == user.pubkeyHex) { - noteEvent.recommendations() - } else { - null - } - } - .flatten() - .map { LocalCache.getOrCreateAddressableNote(it) } - .toSet() + return collection.mapNotNull { filterMap(it) }.flatten().toSet() + } - return recommendations + fun filterMap(it: Note): List? { + val noteEvent = it.event + if (noteEvent is AppRecommendationEvent) { + if (noteEvent.pubKey == user.pubkeyHex) { + return noteEvent.recommendations().map { LocalCache.getOrCreateAddressableNote(it) } + } + } + + return null } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt index 075d8a4be..3ef42a225 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt @@ -47,7 +47,6 @@ class UserProfileBookmarksFeedFilter(val user: User, val account: Account) : Fee return (notes + addresses) .filter { account.isAcceptable(it) } - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() + .sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt index 81953dd82..0af8e0d6a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt @@ -36,7 +36,17 @@ class UserProfileConversationsFeedFilter(val user: User, val account: Account) : } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.noteListCache)) + val notes = + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it) + } + + val longFormNotes = + LocalCache.addressables.filterIntoSet { _, it -> + acceptableEvent(it) + } + + return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): Set { @@ -44,23 +54,21 @@ class UserProfileConversationsFeedFilter(val user: User, val account: Account) : } private fun innerApplyFilter(collection: Collection): Set { - return collection - .filter { - it.author == user && - ( - it.event is TextNoteEvent || - it.event is PollNoteEvent || - it.event is ChannelMessageEvent || - it.event is LiveActivitiesChatMessageEvent - ) && - !it.isNewThread() && - account.isAcceptable(it) == true - } - .toSet() + return collection.filterTo(HashSet()) { acceptableEvent(it) } + } + + fun acceptableEvent(it: Note): Boolean { + return it.author == user && + ( + it.event is TextNoteEvent || + it.event is PollNoteEvent || + it.event is ChannelMessageEvent || + it.event is LiveActivitiesChatMessageEvent + ) && !it.isNewThread() && account.isAcceptable(it) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } override fun limit() = 200 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 616be35e7..ddd9e1436 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 @@ -30,7 +30,9 @@ class UserProfileFollowersFeedFilter(val user: User, val account: Account) : Fee } override fun feed(): List { - return LocalCache.userListCache.filter { it.isFollowing(user) && !account.isHidden(it) } + return LocalCache.users.filter { _, it -> + it.isFollowing(user) && !account.isHidden(it) + } } override fun limit() = 400 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index 9acff0d9b..63923c457 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -41,8 +41,15 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.noteListCache) - val longFormNotes = innerApplyFilter(LocalCache.addressables.values) + val notes = + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it) + } + + val longFormNotes = + LocalCache.addressables.filterIntoSet { _, it -> + acceptableEvent(it) + } return sort(notes + longFormNotes) } @@ -52,28 +59,26 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : } private fun innerApplyFilter(collection: Collection): Set { - return collection - .filter { - it.author == user && - ( - it.event is TextNoteEvent || - it.event is ClassifiedsEvent || - it.event is RepostEvent || - it.event is GenericRepostEvent || - it.event is LongTextNoteEvent || - it.event is PollNoteEvent || - it.event is HighlightEvent || - it.event is AudioTrackEvent || - it.event is AudioHeaderEvent - ) && - it.isNewThread() && - account.isAcceptable(it) == true - } - .toSet() + return collection.filterTo(HashSet()) { acceptableEvent(it) } + } + + fun acceptableEvent(it: Note): Boolean { + return it.author == user && + ( + it.event is TextNoteEvent || + it.event is ClassifiedsEvent || + it.event is RepostEvent || + it.event is GenericRepostEvent || + it.event is LongTextNoteEvent || + it.event is PollNoteEvent || + it.event is HighlightEvent || + it.event is AudioTrackEvent || + it.event is AudioHeaderEvent + ) && it.isNewThread() && account.isAcceptable(it) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } override fun limit() = 200 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt index 96b718549..4eeffd942 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt @@ -44,7 +44,7 @@ class UserProfileReportsFeedFilter(val user: User) : AdditiveFeedFilter() } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } override fun limit() = 400 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 17f3dc24e..c96db1dec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -21,14 +21,12 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import com.vitorpamplona.quartz.utils.TimeUtils class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { override fun feedKey(): String { @@ -36,14 +34,17 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { } override fun showHiddenKey(): Boolean { - return account.defaultStoriesFollowList.value == - PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || - account.defaultStoriesFollowList.value == - MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.noteListCache) + val params = buildFilterParams(account) + + val notes = + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it, params) + } return sort(notes) } @@ -53,36 +54,32 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { } private fun innerApplyFilter(collection: Collection): Set { - val now = TimeUtils.now() - val isGlobal = account.defaultStoriesFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = - account.defaultStoriesFollowList.value == - PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || - account.defaultStoriesFollowList.value == - MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + val params = buildFilterParams(account) - val followingKeySet = account.liveStoriesFollowLists.value?.users ?: emptySet() - val followingTagSet = account.liveStoriesFollowLists.value?.hashtags ?: emptySet() - val followingGeohashSet = account.liveStoriesFollowLists.value?.geotags ?: emptySet() + return collection.filterTo(HashSet()) { acceptableEvent(it, params) } + } - return collection - .asSequence() - .filter { - (it.event is FileHeaderEvent && (it.event as FileHeaderEvent).hasUrl()) || - it.event is FileStorageHeaderEvent - } - .filter { - isGlobal || - it.author?.pubkeyHex in followingKeySet || - (it.event?.isTaggedHashes(followingTagSet) ?: false) || - (it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) - } - .filter { isHiddenList || account.isAcceptable(it) } - .filter { it.createdAt()!! <= now } - .toSet() + fun acceptableEvent( + it: Note, + params: FilterByListParams, + ): Boolean { + val noteEvent = it.event + + return ((noteEvent is FileHeaderEvent && noteEvent.hasUrl()) || noteEvent is FileStorageHeaderEvent) && + params.match(noteEvent) && + account.isAcceptable(it) + } + + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultStoriesFollowList.value, + followLists = account.liveStoriesFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.sortedWith(DefaultFeedOrder) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 68ed29da2..3581b6652 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -655,15 +655,15 @@ class FollowListViewModel(val account: Account) : ViewModel() { val newFollowLists = LocalCache.addressables - .mapNotNull { - val event = (it.value.event as? PeopleListEvent) + .mapNotNull { _, addressableNote -> + val event = (addressableNote.event as? PeopleListEvent) // Has to have an list if ( event != null && event.pubKey == account.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50) ) { - CodeName(event.address().toTag(), PeopleListName(it.value), CodeNameType.PEOPLE_LIST) + CodeName(event.address().toTag(), PeopleListName(addressableNote), CodeNameType.PEOPLE_LIST) } else { null } @@ -974,44 +974,44 @@ fun debugState(context: Context) { Log.d( "STATE DUMP", "Notes: " + - LocalCache.noteListCache.filter { it.liveSet != null }.size + + LocalCache.notes.filter { _, it -> it.liveSet != null }.size + " / " + - LocalCache.noteListCache.filter { it.event != null }.size + + LocalCache.notes.filter { _, it -> it.event != null }.size + " / " + - LocalCache.noteListCache.size, + LocalCache.notes.size(), ) Log.d( "STATE DUMP", "Addressables: " + - LocalCache.addressables.filter { it.value.liveSet != null }.size + + LocalCache.addressables.filter { _, it -> it.liveSet != null }.size + " / " + - LocalCache.addressables.filter { it.value.event != null }.size + + LocalCache.addressables.filter { _, it -> it.event != null }.size + " / " + - LocalCache.addressables.size, + LocalCache.addressables.size(), ) Log.d( "STATE DUMP", "Users: " + - LocalCache.userListCache.filter { it.liveSet != null }.size + + LocalCache.users.filter { _, it -> it.liveSet != null }.size + " / " + - LocalCache.userListCache.filter { it.latestMetadata != null }.size + + LocalCache.users.filter { _, it -> it.latestMetadata != null }.size + " / " + - LocalCache.userListCache.size, + LocalCache.users.size(), ) Log.d( "STATE DUMP", "Memory used by Events: " + - LocalCache.noteListCache.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + + LocalCache.notes.sumOfLong { _, note -> note.event?.countMemory() ?: 0L } / (1024 * 1024) + " MB", ) - LocalCache.noteListCache - .groupBy { it.event?.kind() } - .forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") } - LocalCache.addressables.values - .groupBy { it.event?.kind() } - .forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") } + LocalCache.notes + .countByGroup { _, it -> it.event?.kind() } + .forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value} elements ") } + LocalCache.addressables + .countByGroup { _, it -> it.event?.kind() } + .forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value} elements ") } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 8ad6a0d5b..285a3989f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter -import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveNowFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.theme.Size20dp @@ -46,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size23dp import com.vitorpamplona.amethyst.ui.theme.Size24dp import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.quartz.events.ChatroomKeyable -import com.vitorpamplona.quartz.events.LiveActivitiesEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -309,30 +307,6 @@ object HomeLatestItem : LatestItem() { } } -object DiscoverLatestItem : LatestItem() { - fun hasNewItems( - account: Account, - newNotes: Set, - ): Boolean { - checkNotInMainThread() - - val lastTime = account.loadLastRead(Route.Discover.base + "Live") - - val newestItem = updateNewestItem(newNotes, account, DiscoverLiveNowFeedFilter(account)) - - val noteEvent = newestItem?.event - - val dateToUse = - if (noteEvent is LiveActivitiesEvent) { - noteEvent.starts() ?: newestItem.createdAt() - } else { - newestItem?.createdAt() - } - - return (dateToUse ?: 0) > lastTime - } -} - object NotificationLatestItem : LatestItem() { fun hasNewItems( account: Account, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index 0d396edf5..8e9692177 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -232,7 +232,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { val replies = mutableMapOf() val takenIntoAccount = mutableSetOf() - LocalCache.noteListCache.forEach { + LocalCache.notes.forEach { _, it -> val noteEvent = it.event if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { if (noteEvent is ReactionEvent) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 88ec1d47b..decf7c3bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -911,7 +911,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun getAddressableNoteIfExists(key: String): AddressableNote? { - return LocalCache.addressables[key] + return LocalCache.getAddressableNoteIfExists(key) } suspend fun findStatusesForUser(