diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index c3ff33225..44eecf7fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -117,7 +117,7 @@ object ServiceManager { LocalCache.pruneOldAndHiddenMessages(it) LocalCache.pruneHiddenMessages(it) LocalCache.pruneContactLists(it) - // LocalCache.pruneNonFollows(it) + LocalCache.pruneRepliesAndReactions(it) } } } 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 22e59b520..2333df424 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1272,6 +1272,10 @@ object LocalCache { it.value.clearLive() } + addressables.forEach { + it.value.clearLive() + } + users.forEach { it.value.clearLive() } @@ -1283,6 +1287,8 @@ object LocalCache { channels.forEach { it -> val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) + val childrenToBeRemoved = mutableListOf() + toBeRemoved.forEach { notes.remove(it.idHex) // Doesn't need to clean up the replies and mentions.. Too small to matter. @@ -1291,17 +1297,82 @@ object LocalCache { it.replyTo?.forEach { _ -> it.removeReply(it) } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) } + removeChildrenOf(childrenToBeRemoved) + if (toBeRemoved.size > 100 || it.value.notes.size > 100) { println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept") } } + + users.forEach { userPair -> + userPair.value.privateChatrooms.values.map { + val toBeRemoved = it.pruneMessagesToTheLatestOnly() + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + notes.remove(it.idHex) + + // Counts the replies + it.replyTo?.forEach { _ -> + it.removeReply(it) + } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + removeChildrenOf(childrenToBeRemoved) + + if (toBeRemoved.size > 1) { + println("PRUNE: ${toBeRemoved.size} private messages with ${userPair.value.toBestDisplayName()} removed. ${it.roomMessages.size} kept") + } + } + } + } + + fun pruneRepliesAndReactions(account: Account) { + checkNotInMainThread() + val user = account.userProfile() + + val toBeRemoved = notes.filter { + ( (it.value.event is TextNoteEvent && !it.value.isNewThread()) + || it.value.event is ReactionEvent || it.value.event is LnZapEvent || it.value.event is LnZapRequestEvent + || it.value.event is ReportEvent || it.value.event is GenericRepostEvent) + && it.value.liveSet?.isInUse() != true // don't delete if observing. + && it.value.author != user // don't delete if it is the logged in account + && it.value.event?.isTaggedUser(user.pubkeyHex) != true // don't delete if it's a notification to the logged in user + }.values + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + notes.remove(it.idHex) + + it.replyTo?.forEach { masterNote -> + masterNote.removeReply(it) + masterNote.removeBoost(it) + masterNote.removeReaction(it) + masterNote.removeZap(it) + it.clearEOSE() // allows reloading of these events + } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + if (toBeRemoved.size > 1) { + println("PRUNE: ${toBeRemoved.size} thread replies removed.") + } } fun pruneHiddenMessages(account: Account) { checkNotInMainThread() + val childrenToBeRemoved = mutableListOf() + val toBeRemoved = account.hiddenUsers.map { userHex -> ( notes.values.filter { @@ -1323,11 +1394,30 @@ object LocalCache { } notes.remove(it.idHex) + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) } + removeChildrenOf(childrenToBeRemoved) + println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") } + fun removeChildrenOf(nextToBeRemoved: List) { + nextToBeRemoved.forEach { note -> + if (note.event is LnZapEvent) { + (note.event as LnZapEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) } + } + if (note.event is LnZapRequestEvent) { + (note.event as LnZapRequestEvent).zappedAuthor().mapNotNull { getUserIfExists(it)?.removeZap(note) } + } + if (note.event is ReportEvent) { + (note.event as ReportEvent).reportedAuthor().mapNotNull { getUserIfExists(it.key)?.removeZap(note) } + } + notes.remove(note.idHex) + } + } + fun pruneContactLists(userAccount: Account) { checkNotInMainThread() 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 5448367b5..d09483d37 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -158,6 +158,35 @@ open class Note(val idHex: String) { boosts = boosts - note liveSet?.boosts?.invalidateData() } + + fun removeAllChildNotes(): Set { + val toBeRemoved = replies + + reactions.values.flatten() + + boosts + + reports.values.flatten() + + zaps.keys + + zaps.values.filterNotNull() + + zapPayments.keys + + zapPayments.values.filterNotNull() + + replies = setOf() + reactions = mapOf>() + boosts = setOf() + reports = mapOf>() + zaps = mapOf() + zapPayments = mapOf() + relays = setOf() + lastReactionsDownloadTime = emptyMap() + + liveSet?.replies?.invalidateData() + liveSet?.reactions?.invalidateData() + liveSet?.boosts?.invalidateData() + liveSet?.reports?.invalidateData() + liveSet?.zaps?.invalidateData() + + return toBeRemoved + } + fun removeReaction(note: Note) { val tags = note.event?.tags() ?: emptyList() val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+" @@ -554,6 +583,10 @@ open class Note(val idHex: String) { zaps = emptyMap() } + fun clearEOSE() { + lastReactionsDownloadTime = emptyMap() + } + var liveSet: NoteLiveSet? = null fun live(): NoteLiveSet { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt index 348a46837..18a763647 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt @@ -1,13 +1,17 @@ package com.vitorpamplona.amethyst.model object TimeUtils { - const val fiveMinutes = 60 * 5 - const val oneHour = 60 * 60 - const val oneDay = 24 * 60 * 60 + const val oneMinute = 60 + const val fiveMinutes = 5 * oneMinute + const val oneHour = 60 * oneMinute + const val eightHours = 8 * oneHour + const val oneDay = 24 * oneHour + const val oneWeek = 7 * oneDay fun now() = System.currentTimeMillis() / 1000 fun fiveMinutesAgo() = now() - fiveMinutes fun oneHourAgo() = now() - oneHour fun oneDayAgo() = now() - oneDay - fun eightHoursAgo() = now() - (oneHour * 8) + fun eightHoursAgo() = now() - eightHours + fun oneWeekAgo() = now() - oneWeek } 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 e8e4dc59a..cec48cd70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -137,6 +137,16 @@ class User(val pubkeyHex: String) { } } + fun removeZap(zapRequestOrZapEvent: Note) { + if (zapRequestOrZapEvent in zaps.keys) { + zaps = zaps.minus(zapRequestOrZapEvent) + liveSet?.zaps?.invalidateData() + } else if (zapRequestOrZapEvent in zaps.values) { + zaps = zaps.filter { it.value != zapRequestOrZapEvent } + liveSet?.zaps?.invalidateData() + } + } + fun zappedAmount(): BigDecimal { return zaps.mapNotNull { it.value?.event } .filterIsInstance() @@ -383,6 +393,22 @@ class Chatroom() { roomMessages = roomMessages + msg } } + + fun pruneMessagesToTheLatestOnly(): Set { + val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + + val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) { + // Recent messages, keep last 100 + sorted.take(100).toSet() + } else { + // Old messages, keep the last one. + sorted.take(1).toSet() + } + sorted.filter { it.liveSet?.isInUse() ?: false } + + val toRemove = roomMessages.minus(toKeep) + roomMessages = toKeep + return toRemove + } } @Stable