From d7790cd31e26d378c0ca931d964c67be29ee4289 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 24 May 2024 18:11:57 -0400 Subject: [PATCH] Moves the thread formatter and calculator out of Note --- .../com/vitorpamplona/amethyst/model/Note.kt | 95 ------------- .../amethyst/model/ThreadLevelCalculator.kt | 125 ++++++++++++++++++ .../amethyst/ui/dal/ThreadFeedFilter.kt | 21 +-- .../amethyst/ui/screen/ThreadFeedView.kt | 5 +- 4 files changed, 139 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt 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 291209b76..f7de6ef50 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -72,9 +72,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import java.math.BigDecimal -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter import kotlin.coroutines.resume @Stable @@ -211,98 +208,6 @@ open class Note(val idHex: String) { } } - val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss") - - fun formattedDateTime(timestamp: Long): String { - return Instant.ofEpochSecond(timestamp) - .atZone(ZoneId.systemDefault()) - .format(levelFormatter) - } - - data class LevelSignature(val signature: String, val createdAt: Long?, val author: User?) - - /** - * This method caches signatures during each execution to avoid recalculation in longer threads - */ - fun replyLevelSignature( - eventsToConsider: Set, - cachedSignatures: MutableMap, - account: User, - accountFollowingSet: Set, - now: Long, - ): LevelSignature { - val replyTo = replyTo - if ( - event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() - ) { - return LevelSignature( - signature = "/" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) + ";", - createdAt = createdAt(), - author = author, - ) - } - - val parent = - ( - replyTo - .filter { - it.idHex in eventsToConsider - } // This forces the signature to be based on a branch, avoiding two roots - .map { - cachedSignatures[it] - ?: it - .replyLevelSignature( - eventsToConsider, - cachedSignatures, - account, - accountFollowingSet, - now, - ) - .apply { cachedSignatures.put(it, this) } - } - .maxByOrNull { it.signature.length } - ) - - val parentSignature = parent?.signature?.removeSuffix(";") ?: "" - - val threadOrder = - if (parent?.author == author && createdAt() != null) { - // author of the thread first, in **ascending** order - "9" + - formattedDateTime((parent?.createdAt ?: 0) + (now - (createdAt() ?: 0))) + - idHex.substring(0, 8) - } else if (author?.pubkeyHex == account.pubkeyHex) { - "8" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my replies - } else if (author?.pubkeyHex in accountFollowingSet) { - "7" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // my follows replies. - } else { - "0" + formattedDateTime(createdAt() ?: 0) + idHex.substring(0, 8) // everyone else. - } - - val mySignature = - LevelSignature( - signature = parentSignature + "/" + threadOrder + ";", - createdAt = createdAt(), - author = author, - ) - - cachedSignatures[this] = mySignature - return mySignature - } - - fun replyLevel(cachedLevels: MutableMap = mutableMapOf()): Int { - val replyTo = replyTo - if ( - event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() - ) { - return 0 - } - - return replyTo.maxOf { - cachedLevels[it] ?: it.replyLevel(cachedLevels).apply { cachedLevels.put(it, this) } - } + 1 - } - fun addReply(note: Note) { if (note !in replies) { replies = replies + note diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt new file mode 100644 index 000000000..0bc7518b6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -0,0 +1,125 @@ +/** + * 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 com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.RepostEvent +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +data class LevelSignature(val signature: String, val createdAt: Long?, val author: User?) + +object ThreadLevelCalculator { + val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss") + + private fun formattedDateTime(timestamp: Long): String { + return Instant.ofEpochSecond(timestamp) + .atZone(ZoneId.systemDefault()) + .format(levelFormatter) + } + + /** + * This method caches signatures during each execution to avoid recalculation in longer threads + */ + fun replyLevelSignature( + note: Note, + eventsToConsider: Set, + cachedSignatures: MutableMap, + account: User, + accountFollowingSet: Set, + now: Long, + ): LevelSignature { + val replyTo = note.replyTo + if ( + note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() + ) { + return LevelSignature( + signature = "/" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) + ";", + createdAt = note.createdAt(), + author = note.author, + ) + } + + val parent = + ( + replyTo + .filter { + it.idHex in eventsToConsider + } // This forces the signature to be based on a branch, avoiding two roots + .map { + cachedSignatures[it] + ?: replyLevelSignature( + it, + eventsToConsider, + cachedSignatures, + account, + accountFollowingSet, + now, + ).apply { cachedSignatures.put(it, this) } + } + .maxByOrNull { it.signature.length } + ) + + val parentSignature = parent?.signature?.removeSuffix(";") ?: "" + + val threadOrder = + if (parent?.author == note.author && note.createdAt() != null) { + // author of the thread first, in **ascending** order + "9" + + formattedDateTime((parent?.createdAt ?: 0) + (now - (note.createdAt() ?: 0))) + + note.idHex.substring(0, 8) + } else if (note.author?.pubkeyHex == account.pubkeyHex) { + "8" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // my replies + } else if (note.author?.pubkeyHex in accountFollowingSet) { + "7" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // my follows replies. + } else { + "0" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // everyone else. + } + + val mySignature = + LevelSignature( + signature = parentSignature + "/" + threadOrder + ";", + createdAt = note.createdAt(), + author = note.author, + ) + + cachedSignatures[note] = mySignature + return mySignature + } + + fun replyLevel( + note: Note, + cachedLevels: MutableMap = mutableMapOf(), + ): Int { + val replyTo = note.replyTo + if ( + note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() + ) { + return 0 + } + + return replyTo.maxOf { + cachedLevels[it] ?: replyLevel(it, cachedLevels).apply { cachedLevels.put(it, this) } + } + 1 + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt index a4655eed7..a981c189c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt @@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.ui.dal import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LevelSignature import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -33,7 +35,7 @@ class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter { - val cachedSignatures: MutableMap = mutableMapOf() + val cachedSignatures: MutableMap = mutableMapOf() val followingKeySet = account.liveKind3Follows.value.users val eventsToWatch = ThreadAssembler().findThreadFor(noteId) val eventsInHex = eventsToWatch.map { it.idHex }.toSet() @@ -42,15 +44,14 @@ class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter { - it - .replyLevelSignature( - eventsInHex, - cachedSignatures, - account.userProfile(), - followingKeySet, - now, - ) - .signature + ThreadLevelCalculator.replyLevelSignature( + it, + eventsInHex, + cachedSignatures, + account.userProfile(), + followingKeySet, + now, + ).signature } return eventsToWatch.sortedWith(order) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 3bdaf5684..0de0df8c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -79,6 +79,7 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InlineCarrousel import com.vitorpamplona.amethyst.ui.components.LoadNote @@ -247,7 +248,7 @@ fun RenderThreadFeed( item, modifier = Modifier.drawReplyLevel( - item.replyLevel(), + ThreadLevelCalculator.replyLevel(item), MaterialTheme.colorScheme.placeholderText, if (item.idHex == noteId) { MaterialTheme.colorScheme.lessImportantLink @@ -270,7 +271,7 @@ fun RenderThreadFeed( item, modifier = Modifier.drawReplyLevel( - item.replyLevel(), + ThreadLevelCalculator.replyLevel(item), MaterialTheme.colorScheme.placeholderText, if (item.idHex == noteId) { MaterialTheme.colorScheme.lessImportantLink