mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-28 18:17:07 +02:00
Moves the thread formatter and calculator out of Note
This commit is contained in:
@@ -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<HexKey>,
|
||||
cachedSignatures: MutableMap<Note, LevelSignature>,
|
||||
account: User,
|
||||
accountFollowingSet: Set<String>,
|
||||
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<Note, Int> = 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
|
||||
|
@@ -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<HexKey>,
|
||||
cachedSignatures: MutableMap<Note, LevelSignature>,
|
||||
account: User,
|
||||
accountFollowingSet: Set<String>,
|
||||
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<Note, Int> = 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
|
||||
}
|
||||
}
|
@@ -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<No
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val cachedSignatures: MutableMap<Note, Note.LevelSignature> = mutableMapOf()
|
||||
val cachedSignatures: MutableMap<Note, LevelSignature> = 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<No
|
||||
// Currently orders by date of each event, descending, at each level of the reply stack
|
||||
val order =
|
||||
compareByDescending<Note> {
|
||||
it
|
||||
.replyLevelSignature(
|
||||
eventsInHex,
|
||||
cachedSignatures,
|
||||
account.userProfile(),
|
||||
followingKeySet,
|
||||
now,
|
||||
)
|
||||
.signature
|
||||
ThreadLevelCalculator.replyLevelSignature(
|
||||
it,
|
||||
eventsInHex,
|
||||
cachedSignatures,
|
||||
account.userProfile(),
|
||||
followingKeySet,
|
||||
now,
|
||||
).signature
|
||||
}
|
||||
|
||||
return eventsToWatch.sortedWith(order)
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user