From 211305c5727e56949d834f393da1f930d1962f6e Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 8 Jan 2026 10:45:44 +0200 Subject: [PATCH] A1 extract to commons --- .../amethyst/model/LocalCache.kt | 29 +++++++++++++--- .../threadview/dal/LevelFeedViewModel.kt | 2 +- .../threadview/dal/ThreadFeedFilter.kt | 9 ++--- .../FilterMissingEventsForThread.kt | 9 ++--- .../ThreadEventLoaderSubAssembler.kt | 5 +-- .../subassembies/ThreadFilterSubAssembler.kt | 5 +-- .../model/ThreadLevelCalculator.android.kt | 33 +++++++++++++++++++ .../commons}/model/ThreadAssembler.kt | 20 ++++++----- .../commons}/model/ThreadLevelCalculator.kt | 21 +++++------- .../commons/model/cache/ICacheProvider.kt | 18 ++++++++++ .../model/ThreadLevelCalculator.jvm.kt | 33 +++++++++++++++++++ 11 files changed, 146 insertions(+), 38 deletions(-) create mode 100644 commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.android.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/ThreadAssembler.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/ThreadLevelCalculator.kt (92%) create mode 100644 commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.jvm.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1b452063f..d8aeb392c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.cache.IChannel import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel @@ -211,7 +213,7 @@ interface ILocalCache { ) {} } -object LocalCache : ILocalCache { +object LocalCache : ILocalCache, ICacheProvider { val antiSpam = AntiSpamFilter() val users = LargeSoftCache() @@ -316,16 +318,35 @@ object LocalCache : ILocalCache { } } - fun getUserIfExists(key: String): User? { + override fun getUserIfExists(key: String): User? { if (key.isEmpty()) return null return users.get(key) } + override fun countUsers(predicate: (String, Any) -> Boolean): Int { + var count = 0 + users.forEach { key, user -> + if (predicate(key, user)) count++ + } + return count + } + + override fun getAnyChannel(note: Any?): IChannel? { + val channelNote = note as? Note ?: return null + val channel = getAnyChannel(channelNote) + // Wrap Channel to implement IChannel interface + return channel?.let { + object : IChannel { + override fun relays(): List? = it.relays().toList() + } + } + } + fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) - fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) } + override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) } fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) @@ -356,7 +377,7 @@ object LocalCache : ILocalCache { return null } - fun checkGetOrCreateNote(key: String): Note? { + override fun checkGetOrCreateNote(key: String): Note? { if (ATag.isATag(key)) { return checkGetOrCreateAddressableNote(key) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt index 3d006fffb..53742b7c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt index f1ae4c13a..ec0fd2ed7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.model.LevelSignature +import com.vitorpamplona.amethyst.commons.model.ThreadAssembler +import com.vitorpamplona.amethyst.commons.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LevelSignature +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.ThreadAssembler -import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableSet @@ -40,7 +41,7 @@ class ThreadFeedFilter( override fun feed(): List { val cachedSignatures: MutableMap = mutableMapOf() val followingKeySet = account.kind3FollowList.flow.value.authors - val eventsToWatch = ThreadAssembler().findThreadFor(noteId) ?: return emptyList() + val eventsToWatch = ThreadAssembler(LocalCache).findThreadFor(noteId) ?: return emptyList() // Filter out drafts made by other accounts on device val filteredEvents = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 8e0ad15d1..93ba583f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies +import com.vitorpamplona.amethyst.commons.model.ThreadAssembler import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress @@ -53,9 +53,10 @@ fun filterMissingEventsForThread( val missingAddresses = mapOfSet { - if (threadInfo.root.event == null && threadInfo.root is AddressableNote) { - potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl -> - add(relayUrl, threadInfo.root.address) + val rootNote = threadInfo.root + if (rootNote.event == null && rootNote is AddressableNote) { + potentialRelaysToFindEvent(rootNote).ifEmpty { defaultRelays }.forEach { relayUrl -> + add(relayUrl, rootNote.address) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt index 077bb01f4..841c580a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.commons.model.ThreadAssembler +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState @@ -45,7 +46,7 @@ class ThreadEventLoaderSubAssembler( key: ThreadQueryState, since: SincePerRelayMap?, ): List? { - val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null + val branches = ThreadAssembler(LocalCache).findThreadFor(key.eventId) ?: return null val defaultRelays = key.account.followPlusAllMineWithSearch.flow.value return filterMissingEventsForThread(branches, defaultRelays) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt index 173d53afb..0ee821b77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.commons.model.ThreadAssembler +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState @@ -42,7 +43,7 @@ class ThreadFilterSubAssembler( key: ThreadQueryState, since: SincePerRelayMap?, ): List? { - val root = ThreadAssembler().findRoot(key.eventId) ?: return null + val root = ThreadAssembler(LocalCache).findRoot(key.eventId) ?: return null return filterEventsInThreadForRoot(root, since) } diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.android.kt new file mode 100644 index 000000000..8c8cea523 --- /dev/null +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.android.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 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.commons.model + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +private val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss") + +actual fun formattedDateTime(timestamp: Long): String = + Instant + .ofEpochSecond(timestamp) + .atZone(ZoneId.systemDefault()) + .format(levelFormatter) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt index 03ba05a3d..0985294fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt @@ -18,10 +18,11 @@ * 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 +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -29,7 +30,9 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet -class ThreadAssembler { +class ThreadAssembler( + private val cache: ICacheProvider, +) { private fun searchRoot( note: Note, testedNotes: MutableSet = mutableSetOf(), @@ -48,9 +51,10 @@ class ThreadAssembler { ?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" } ?.getOrNull(1) if (markedAsRoot != null) { - // Check to ssee if there is an error in the tag and the root has replies - if (LocalCache.getNoteIfExists(markedAsRoot)?.replyTo?.isEmpty() == true) { - return LocalCache.checkGetOrCreateNote(markedAsRoot) + // Check to see if there is an error in the tag and the root has replies + val rootNote = cache.getNoteIfExists(markedAsRoot) as? Note + if (rootNote?.replyTo?.isEmpty() == true) { + return cache.checkGetOrCreateNote(markedAsRoot) as? Note } } @@ -84,7 +88,7 @@ class ThreadAssembler { ) fun findRoot(noteId: String): Note? { - val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null + val note = cache.checkGetOrCreateNote(noteId) as? Note ?: return null return if (note.event != null) { val thread = OnlyLatestVersionSet() @@ -98,7 +102,7 @@ class ThreadAssembler { fun findThreadFor(noteId: String): ThreadInfo? { checkNotInMainThread() - val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null + val note = cache.checkGetOrCreateNote(noteId) as? Note ?: return null return if (note.event != null) { val thread = OnlyLatestVersionSet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.kt index 8b066ce09..ed2234528 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.kt @@ -18,15 +18,12 @@ * 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 +package com.vitorpamplona.amethyst.commons.model import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import java.lang.Long.min -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter +import kotlin.math.min data class LevelSignature( val signature: String, @@ -34,15 +31,13 @@ data class LevelSignature( val author: User?, ) +/** + * Platform-specific date-time formatter for thread signatures. + * Returns formatted timestamp in pattern "uuuu-MM-dd-HH:mm:ss" + */ +expect fun formattedDateTime(timestamp: Long): String + object ThreadLevelCalculator { - val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss") - - private fun formattedDateTime(timestamp: Long): String = - Instant - .ofEpochSecond(timestamp) - .atZone(ZoneId.systemDefault()) - .format(levelFormatter) - /** * This method caches signatures during each execution to avoid recalculation in longer threads */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt index ab9fb6a6d..d1d0d8de8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -61,6 +61,24 @@ interface ICacheProvider { * @return Count of users matching the predicate */ fun countUsers(predicate: (String, Any) -> Boolean): Int + + /** + * Gets a Note if it exists in cache. + * Used by ThreadAssembler for finding existing notes. + * + * @param hexKey The note's ID in hex format + * @return The Note if exists in cache, null otherwise + */ + fun getNoteIfExists(hexKey: HexKey): Any? + + /** + * Gets an existing Note or creates a new one if it doesn't exist. + * Used by ThreadAssembler for building thread structures. + * + * @param hexKey The note's ID in hex format + * @return The Note (existing or newly created) + */ + fun checkGetOrCreateNote(hexKey: HexKey): Any? } /** diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.jvm.kt new file mode 100644 index 000000000..8c8cea523 --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadLevelCalculator.jvm.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 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.commons.model + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +private val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss") + +actual fun formattedDateTime(timestamp: Long): String = + Instant + .ofEpochSecond(timestamp) + .atZone(ZoneId.systemDefault()) + .format(levelFormatter)