mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-29 09:02:34 +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.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@Stable
|
@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) {
|
fun addReply(note: Note) {
|
||||||
if (note !in replies) {
|
if (note !in replies) {
|
||||||
replies = replies + note
|
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 androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LevelSignature
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||||
|
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -33,7 +35,7 @@ class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter<No
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
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 followingKeySet = account.liveKind3Follows.value.users
|
||||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
||||||
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
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
|
// Currently orders by date of each event, descending, at each level of the reply stack
|
||||||
val order =
|
val order =
|
||||||
compareByDescending<Note> {
|
compareByDescending<Note> {
|
||||||
it
|
ThreadLevelCalculator.replyLevelSignature(
|
||||||
.replyLevelSignature(
|
it,
|
||||||
eventsInHex,
|
eventsInHex,
|
||||||
cachedSignatures,
|
cachedSignatures,
|
||||||
account.userProfile(),
|
account.userProfile(),
|
||||||
followingKeySet,
|
followingKeySet,
|
||||||
now,
|
now,
|
||||||
)
|
).signature
|
||||||
.signature
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventsToWatch.sortedWith(order)
|
return eventsToWatch.sortedWith(order)
|
||||||
|
@@ -79,6 +79,7 @@ import coil.compose.AsyncImage
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
@@ -247,7 +248,7 @@ fun RenderThreadFeed(
|
|||||||
item,
|
item,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.drawReplyLevel(
|
Modifier.drawReplyLevel(
|
||||||
item.replyLevel(),
|
ThreadLevelCalculator.replyLevel(item),
|
||||||
MaterialTheme.colorScheme.placeholderText,
|
MaterialTheme.colorScheme.placeholderText,
|
||||||
if (item.idHex == noteId) {
|
if (item.idHex == noteId) {
|
||||||
MaterialTheme.colorScheme.lessImportantLink
|
MaterialTheme.colorScheme.lessImportantLink
|
||||||
@@ -270,7 +271,7 @@ fun RenderThreadFeed(
|
|||||||
item,
|
item,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.drawReplyLevel(
|
Modifier.drawReplyLevel(
|
||||||
item.replyLevel(),
|
ThreadLevelCalculator.replyLevel(item),
|
||||||
MaterialTheme.colorScheme.placeholderText,
|
MaterialTheme.colorScheme.placeholderText,
|
||||||
if (item.idHex == noteId) {
|
if (item.idHex == noteId) {
|
||||||
MaterialTheme.colorScheme.lessImportantLink
|
MaterialTheme.colorScheme.lessImportantLink
|
||||||
|
Reference in New Issue
Block a user